簡單的代碼跟蹤,順便弄清了之前一直困惑的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。