我們知道innodb的索引組織結構為btree。通常情況下,我們需要根據查詢條件,從根節點開始尋路到葉子節點,找到滿足條件的記錄。為了減少尋路開銷,innodb本身做了幾點優化。
首先,對于連續記錄掃描,innodb在滿足比較嚴格的條件時采用row cache的方式連續讀取8條記錄(并将記錄格式轉換成mysql format),存儲線上程私有的<code>row_prebuilt_t::fetch_cache</code>中;這樣一次尋路就可以擷取多條記錄,在server層處理完一條記錄後,可以直接從cache中取資料而無需再次尋路,直到cache中資料取完,再進行下一輪。
另一種方式是,當一次進入innodb層獲得資料後,在傳回server層前,目前在btree上的cursor會被暫時存儲到<code>row_prebuilt_t::pcur</code>中,當再次傳回innodb層撈資料時,如果對應的block沒有發生任何修改,則可以繼續沿用之前存儲的cursor,無需重新定位。
上面這兩種方式都是為了減少了重新尋路的次數,而對于一次尋路的開銷,則使用adaptive hash index來解決。ahi是一個記憶體結構,嚴格來說不是傳統意義上的索引,可以把它了解為建立在btree索引上的“索引”。
本文代碼分析基于mysql 5.7.7-rc,描述的邏輯适用于5.7.7之前及5.6版本。但在即将釋出的mysql-5.7.8版本中, innodb根據索引id對ahi進行了分區處理,以此來降低btr_search_latch讀寫鎖競争,由于尚未釋出,本文暫不覆寫相關内容。
我們以一個幹淨啟動的執行個體作為起點,分析下如何進行ahi建構的過程。
ahi在記憶體中表現就是一個普通的哈希表對象,存儲在<code>btr_search_sys_t::hash_index</code>中,對ahi的查删改操作都是通過一個全局讀寫鎖<code>btr_search_latch</code>來保護。
在執行個體啟動,完成buffer pool初始化後,會初始化ahi子系統相關對象,并配置設定ahi記憶體,大小為buffer pool的1/64。
參考函數:<code>btr_search_sys_create</code>
tips:mysql 5.7已經開始支援innodb buffer pool的動态調整,其政策是buffer pool的大小改變超過1倍,就重新配置設定ahi hash記憶體(<code>btr_search_sys_resize</code>)。
在系統剛啟動時,索引對象上沒有足夠的資訊來啟發是否适合進行ahi緩存,是以開始有個資訊搜集的階段,在索引對象上維護了<code>dict_index_t::search_info</code>,類型為<code>btr_search_t</code>,用于跟蹤目前索引使用ahi的關鍵資訊。
在第一次執行sql時,需要從btree的root節點開始,當尋址到比對的葉子節點時,會走如下邏輯:
btr_cur_search_to_nth_level:
這裡會判斷髒讀ahi開關(btr_search_enabled)是否打開,以及<code>index->diable_ahi</code>是否為false。第二個條件是mysql5.7對臨時表的優化,避免臨時表操作對全局對象的影響,針對臨時表不做ahi建構。
我們看看函數btr_search_info_update的邏輯:
對<code>info->hash_analysis++</code>,當<code>info->hash_analysis</code>值超過<code>btr_search_hash_analysis</code>(17)時,也就是說對該索引尋路到葉子節點17次後,才會去做ahi分析(進入步驟2)
進入函數<code>btr_search_info_update_slow</code>
在連續執行17次對相同索引的操作後,滿足<code>info->hash_analysis</code>大于等于<code>btr_search_hash_analysis</code>的條件,就會調用函數<code>btr_search_info_update_slow</code>來更新search_info,這主要是為了避免頻繁的索引查詢分析産生的過多cpu開銷。
innodb通過索引條件建構一個可用于查詢的tuple,而ahi需要根據tuple定位到葉子節點上記錄的位置,既然ahi是建構在btree索引上的索引,它的鍵值就是通過索引的前n列的值計算的來,所有的資訊搜集統計都是為了确定一個合适的”N” ,這個值也是個動态的值,會跟随應用的負載自适應調整并觸發block上的ahi重建構。
<code>btr_search_info_update_slow</code>包含三個部分:更新索引查詢資訊、block上的查詢資訊以及為目前block建構ahi,下面幾小節分别介紹。
參考函數:<code>btr_search_info_update_hash</code>
這裡涉及到的幾個search_info變量包括:
<code>btr_search_t::n_hash_potential</code> 表示如果使用ahi建構索引,潛在的可能成功的次數;
<code>btr_search_t::hash_analysis</code> 若設定了新的建議字首索引模式,則重置為0,随後的17次查詢分析可以忽略更新search_info。
下面兩個字段表示推薦的字首索引模式:
<code>btr_search_t::n_fields</code> 推薦建構ahi的索引列數;
<code>btr_search_t::left_side</code> 表示是否在相同索引字首的最左索引記錄建構ahi;值為true時,則對于相同字首索引的記錄,隻存儲最右的那個記錄。
通過n_fields和left_side可以指導選擇哪些列作為索引字首來建構(fold, rec)哈希記錄。如果使用者的sql的索引字首列的個數大于等于建構ahi時的字首索引,就可以用上ahi。
兩種情況需要建構建議的字首索引列:
目前是第一次為該索引做ahi分析,<code>btr_search_t::n_hash_potential</code>值為0,需要建構建議的字首索引列;
新的記錄比對模式發生了變化<code>(info->left_side == (info->n_fields <=cursor->low_match))</code>,需要重新設定字首索引列。
相關代碼段:
從上述代碼可以看到,在low_match和up_match之間,選擇小一點match的索引列數的來進行設定,但不超過唯一确定索引記錄值的列的個數:
當low_match小于up_match時,left_side設定為true,表示相同字首索引的記錄隻緩存最左記錄;
當low_match大于up_match時,left_side設定為false,表示相同字首索引的記錄隻緩存最右記錄。
如果不是第一次進入seach_info分析,有兩種情況會遞增<code>btr_search_t::n_hash_potential</code>:
本次查詢的up_match和目前推薦的字首索引都能唯一決定一條索引記錄(例如唯一索引),則根據search_info推薦的字首索引列建構ahi肯定能命中,遞增 <code>info->n_hash_potential</code>;
本次查詢的tuple可以通過建議的字首索引列建構的ahi定位到。
很顯然,如果對同一個索引的查詢交替使用不同的查詢模式,可能上次更新的search_info很快就會被重新設定,具有固定模式的索引查詢将會受益于ahi索引。
參考函數:<code>btr_search_update_block_hash_info</code>
更新資料頁block上的查詢資訊,涉及到修改的變量包括:
<code>btr_search_info::last_hash_succ</code> 最近一次成功(或可能成功)使用ahi;
<code>buf_block_t::n_hash_helps</code> 計數值,如果使用目前推薦的字首索引列建構ahi可能命中的次數,用于啟發建構/重新建構資料頁上的ahi記錄項;
<code>buf_block_t::n_fields</code> 推薦在block上建構ahi的字首索引列數;
<code>buf_block_t::left_side</code> 和search_info上對應字段含義相同。
函數主要流程包括:
首先設定<code>btr_search_info::last_hash_succ</code> 為false
這會導緻在分析過程中無法使用ahi進行檢索,感覺這裡的設定不是很合理。這意味着每次分析一個新的block,都會導緻ahi短暫不可用。
初始化或更新block上的查詢資訊
當block第一次被touch到并進入該函數時,設定block上的建議索引列值;以後再進入時,如果和索引上的全局search_info相比對,則遞增<code>block->n_hash_helps</code>,啟發後續的建立或重建構ahi。
如果目前資料頁block上已經建構了ahi記錄項,且<code>buf_block_t::curr_n_fields</code>等字段和<code>btr_search_info</code>上對應字段值相同時,則認為目前sql如果使用ahi索引能夠命中,是以将<code>btr_search_info::last_hash_succ</code>設定為true,下次再使用相同索引檢索btree時就會嘗試使用ahi。
在初始化或更新block上的變量後,需要判斷是否為整個page建構ahi索引:
簡單來說,當滿足下面三個條件時,就會去為整個block上建構ahi記錄項:
分析使用ahi可以成功查詢的次數(<code>buf_block_t::n_hash_helps</code>)超過block上記錄數的16(<code>btr_search_page_build_limit</code>)分之一;
<code>btr_search_info::n_hash_potential</code>大于等于<code>btr_search_build_limit</code> (100),表示連續100次潛在的成功使用ahi可能性;
尚未為目前block構造過索引、或者目前block上已經建構了ahi索引且<code>block->n_hash_helps</code>大于page上記錄數的兩倍、或者目前block上推薦的字首索引列發生了變化 。
如果在上一階段判斷認為可以為目前page建構ahi索引(函數<code>btr_search_update_block_hash_info</code>傳回值為true),則根據目前推薦的索引字首進行ahi建構。
參考函數:<code>btr_search_build_page_hash_index</code>
分為三個階段:
檢查階段:加btr_search_latch的s鎖,判斷ahi開關是否打開;如果block上已經建構了老的ahi但字首索引列和目前推薦的不同,則清空block對應的ahi記錄項(<code>btr_search_drop_page_hash_index</code>);檢查n_fields和page上的記錄數;然後釋放btr_search_latch的s鎖;
搜集階段:根據推薦的索引列數計算記錄fold值,将對應的資料頁記錄記憶體位址到數組裡;
根據left_mode值,相同的字首索引列值會有不同的行為,舉個簡單的例子,假設page上記錄為 (2,1), (2,2), (5, 3), (5, 4), (7, 5), (8, 6),n_fields=1
若left_most為true,則hash存儲的記錄為(2,1) , (5, 3), (7, 5), (8,6)
若left_most為false,則hash存儲的記錄為(2, 2), (5, 4), (7,5), (8, 6)
插入階段:加btr_search_latch的x鎖,将第二階段搜集的(fold, rec)插入到ahi中,并更新:
ps:由于第二階段釋放了btr_search_latch鎖,這裡還得判斷block上的ahi資訊是否發生了變化,如果block上已經建構了ahi且block->curr_*幾個變量和目前嘗試建構的檢索模式不同,則放棄本次建構。
ahi的目的是根據使用者提供的查詢條件加速定位到葉子節點,一般如果有固定的查詢pattern,都可以通過ahi受益,尤其是btree高度比較大的時候。
入口函數:<code>btr_cur_search_to_nth_level</code>
相關代碼:
從代碼段可以看出,需要滿足如下條件才能夠使用ahi:
沒有加btr_search_latch寫鎖。如果加了寫鎖,可能操作時間比較耗時,走ahi檢索記錄就得不償失了;
latch_mode <= btr_modify_leaf,表明本次隻是一次不變更btree結構的dml或查詢(包括等值、range等查詢)操作;
<code>btr_search_info::last_hash_succ</code>為true表示最近一次使用ahi成功(或可能成功)了;
打開ahi開關;
查詢優化階段的估值操作,例如計算range範圍等,典型的堆棧包括:<code>handler::multi_range_read_info_const</code> –> <code>ha_innobase::records_in_range</code> –> <code>btr_estimate_n_rows_in_range</code> –> <code>btr_cur_search_to_nth_level</code>;
不是spatial索引;
調用者無需配置設定外部存儲頁(btr_modify_external,主要用于輔助寫入大的blob資料,參考struct btr_blob_log_check_t)。
當滿足上述條件時,進入函數<code>btr_search_guess_on_hash</code>,根據目前的查詢tuple對象計算fold,并查詢ahi;隻有目前檢索使用的tuple列的個數大于等于建構ahi的列的個數時,才能夠使用ahi索引。
<code>btr_search_guess_on_hash</code>:
首先使用者提供的字首索引查詢條件必須大于等于建構ahi時的字首索引列數,這裡存在一種可能性:索引上的search_info的n_fields 和block上建構ahi時的cur_n_fields值已經不相同了,但是我們并不知道本次查詢到底落在哪個block上,這裡一緻以search_info上的n_fields為準來計算fold,去查詢ahi;
在檢索ahi時需要加&btr_search_latch的s鎖;
如果本次無法命中ahi,就會将<code>btr_search_info::last_hash_succ</code>設定為false,這意味着随後的查詢都不會去使用ahi了,隻能等待下一路查詢資訊分析後才可能再次啟動(<code>btr_search_failure</code>);
對于從ahi中獲得的記錄指針,還需要根據目前的查詢模式檢查是否是正确的記錄位置(<code>btr_search_check_guess</code>)。
如果本次查詢使用了ahi,但查詢失敗了(<code>cursor->flag == btr_cur_hash_fail</code>),并且目前block建構ahi索引的curr_n_fields等字段和btr_search_info上的相符合,則根據目前cursor定位到的記錄插入ahi。參考函數:<code>btr_search_update_hash_ref</code>。
從上述分析可見,ahi如其名,完全是自适應的,如果檢索模式不固定,很容易就出現無法用上ahi或者ahi失效的情況。
關閉選項innodb_adaptive_hash_index;
持有<code>dict_sys->mutex</code>和<code>btr_search_latch</code>的x鎖;
周遊<code>dict_sys->table_lru</code>和<code>dict_sys->table_non_lru</code>連結清單,将每個表上的所有索引的<code>index->search_info->ref_count</code>設定為0;
釋放<code>dict_sys->mutex</code>;
周遊buffer pool,将block上的index标記(<code>buf_block_t::index</code>)清空為null;
清空ahi中的哈希項,并釋放為記錄項配置設定的heap;
釋放btr_search_latch。
參考函數:<code>btr_search_disable</code>
<code>index->search_info</code>的ref_count不為0時,無法從資料集詞典cache中将對應的表驅逐,workaround的方式是臨時關閉ahi開關;
參考函數:<code>dict_table_can_be_evicted</code>、<code>dict_index_remove_from_cache_low</code>
删除索引頁上的記錄,或者更新的是二級索引、或者更新了主鍵且影響了排序鍵值,則需要從ahi上将對應的索引記錄删除;
參考函數:<code>btr_search_update_hash_on_delete</code>
插入新的記錄時,如果本次插入未産生頁面重組、操作的page為葉子節點,且本次插入操作使用過ahi定位成功,則先嘗試更新再嘗試插入,否則直接插入對應的ahi記錄項;
參考函數:<code>btr_search_update_hash_node_on_insert</code>、<code>btr_search_update_hash_on_insert</code>
涉及索引樹分裂或者節點合并,或從lru中驅逐page(buf_lru_free_page)時,需要清空ahi對應的page。
參考函數:<code>btr_search_drop_page_hash_index</code>
在<code>row_search_mvcc</code>函數中,首先會去判斷在滿足一定條件時,使用shortcut模式,利用ahi索引來進行檢索。
隻有滿足嚴苛的條件時(例如需要唯一鍵查詢、使用聚集索引、長度不超過八分之一的page size、隔離級别在rc及rc之上、活躍的read view等等條件,具體的參閱代碼),才能使用shortcut:
加<code>btr_search_latch</code>的s鎖;
然後通過<code>row_sel_try_search_shortcut_for_mysql</code>檢索記錄;如果找到滿足條件的記錄,本次查詢可以不釋放 btr_search_latch,這意味着innodb/server層互動期間可能持有ahi鎖,但最多在10000次(btr_sea_timeout)互動後釋放ahi latch。一旦發現有别的線程在等待ahi x 鎖,也會主動釋放其擁有的s鎖。
我們可以通過<code>information_schema.innodb_metrics</code>來監控ahi子產品的運作狀态
首先打開監控:
重置所有的計數
該表搜集了ahi子系統諸如ahi查詢次數,更新次數等資訊,可以很好的監控其運作狀态,在某些負載下,ahi并不适合打開,關閉ahi可以避免額外的維護開銷。當然這取決于你針對具體負載的性能測試。