天天看點

[MySQL學習] Innodb change buffer(2) 相關函數及流程A.相關結構體B.何時決定使用ibuf:C.何時進行ibuf 合并

簡單的代碼跟蹤,順便弄清了之前一直困惑的bp->watch的用途。。。。

////////////////////////////////

在介紹ibuf在innodb中的使用前,我們先介紹下相關的結構體及全局變量。

我們知道通過ibuf可以緩沖多種操作類型,每種操作類型,在内部都有一個宏與之對應:

ibuf_op_insert

ibuf_op_delete_mark

ibuf_op_delete

至于對update操作的緩沖,由于二級索引記錄的更新是先delete-mark,再insert,是以其ibuf實際有兩條記錄ibuf_op_delete_mark+ibuf_op_insert

ibuf是全局對象,用于控制change buffer的控制對象,從ibuf_struct結構體來看,其中存儲了ibuf索引樹資訊和其他一些統計資訊。

ibuf_flush_count計數器,計算調用ibuf_should_try的次數

ibuf_use用于内部标示目前使用的change buffer 類型

由于從5.5開始擴充了change buffer緩沖的操作類型,是以在ibuf記錄的格式也需要做變化,需要記錄在同一個page上的操作計數器并标示操作類型

ibuf entry的格式在ibuf0ibuf.c檔案頭部的注釋中有較長的描述:

4位元組

space id

1位元組,marker (0)

區分老版本

page no

類型資訊,包括:

5.5特有的counter(2位元組)

、操作類型(1位元組)

、flags1(1位元組,

值為ibuf_rec_compact.

剩下的是實際資料

當我們更新一條資料的時候,首先是更新聚集索引記錄,然後再更新二級索引,當通過聚集索引記錄尋找搜尋二級索引btree時,會做判斷是否可以進行ibuf,判斷函數為ibuf_should_try

row_update_for_mysql->row_upd_step->row_upd->row_upd_sec_step->row_upd_sec_index_entry->row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

而對于二級索引purge操作的緩沖,則調用如下backtrace:

row_purge->row_purge_del_mark->row_purge_remove_sec_if_poss->row_purge_remove_sec_if_poss_leaf->row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

可以看到最終的backtrace都彙總到row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

是以以下我們也不區分對待這兩種backtrace類型

 ibuf_should_try作為基礎判斷是否使用ibuf,其判斷邏輯為:

1.打開了change buffer(即ibuf_use != ibuf_use_none)

2.不是聚集索引,聚集索引不可以做ibuf

3.對于唯一索引,不緩存插入操作(btr_insert_op)

當判斷可以緩存時,對ibuf_flush_count++,每四次(ibuf_flush_count % 4 == 0),調用一次buf_lru_try_free_flushed_blocks,嘗試去把buffer pool中lru連結清單上幹淨的block(已經和磁盤同步)轉移到free list上。這樣做的目的是盡量把幹淨的block放到free list,防止在希望使用ibuf時,依然能讀到該二級索引頁并進行修改,這就達不到使用ibuf的目的。髒頁的增加會加重io線程的負擔。

但從5.6的代碼來看,這一步是被移除掉的。這麼做有什麼優化麼?值得測試。

以上步驟都是在從btree上檢索到葉子節點時,才會去做判斷,因為根節點和非葉子節點不可以做ibuf。

當判斷可以使用ibuf時,根據btr_op判斷使用什麼樣的buf_mode,然後作為參數傳遞給buf_page_get_gen,這樣就可以在從buffer pool中讀取page時,決定是否從磁盤讀取檔案頁。

            buf_mode = btr_op == btr_delete_op

                ? buf_get_if_in_pool_or_watch

                : buf_get_if_in_pool;

如果是purge操作(btr_delete_op),buf_mode為buf_get_if_in_pool_or_watch,其他類型的可ibuf的操作為buf_get_if_in_pool

對于不可ibuf的操作,buf_mode值為buf_get

這幾個宏變量分别代表如下意義:

buf_get

總是要擷取到檔案page,如果bp沒有,則從磁盤讀進來

buf_get_if_in_pool

隻從bp讀取檔案page

buf_peek_if_in_pool

隻從bp讀取檔案page,并且不在lru連結清單中置其為young

buf_get_no_latch

和buf_get類似,但不在page上加latch

buf_get_if_in_pool_or_watch

隻從bp讀取檔案page,如果沒有的話,則在這個page上設定一個watch

buf_get_possibly_freed

和buf_get類似,但不care這個page是否已經被釋放了

其他的倒還好了解,這裡的watch是個神馬東東呢?從buf_page_get_gen來看,當從buffer pool的page hash中找不到對應的block時,會做如下處理:

        if (mode == buf_get_if_in_pool_or_watch) {

            block = (buf_block_t*) buf_pool_watch_set(

                space, offset, fold);

            if (univ_likely_null(block)) {

                block_mutex = buf_page_get_mutex((buf_page_t*)block);

                ut_a(block_mutex);

                ut_ad(mutex_own(block_mutex));

                goto got_block;

            }    

        } 

在每個buffer pool的控制結構體中,有一個成員buf_pool->watch[buf_pool_watch_size],該數組類型為buf_page_t,修改或通路該數組需要持有bp->mutex鎖或者bp->zip_mutex。

目前buf_pool_watch_size值為1,而在5.6中這個值為purge線程數加1。

我們來看看函數buf_pool_watch_set幹啥了。

首先從page hash中根據指定的space id 和page no查找page,如果查找到了,說明可能已經有線程把這個page讀到了bp中,如果這個bpage不屬于bp->watch數組中的一員,就直接傳回這個page。

 如果在page hash中沒有的話,就檢視bp->watch數組成員的狀态,在5.5中隻有一個成員。

如果bp->watch[]的state是buf_block_pool_watch,則将目前請求的page進行進行指派:

            bpage->state = buf_block_zip_page;

            bpage->space = space;

            bpage->offset = offset;

            bpage->buf_fix_count = 1; 

            bpage->buf_pool_index = buf_pool_index(buf_pool);

            ut_d(bpage->in_page_hash = true);

            hash_insert(buf_page_t, hash, buf_pool->page_hash,

                    fold, bpage);

bp->watch[]的狀态被設定為buf_block_zip_page,這樣可以保證一次隻會設定一個watch的page,然後把請求的page no和space id都指派給page,并将其插入到page hash中。

 如果bp->watch[]的state為buf_block_zip_page的話,就不做插入。

在設定為bp->wath[]後就直接傳回null.

在從磁盤讀入檔案塊的時候,會調用buf_page_init_for_read->buf_page_init初始化一個block,這時候會做一個判斷,如果将被讀入的page被設定為sentinel(在watch數組中被設定),則調用buf_pool_watch_remove将其從page hash中移除,并對bp->watch進行重置,但block->page的buf_fix_count會被設定+1,以防止這個page被替換出去。

buf_pool_watch_occurred函數用于檢測目前page是否依然被watch住。我們可以看到,它是在ibuf_insert_low被調用到。

    if (op == ibuf_op_delete

        && (min_n_recs < 2

        || buf_pool_watch_occurred(space, page_no))) {

op == ibuf_op_delete 表示該操作類型是purge操作,如果purge操作會導緻page為空,或者剛剛被設定為watch的頁面被讀入了bp,那麼就走實際的記錄purge流程,不做purge的緩沖操作。

我們繼續回到函數btr_cur_search_to_nth_level,如果二級索引page不在bp中,那麼就開始真正的ibuf記錄建立流程,針對不同的操作,為函數ibuf_insert傳遞不同的參數。對于purge操作略有不同,在調用ibuf_insert之前要先判斷該二級索引記錄是否可以被purge(row_purge_poss_sec,當該二級索引記錄對應的聚集索引記錄沒有delete mark并且其trx id比目前的purge view還舊時,不可以做purge操作);當完成ibuf_insert後,還需要移除watch的page(buf_pool_watch_unset)

ibuf_insert是建立ibuf entry的接口函數,

a.首先檢查對應操作的ibuf是否已經開啟(由參數innodb_change_buffering決定)

對于ibuf_op_insert/ibuf_op_delete_mark操作,需要做一些額外的檢查(goto check_watch),檢查page hash中是否已經有該page(剛剛被讀入bp或者被一個purge操作設定為watch),如果存在,則直接傳回false,不走ibuf

這麼做的原因是,如果在purge線程進行的過程中,一條insert/delete_mark操作嘗試緩存同樣page上的操作時,purge不應該被緩存,因為他可能移除一條随後被重新插入的記錄。簡單起見,在有一個purge pending的請求時,我們讓随後對該page的ibuf操作都失效。

如果這裡的insert/delete_mark的ibuf操作失效,那麼随後必然要去讀取相應的二級索引頁,這可以保證之前pending的purge操作先被合并掉。

是以bp->watch還有個作用,就是告訴其他使用者線程,對這個page上已經有一個purge被緩存了。

b.檢查操作的記錄是否大于空page可用空間的1/2,如果大于的話,也不可以使用ibuf,傳回false.

c.調用ibuf_insert_low插入ibuf entry,這裡和普通的insert的樂觀/悲觀插入類似,也根據是否産生ibuf btree分裂分為兩種情況:

    err = ibuf_insert_low(btr_modify_prev, op, no_counter,

                  entry, entry_size,

                  index, space, zip_size, page_no, the);

    if (err == db_fail) {

        err = ibuf_insert_low(btr_modify_tree, op, no_counter,

                      entry, entry_size,

                      index, space, zip_size, page_no, the);

    }  

d.ibuf_insert_low

–>首先判斷ibuf->size >= ibuf->max_size + ibuf_contract_do_not_insert,這表明目前change buffer太大了,需要

ibuf->max_size是一個常量(在函數ibuf_init_at_db_start中進行初始化),表示一半的buffer pool大小所容納的ibuf page數。

ibuf->size表示目前的ibuf page數,當這個值過大時,需要做一次同步ibuf tree收縮(ibuf_contract->ibuf_contract_ext),随機的選取一個ibuf tree上的葉子節點上(btr_pcur_open_at_rnd_pos),每次最多選擇8個(ibuf_max_n_pages_merged) 二級索引頁進行merge(ibuf_get_merge_page_nos),然後将選擇的page讀入記憶體中(buf_read_ibuf_merge_pages),在讀入的時候,會進行merge操作。

至于具體如何合并,下一篇再議。

–>然後建構ibuf entry

    ibuf_entry = ibuf_entry_build(

        op, index, entry, space, page_no,

        no_counter ? ulint_undefined : 0xffff, heap);

如果需要對ibuf的btree進行pessimistic insert(mode == btr_modify_tree),還需要保證ibuf btree上有足夠的page(ibuf_data_enough_free_for_insert),如果不夠,則需要擴充空閑塊(ibuf_add_free_page)

然後開啟一個mini transaction(ibuf_mtr_start),并将cursor定位到ibuf btree的對應位置:btr_pcur_open(ibuf->index, ibuf_entry, page_cur_le, mode, &pcur, &mtr);

–> 計算已經為該page配置設定的ibuf entry大小

    buffered = ibuf_get_volume_buffered(&pcur, space, page_no,

                        op == ibuf_op_delete

                        ? &min_n_recs

                        : null, &mtr);

min_n_recs表示在為目前page應用所有的ibuf entry後還剩下的記錄數。

–>當目前操作為purge操作(ibuf_op_delete)且操作的二級索引page上隻剩下一條記錄或者操作的page被讀入了bp中(buf_pool_watch_occurred),則不進行buffer操作,

–>讀入操作page對應的ibuf bitmap page

    bitmap_page = ibuf_bitmap_get_map_page(space, page_no,

                           zip_size, &bitmap_mtr);

如果該page讀入了bp或者該page上有顯示鎖,不進行buffer操作(何時會發生呢?)

對于insert操作,需要去檢查該page是否能夠滿足插入空間大小,從bitmap_page中找到目前二級索引page對應的bit位(ibuf_bitmap_page_get_bits),獲得該page上還能寫入的空閑空間(ibuf_index_page_calc_free_from_bits),如果新記錄加不上的話,則需要對該page上的ibuf entry進行合并,然後退出,不進行buffer操作

–>設定目前ibuf的counter(ibuf_set_entry_counter),如果隻用到了insert的ibuf,則無需設定counter

在設定完counter後,需要更新bitmap_page上對應二級索引頁的ibuf_bitmap_buffered為true,表名這個page上緩存的ibuf entry.

–>在完成上述流程後,調用btr_cur_optimistic_insert/btr_cur_pessimistic_insert向ibuf btree中插入記錄。

如果使用的是悲觀插入,還需要更新ibuf(ibuf_size_update),并在後面進行一次ibuf收縮(ibuf_contract_after_insert)

相應的,該二級索引頁的max trx id也需要更新(page_update_max_trx_id)

以上介紹的比較粗略,還有很多細節需要深入。

ibuf merge可以在多個地方發生,在使用者線程中,當發現ibuf tree的空閑空間不夠,或者發生ibuf tree分裂時,會去做合并,以收縮ibuf,防止過于膨脹。

在master線程中,也可能去做ibuf merge srv_master_thread->ibuf_contract_for_n_pages,每10秒必有一次merge,在系統空閑時,也會去嘗試做ibuf merge。通過喚醒異步io線程讀入page,異步io線程在讀入page後,會進行merge操作io_handler_thread->fil_aio_wait->buf_page_io_complete

buf_merge_or_delete_for_page是進行change buffer 合并的核心函數,先來看看在什麼地方會調用這個函數:

在三個地方會調用ibuf_merge_or_delete_for_page

1.buf_page_create

       ibuf_merge_or_delete_for_page(null, space, offset, zip_size, true);

2.buf_page_get_gen  //針對壓縮表

        if (univ_likely(!recv_no_ibuf_operations)) {

            ibuf_merge_or_delete_for_page(block, space, offset,

                              zip_size, true);

        }    

這裡主要針對壓縮表,在對檔案頁進行解壓後,會調用ibuf_merge_or_delete_for_page。

這裡存在過度調用ibuf_merge_or_delete_for_page的問題(http://bugs.mysql.com/bug.php?id=65886)

對于非壓縮頁,在buf_page_io_complete中會調用函數做ibuf merge。

對于壓縮頁,則在解壓後做ibuf merge。

這裡存在的問題有兩個,一個是壓縮頁的解壓頁可能會被驅逐(隻在記憶體中保留壓縮頁),如果下次要用,就需要去解壓;

另外一種情況是,壓縮頁被預讀到記憶體中(read ahead),隻在用到的時候才解壓。

根據ibuf merge的規則,隻有在第一次從磁盤讀取對應檔案頁到記憶體時,才需要去合并ibuf。

是以第一種情況實際上是無需去進行ibuf merge的,在io-bound的場景下,這可能會比較頻繁,進而影響到性能,因為當io吃緊時,壓縮表優先選擇釋放解壓頁

3.buf_page_io_complete           //為非壓縮頁merge ibuf

        if (uncompressed && !recv_no_ibuf_operations) {

            ibuf_merge_or_delete_for_page(

這裡也是主要的合并ibuf的方式,在讀入一個page的io完成後,進行ibuf entry的合并。

說起change buffer,就不得不提到一個有名的bug,在2011年report的bug61104(http://bugs.mysql.com/bug.php?id=61104),在crash recovery時,合并ibuf中的delete操作時,發現二級索引page上記錄為空,導緻斷言失敗(沒有記錄,怎麼做delete合并呢?)

直到去年(2012)年percona的alexey kopytov送出了bug#66819(http://bugs.mysql.com/bug.php?id=66819),指出change buffer并不是crash-safe的。特别是對于delete操作,在執行完delete操作(mtr commit)才會去删除ibuf記錄。

以下代碼選自ibuf_merge_or_delete_for_page:

首先讀取ibuf記錄,進行merge操作,針對不同的操作類型,走不同的分支,ibuf_ip_delete有些特殊,它這裡直接commit了mtr 

            case ibuf_op_delete:

                ibuf_delete(entry, block, dummy_index, &mtr);

                /* because ibuf_delete() will latch an

                insert buffer bitmap page, commit mtr

                before latching any further pages.

                store and restore the cursor position. */

                ut_ad(rec == btr_pcur_get_rec(&pcur));

                ut_ad(page_rec_is_user_rec(rec));

                ut_ad(ibuf_rec_get_page_no(&mtr, rec) 

                      == page_no);

                ut_ad(ibuf_rec_get_space(&mtr, rec) == space);

                btr_pcur_store_position(&pcur, &mtr);

                ibuf_btr_pcur_commit_specify_mtr(&pcur, &mtr);

我們知道,在innodb中,被送出的mtr日志也就是redo 日志,如果被其他線程刷到了磁盤,實際上就相當于對這個page的一次ibuf merge的完成。

随後,會去嘗試删除ibuf記錄,如下:

        /* delete the record from ibuf */

        if (ibuf_delete_rec(space, page_no, &pcur, search_tuple,

                    &mtr)) {

            /* deletion was pessimistic and mtr was committed:

            we start from the beginning again */

在函數ibuf_delete_rec中,如果btr_cur_optimistic_delete失敗,會先把mtr送出,然後再做btr_cur_pessimistic_delete。

在mtr送出和做btr_cur_pessimistic_delete之間crash的話,ibuf記錄和實際資料就可能處于不一緻狀态。如果crash之前删除的記錄後,page上隻剩下最後一條記錄,在crash recovery時,ibuf記錄還在的話,就會調用ibuf_delete去繼續重複的apply ibuf記錄,觸發斷言錯誤,如下:

ibuf_delete:

        /* refuse to delete the last record. */

        ut_a(page_get_n_recs(page) > 1);   

斷言的目的是在函數ibuf_insert_low中能夠確定索引頁中至少有一條記錄,因為change buffer在生成ibuf entry時已經保證了這一點。

官方已經放出了patch:

http://bazaar.launchpad.net/~mysql/mysql-server/5.5/revision/3979

從官方的修複來看,在ibuf_delete_rec中,在進行btr_cur_pessimistic_delete之前,先把ibuf記錄設定為标記删除;這樣如果發生crash,重新開機後就不會再應用這條ibuf.

不過目前官方的fix還不完整,沒有修複ibuf_op_delete操作,期望下一個版本修複這個問題,目前線上設定為inserts。

繼續閱讀