愛鋒貝

標(biāo)題: MySQL8.0 存儲引擎(InnoDB )buffer pool的實現(xiàn)原理 [打印本頁]

作者: 小強實驗室    時間: 2023-4-4 12:51
標(biāo)題: MySQL8.0 存儲引擎(InnoDB )buffer pool的實現(xiàn)原理
數(shù)據(jù)庫為了高效讀取和存儲物理數(shù)據(jù),通常都會采用緩存的方式來彌補磁盤IO與CPU運算速度差。InnoDB 作為一個具有高可靠性和高性能的通用存儲引擎也不例外,Buffer Pool就是其用來在內(nèi)存中緩存數(shù)據(jù)頁面的結(jié)構(gòu)。本文將基于MySQL-8.0.22源碼,從buffer pool結(jié)構(gòu)、buffer pool初始化、buffer pool管理、頁面讀取過程、頁面淘汰過程、buffer pool加速等方面介紹buffer pool的實現(xiàn)原理。
第一部分、Buffer pool結(jié)構(gòu)

Buffer pool不僅僅緩存了磁盤的數(shù)據(jù)頁,也存儲了鎖信息、change buffer信息、adaptive hash index、double write buffer等信息。本文將從物理與邏輯兩方面介紹buffer pool的結(jié)構(gòu)。
1.1  Buffer pool的物理結(jié)構(gòu)

Buffer pool的物理結(jié)構(gòu)自上而下分instance、chunk和page三層,如下圖所示:

(, 下載次數(shù): 73)

Buffer pool instance對應(yīng)的結(jié)構(gòu)體是buf_pool_t。整個buffer pool由多個instance組成,個數(shù)等于innodb_buffer_pool_size/innodb_buffer_pool_instances。instances是為并發(fā)讀取與寫入而設(shè)計,各instance之間沒有鎖競爭關(guān)系。當(dāng) innodb_buffer_pool_size小于1GB時為防止instances太小而出現(xiàn)性能問題,innodb_buffer_pool_instances會被重置為1。instance之內(nèi)包含完整鎖、信號量、chunks、為方便數(shù)據(jù)頁管理而設(shè)計的邏輯鏈表(lru list、free list、flush list)以及一個用于快速找到指定space_id和page_no的數(shù)據(jù)頁的的page hash鏈表。
Buffer pool chunk對應(yīng)的結(jié)構(gòu)體是buf_chunk_t。每個buffer pool instance被均勻劃分為多個chunk,buffer pool resize以chunk為粒度。chunk分為數(shù)據(jù)頁和數(shù)據(jù)頁對應(yīng)的控制體,控制體中有指針指向數(shù)據(jù)頁。遍歷所有instance的chunk可以幾乎訪問innodb所有緩存的數(shù)據(jù),只有部分諸如尚未解壓的壓縮頁等除外。
Buffer pool page對應(yīng)的結(jié)構(gòu)體包括塊描述符buf_block_t和頁面描述符buf_page_t。Buffer pool page大小為16KB,默認(rèn)與磁盤上數(shù)據(jù)頁的大小相同(innodb的文件結(jié)構(gòu)的介紹,請參考淺析InnoDB文件結(jié)構(gòu)),其緩存的不僅僅是數(shù)據(jù)頁,緩存的對象還包括:undo log頁面、change buffer(插入緩存信息)、AHI(自適應(yīng)哈希)、SDI(結(jié)構(gòu)化字典信息)、行鎖等。所有緩存對象在buffer pool中都是以頁面為單位存儲。
Innodb支持?jǐn)?shù)據(jù)頁壓縮,壓縮頁的大小在建表的時候指定,目前支持的范圍包括16K,8K,4K,2K,1K等(由于壓縮頁的管理方式與普通頁面不同,即使指定16K的壓縮頁,也能對數(shù)據(jù)量大的類型有一定益處)。壓縮頁面在buffer pool中使用伙伴系統(tǒng)管理,不論壓縮頁面在磁盤上的大小是多少,解壓后都為16K。當(dāng)buffer pool空閑頁面不足時,innodb會優(yōu)先淘汰壓縮頁面的解壓頁(buf_LRU_free_from_unzip_LRU_list),當(dāng)前者操作后仍不能為innodb提供足夠的空閑頁面時,會接著淘汰LRU list上的正常頁面和壓縮頁面(buf_LRU_free_from_common_LRU_list)。
1.1.1 Buffer pool page結(jié)構(gòu)

buf_block_t主要包含以下信息:
每個buffer pool page的buf_block_t和frame等信息于buf_chunk_init函數(shù)中被初始化。在使用allocate_chunk分配到chunk的空間后,chunk內(nèi)所有頁面的buf_block_t連在一起從內(nèi)存前向后初始化,chunk內(nèi)所有frame連在一起,從內(nèi)存后向前初始化,最左側(cè)的buf_block_t控制最左側(cè)的frame,如下圖所示:

(, 下載次數(shù): 81)

1.1.2 Buffer pool page分類

所有buffer pool page分為以下類別:
1.1.3 頁面讀取方法

頁面讀取方法包括:NORMAL、SCAN、IF_IN_POOL、PEEK_IF_IN_POOL、NO_LATCH、IF_IN_POOL_OR_WATCH、POSSIBLY_FREED。在詳細介紹頁面讀取方法之前,先介紹隨機預(yù)讀、make young、線性預(yù)讀等概念:
下面來了解各種頁面讀取方法的具體作用:
1.1.4 Page hash

innodb為加速buffer pool中頁面的查找,在每個buffer pool instance(buf_pool_t)中提供了page hash。page hash對應(yīng)的結(jié)構(gòu)體為hash_table_t。page hash中只存儲對應(yīng)到物理文件的頁面(buf_page_in_file() == TRUE),類型包括BUF_BLOCK_ZIP_PAGE、BUF_BLOCK_ZIP_DIRTY、BUF_BLOCK_FILE_PAGE三類。page hash的key為(m_space << 20) + m_space + m_page_no,value為頁面描述符buf_page_t*。page hash為了提高性能,提供了鎖分區(qū)的功能,即采用一系列鎖來保護page hash的單元(hash_cell_t),不同鎖保護不同分區(qū)。分區(qū)個數(shù)為默認(rèn)16個,只有在debug模式下才能通過innodb_page_hash_locks參數(shù)更改。
1.1.5 Zip hash

innodb的壓縮頁存儲空間由伙伴系統(tǒng)管理。為加速伙伴系統(tǒng)所有頁面的查找,其頁面都會存入zip hash中。
1.2 Buffer Pool的邏輯結(jié)構(gòu)

為方便管理buffer pool page,innodb用多個邏輯鏈表將相同屬性的buffer pool頁面描述符(通常都是buf_page_t)串聯(lián)在一起。
1.2.1 Free list

free list對應(yīng)暫時沒有被使用的節(jié)點,對應(yīng)結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) free,如前所述,其頁面的類型為BUF_BLOCK_NOT_USED。LRU list、CLOCK list等鏈表需要新頁面時就會向free list申請,當(dāng)后者空閑頁面不足時(buf_get_free_only返回為空),則需要通過掃描CLOCK list(buf_CLOCK_scan_and_free_block)或者LRU list(buf_LRU_scan_and_free_block),淘汰合適的節(jié)點以騰出空間頁面。
1.2.2 LRU list

LRU list是buffer pool最重要的鏈表,對應(yīng)的結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) LRU,除被independent標(biāo)記的頁面外,所有讀進來的頁面都放在LRU list上。LRU list頁面被用改良后的LRU算法管理,除LRU算法的基本特點之外,還包括以下特點:
如上方式管理LRU list的主要原因是擔(dān)心buffer pool中經(jīng)常被使用的頁面被預(yù)讀的頁面以及全表掃描類操作淘汰,影響數(shù)據(jù)庫的性能。
1.2.3 Unzip LRU list

unzip LRU list是用于存儲LRU list中壓縮頁的解壓頁的鏈表,對應(yīng)結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_block_t) unzip_LRU,存儲單元是塊描述符buf_block_t。讀取磁盤中的壓縮頁時,會調(diào)用buf_page_alloc_descriptor臨時分配一個頁面描述符buf_page_t,再調(diào)用buf_buddy_alloc從伙伴系統(tǒng)中分配一個空間存放壓縮頁原始數(shù)據(jù),臨時的buf_page_t會被加入LRU list和page hash(buf_page_init_for_read),如果是debug模式,還會將未解壓的壓縮頁放入zip clean list。
壓縮頁在被用戶請求的過程中,在壓縮頁面獲取之后,innodb會在zip_page_handler函數(shù)內(nèi),先通過buf_relocate從free list申請新的空閑頁面替換掉臨時的buf_page_t,放在LRU list相同的位置,并把解壓頁面的塊描述符buf_block_t放入unzip LRU list中(unzip LRU list中頁面的前后順序與LRU list相同),并且刪除page hash中的臨時buf_page_t,在其中加入新的buf_page_t;后通過buf_zip_decompress對壓縮數(shù)據(jù)進行解壓。由于解壓頁既以buf_page_t存在于LRU list,又以buf_block_t存在于unzip LRU list,buf_page_t和buf_block_t又能互相轉(zhuǎn)化,因此unzip LRU list實際上是LRU list的子集。
當(dāng)free list空間不足時,innodb會優(yōu)先淘汰unzip LRU list,并且只淘汰解壓頁而不淘汰壓縮頁。如果從unzip LRU list中沒能淘汰出頁面,則會嘗試從LRU list中淘汰,此時如果遇到解壓頁,則會連同壓縮頁本身一起淘汰(buf_LRU_scan_and_free_block)。當(dāng)解壓頁被淘汰,而壓縮頁未被淘汰時,innodb會重新為壓縮頁分配臨時頁面描述符buf_page_t,將其插入LRU list中與解壓頁相同的位置,并且從page hash中刪除解壓頁,將臨時buf_page_t加入page hash(buf_LRU_free_page)。
1.2.4 CLOCK list

雖然innodb小心翼翼地設(shè)計了midpoint的buffer pool page讀取方案,8.0還引入了SCAN讀取模式盡可能減少全表掃描與正常query的數(shù)據(jù)頁面間的沖突,但并未根除。假設(shè)buffer pool有限,正常query需要的頁面可以恰好完整緩存到buffer pool或者還無法緩存到buffer pool,若此時發(fā)起類似全表掃描的操作,全表掃描需要的頁面不論需要多少,都會導(dǎo)致以下問題:
上述問題的根本原因在于全表掃描頁面與正常query頁面存在耦合,能夠互相影響。CLOCK list是CDB為接觸全表掃描頁面與正常query頁面耦合而引入的新鏈表結(jié)構(gòu),對應(yīng)結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) CLOCK。所有用independent hint標(biāo)記過的頁面都會存入CLOCK list,而非LRU list。CLOCK list采用類似優(yōu)化過的CLOCK算法進行管理:
使用CLOCK list的方法很簡單,只需要在INSERT、DELETE、UPDATE等DML關(guān)鍵詞后添加名為independent的hint即可,例如select /*+ independent */ id from t。CLOCK list的長度、頁面被使用次數(shù)分布等信息可以使用show engine innodb status命令觀察。如下圖所示:

(, 下載次數(shù): 68)

1.2.5 Unzip CLOCK list

unzip CLOCK list與unzip LRU list類似,都是用于存儲解壓的壓縮頁,對應(yīng)的結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_block_t) unzip_CLOCK。CLOCK list對壓縮頁的讀取,解壓過程都與LRU list相似,都是先用臨時buf_page_t讀取原始壓縮數(shù)據(jù),將臨時buf_page_t加入CLOCK list和page hash,如果是debug模式,還會將未解壓的壓縮頁放入zip clean list。之后在zip_page_handler函數(shù)內(nèi),再用從free list申請的新頁面替換臨時buf_page_t,存放到CLOCK list和unzip CLOCK list對應(yīng)的位置中,最后用buf_zip_decompress進行數(shù)據(jù)解壓。與unzip LRU list相似,unzip CLOCK list也是CLOCK list的子集。
unzip CLOCK list的淘汰分兩種,一種與unzip LRU list相似:當(dāng)independent hint訪問需要的頁面不足時,先掃描unzip CLOCK list,后掃CLOCK list,直至找到use_times為0的頁面并淘汰復(fù)用(buf_CLOCK_scan_and_free_block)。淘汰頁面時如果只有解壓頁被淘汰而壓縮頁未被淘汰,處理細節(jié)與unzip LRU list相同。另一種是后臺線程buf_independent_buffer_pool_evict_thread主動掃描unzip CLOCK list和CLOCK list,將每個頁面的use_times減1,遇到解壓頁會連同壓縮頁一起釋放,需要的話還會進行刷臟,從而避免全表掃描頁面長時間駐留在unzip CLOCK list和CLOCK list中。
1.2.6 Flush list

flush list用來存儲buffer pool中所有修改過的頁面,對應(yīng)結(jié)構(gòu)是buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) flush_list。如前所述,flush list中只有BUF_BLOCK_FILE_PAGE和BUF_BLOCK_ZIP_DIRTY這兩種頁面。flush list是LRU list + CLOCK list的子集。flush list中的頁面按照最老修改的LSN(即第一次修改頁面的LSN)排序,鏈表尾是LSN最小的頁面,優(yōu)先被刷入磁盤。多次修改不影響頁面在flush list中的位置,如果刷盤的時候,頁面存在多個LSN,那么這些修改將合并刷入磁盤。
1.2.7 Zip clean list

zip clean list僅存在于debug模式下,用于調(diào)試buffer pool的頁壓縮功能,對應(yīng)結(jié)構(gòu)是buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) zip_clean。zip clean list用于存儲還沒解壓的,類型為BUF_BLOCK_ZIP_PAGE的干凈壓縮頁。從源碼中觀察,zip clean list中的頁面來源以下三類:
壓縮頁完成解壓后,就會從zip clean鏈表中刪除,然后根據(jù)請求是否帶有independent hint來判斷,解壓頁是加入unzip LRU list還是unzip CLOCK list。
1.2.8 Zip free list

zip free list是用來管理壓縮頁伙伴系統(tǒng)的鏈表,對應(yīng)結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX]。buf_buddy_free_t由三部分組成分別記錄了狀態(tài)、頁面描述符和下一節(jié)點指針,鏈表的第一個buf_buddy_free_t會記錄鏈表中節(jié)點頁面大小。從zip free list對應(yīng)結(jié)構(gòu)可以看出,其由多個對應(yīng)page size不同大?。?6K、8K、4K、2K、1K)的鏈表組成。構(gòu)成zip free list的所有page的類似為BUF_BLOCK_MEMORY,所有頁面存入zip hash中。
壓縮頁讀取時,需要從zip free list中申請空間到block→page.zip.data,用于存儲壓縮頁原始數(shù)據(jù)(buf_buddy_alloc),下面舉例說明其申請過程,假設(shè)申請2K的空間:
伙伴系統(tǒng)調(diào)用buf_buddy_alloc釋放空間。對于size為16K的空間回收,則直接清空后掛回到16K鏈表即可。對于size低于16K的空間回收,會先遞歸尋找其伙伴空間,如果伙伴空間也處于空閑狀態(tài),則合并至更大的size;如果合并失敗,則將釋放的空間掛至zip free對應(yīng)鏈表中。通過這樣的方式,盡可能減少伙伴系統(tǒng)中的內(nèi)存碎片。
1.2.9 Withdraw list

withdraw list僅僅用于buffer pool縮小過程(bp resize),對應(yīng)的結(jié)構(gòu)為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) withdraw。下面介紹buffer pool resize的過程:


1.2.10 Hazard point

hazard point是用來避免逆向掃描鏈表過程的迭代器。hazard point與普通迭代器不同,當(dāng)掃描過程的下一節(jié)點被其他操作移除或替換了時,hazard point能調(diào)整位置并指向被移除或替換的節(jié)點的下一有效節(jié)點,避免逆向掃描過程失敗。使用hazard point逆向掃描鏈表的時間復(fù)雜度為O(N),可以避免逆向掃描失敗時從頭開始掃描而導(dǎo)致的最高 O(N*N)的時間復(fù)雜度。因此hazard point廣泛應(yīng)用于flush list、LRU list、CLOCK list等鏈表的批量操作中的鏈表掃描中。
hazard point的實現(xiàn)也不復(fù)雜,對應(yīng)的基類為HazardPointer,包括get、set、is_hp、adjust、move等方法。使用于不同的鏈表時,基于基類派生出不同的子類,并添加定制化的方法。當(dāng)逆向掃描鏈表時,每次循環(huán)通過get函數(shù)獲取下一個節(jié)點,并將節(jié)點的下一節(jié)點設(shè)置為hazard point。并發(fā)的負載如果修改了鏈表,并且節(jié)點是hazard point(is_hp返回為true),則使用adjust函數(shù)將被修改的節(jié)點的下一節(jié)點設(shè)置為hazard point。
1.2.11 總結(jié)

上述介紹的鏈表都是用于管理需要持久化的頁面的緩存。從內(nèi)存來源來看,除未解壓的壓縮頁的buf_page_t存儲于臨時分配空間,其他所有頁面都存儲于buf_pool→chunks中。從頁面分布來看,通常而言,LRU list和CLOCK list描述了所有在buffer pool中的頁面,unzip LRU list、unzip CLOCK list、flush list都是LRU list + CLOCK list的子集。從用途來看,LRU/unzip LRU list、CLOCK/unzip CLOCK list、free list、zip free list、flush list屬于常規(guī)鏈表,服務(wù)于數(shù)據(jù)庫正常服務(wù),zip clean list和withdraw list屬于特殊用途的list,zip clean list用于debug模式下壓縮頁面的調(diào)試,withdraw list僅僅服務(wù)于buffer pool resize。
第二部分、Buffer pool快速讀寫優(yōu)化

2.1 Buffer pool初始化

Buffer pool初始化入口是buf_pool_init。在buf_pool_init中先通過ut_zalloc_nokey分配管理所有buffer pool instance的結(jié)構(gòu)體(buf_pool_t)的buf_pool_ptr,接著為每個instance創(chuàng)建buf_pool_create線程來初始化instance中的鏈表、chunks、mutex、page_hash、zip_hash、hazard_point、用于purge異步讀取頁面的watch結(jié)構(gòu)體等。chunks初始化結(jié)束后,逐一對每個block調(diào)用buf_block_init,對block的io、是否在邏輯鏈表、是否在page hash、zip hash等信息進行初始化,并添加到buf_pool->free鏈表中。
Buffer pool初始化過程中,內(nèi)存申請比較常見的是使用減少了用戶空間與內(nèi)核空間地址拷貝(零拷貝)的mmap。在chunks初始化時,也會調(diào)用os_mem_alloc_large分配內(nèi)存,嘗試在Linux支持的情況下采用HUGETLB的方式分配內(nèi)存。
2.2 單機buffer pool預(yù)熱

MySQL官方為了讓實例能夠在重啟后快速恢復(fù)到重啟前的狀態(tài),在MySQL 5.6版本開始支持了buffer pool單機預(yù)熱的功能。單機預(yù)熱功能分為dump和load兩個步驟。
dump將實例重啟前的所有buffer pool instance的LRU list中每個頁面的space_id和page_no記錄到本地innodb_buffer_pool_filename文件中。(space_id是數(shù)據(jù)文件的編號,page_no數(shù)據(jù)文件內(nèi)的偏移。space_id與page_no可以唯一確定一個頁面,詳見淺析InnoDB文件結(jié)構(gòu))。為了盡可能避免掃描過程對buffer pool正常工作的影響,dump先將LRU list掃描過程中的頁面信息記錄在臨時的數(shù)組中,掃描LRU list后立刻釋放LRU_list_mutex鎖,再在無鎖的情況下將LRU list的信息寫入innodb_buffer_pool_filename文件中。
load在實例重啟后將本地innodb_buffer_pool_filename文件中記錄的所有頁面加載到buffer pool中,從而完成數(shù)據(jù)預(yù)熱,將實例快速恢復(fù)成重啟前的狀態(tài)。load操作會遍歷innodb_buffer_pool_filename文件兩次(由于每個頁面的信息只包括space_id和page_no,一共只占8個字節(jié),正常情況下文件只有幾百K到幾G的級別,兩次掃描帶來的IO量尚可接受)。第一次遍歷整個文件以確定其中總共包含的page記錄數(shù),如果page記錄數(shù)超過當(dāng)前buffer pool size,調(diào)整待讀入的page總數(shù)。第二遍掃描文件時,先按照第一遍得到的page數(shù)量,創(chuàng)建好page數(shù)組,再依次讀入innodb_buffer_pool_filename文件中所有的page記錄。為提高IO效率,對page數(shù)組進行按照space_id和page_no排序。最后才使用buf_read_page_background函數(shù)在后臺將所有頁面加載到LRU list中的midpoint中。
官方數(shù)據(jù)預(yù)熱的特點如下:
2.3 主從buffer pool同步

MySQL官方提供的dump和load操作只能用于單機預(yù)熱的根本原因是:MySQL的主從復(fù)制是邏輯復(fù)制,非物理復(fù)制。數(shù)據(jù)雖然是一致的,但是主從數(shù)據(jù)的分布是不一樣的。InnoDB的所有數(shù)據(jù)都存儲于B+樹中,也可簡單理解為主從所對應(yīng)數(shù)據(jù)的B+樹的結(jié)構(gòu)是不一樣的,在從庫按部就班加載B+樹相同位置的頁面無法解決主從切換、跨機遷移等多場景的預(yù)熱問題。
主從緩存同步是CDB針對主從多場景下快速buffer pool預(yù)熱的解決方案,其從邏輯復(fù)制的思路出發(fā),通過主從邏輯數(shù)據(jù)一致的角度解決了多場景的預(yù)熱問題。主從緩存同步分為snapshot、transmit、recover三個步驟:
snapshot將主庫buffer pool狀態(tài)邏輯導(dǎo)出到主庫本地的ib_bp_info文件中,具體過程如下(buf_snapshot):
針對如何傳輸ib_bp_info文件,有很多備選方案:采用scp來傳輸文件誤操作的可能性比較高,并且經(jīng)常修改傳輸腳本;采用binlog傳輸會加重binlog的負擔(dān),加大主從延遲的風(fēng)險,方案復(fù)雜,風(fēng)險大;采用內(nèi)建表的方式同步文件又擔(dān)心引入兼容性的問題。主從緩存通過最終選擇了transmit方案,即在slave新創(chuàng)建一種與IO線程相似,需要與master建立tcp連接的transmit線程(handle_slave_transmit),該線程負責(zé)向master發(fā)送ib_bp_info文件傳輸申請,并負責(zé)接收ib_bp_info文件。transmit方案不侵入主從復(fù)制的原有邏輯,對于不支持transmit的主庫,也僅僅是無法從主庫獲取ib_bp_info文件,不存在兼容問題。
recover將ib_bp_info文件中代表的數(shù)據(jù)加載到從庫buffer pool中。recover可以在跨機遷移前、版本升級前、主從切換前或切換過程中進行,也可以定期將主庫的buffer pool同步到從庫,隨時做好切換準(zhǔn)備。recover具體過程如下(buf_recover):
2.4 Change buffer優(yōu)化

2.4.1 原理

innodb的二級索引與cluster index相同,也存儲在B+樹中。cluster index與二級索引的B+樹節(jié)點散落在數(shù)據(jù)文件中。在此架構(gòu)下,一條普通的DML都可能因為需要修改多個二級索引,而帶來大量隨機IO,對innodb的性能帶來巨大挑戰(zhàn)。
change buffer就是為了解決二級索引隨機IO問題而引入的緩存。當(dāng)待插入、刪除、修改的記錄所在的二級索引頁面不在buffer pool中時,修改內(nèi)容會以記錄的形式緩存到change buffer中。當(dāng)二級索引頁面最終被讀入buffer pool中時,需要檢查change buffer中是否有該頁面的修改記錄,如果存在需要將修改記錄合并到新讀入的二級索引頁面上,再返回。change buffer適用于buffer pool大小有限,無法將所有數(shù)據(jù)緩存到buffer pool,并且有大量二級索引需要更新的場景。但如果buffer pool足夠大到能緩存所有索引頁面,或者隨機IO的速度和順序IO的速度所差無幾,也可以考慮關(guān)閉change buffer。
2.4.2 實現(xiàn)

innodb在Mysql-5.5版本之前的版本中只支持了二級索引的insert buffer,后續(xù)才支持了update和delete,名稱改為了change buffer。而在代碼層面上,很多以ibuf為前綴的參數(shù)、函數(shù)、文件沿襲了下來,沒有做修改。
change buffer本身也是一顆B+樹,它位于系統(tǒng)表空間(space_id為0),根結(jié)點的page_no固定為4(FSP_IBUF_TREE_ROOT_PAGE_NO),所有innodb用戶表的二級索引變更都緩存在同一顆B+樹上。一條change buffer record中的信息包括:

change buffer的B+樹以(space_id、page_no、counter)為主鍵,來確保記錄的唯一性以及apply時的順序。change buffer在正常運行過程中必須保證緩存合并到二級索引后,二級索引不能發(fā)生分裂或合并操作,否則緩存到主鍵將對應(yīng)到未知的頁面而失效。為此需要實時獲取目標(biāo)二級索引頁面的剩余空間,innodb的方案是在數(shù)據(jù)文件中(ibdata或ibd)。page_size是默認(rèn)大小16KB的情況下,page_no為1、1+16384*N的頁面都是FIL_PAGE_IBUF_BITMAP類型,用于記錄其后16384個頁面change buffer的信息。FIL_PAGE_IBUF_BITMAP頁面用4個bit來描述一個頁面:
DML運行過程中會實時將二級索引信息變化更新到FIL_PAGE_IBUF_BITMAP頁面中。在change buffer插入過程中,如果發(fā)現(xiàn)準(zhǔn)備插入的change buffer record可能會導(dǎo)致二級索引頁面分裂,則插入失敗并調(diào)用ibuf_get_merge_page_nos觸發(fā)一次從當(dāng)前cursor位置附近開始的異步的change buffer 合并操作,目的是盡量將當(dāng)前頁面的緩存操作做一次合并(ibuf_insert_low)。此外為避免出現(xiàn)空頁面,需要在purge線程真正刪除記錄時使用ibuf_get_volume_buffered預(yù)估合并完頁面所有change buffer record之后的記錄數(shù),如果二級索引頁面上記錄等于1(加上purge線程這次緩存的刪除操作將變?yōu)榭枕撁妫瑒t索引插入失敗,改走正常讀入物理頁面邏輯。
二級索引記錄沒有trx_id這一系統(tǒng)列,這為緩存purge線程刪除記錄操作帶來了困難,難以區(qū)分真正要刪除掉的記錄。為避免purge線程刪除操作誤刪用戶reinsert的二級索引記錄,如果purge操作先進入ibuf_insert,則用戶后續(xù)的insert操作將放棄緩存插入,轉(zhuǎn)而讀取物理頁面;如果insert操作先進入ibuf_insert,則purge操作也放棄緩存刪除。
change buffer頁面的與普通索引頁面相同,其修改也是先記錄redo日志后刷盤,在crash recover時,同樣需要redo來保證一致性。change buffer可以理解為記錄在index page上的二級索引更改日志。
2.4.3 緩存條件

只有滿足一定條件時,二級索引變更才會被change buffer緩存,判斷條件如下(ibuf_should_try):
2.4.4 合并時機

有以下幾種場景會觸發(fā)change buffer合并操作:

2.5 Adaptive hash index優(yōu)化

2.5.1 原理

innodb的數(shù)據(jù)存儲于B+樹中,B+樹通常不會太高,通常只有3到5層。從根節(jié)點到葉節(jié)點的尋路涉及到多層頁面內(nèi)記錄的比較,即使所有路徑上的頁面都在內(nèi)存中,也是比較消耗CPU的操作。
對尋路到CPU開銷優(yōu)化分兩部分,第一部分為盡可能避免尋路次數(shù),innodb為此設(shè)計了多個優(yōu)化,例如一次尋路多緩存幾條記錄到fetch_cache中;尋路結(jié)束將cursor緩存到row_prebuilt_t::pcur,方便下次查詢復(fù)用。第二部分為盡可能避免單詞尋路的開銷。Adaptive hash index(AHI)便是為此而設(shè)計。AHI可以理解為建立在B+樹上的索引,它采用自適應(yīng)的方式管理,為B+樹尋路建立以查詢條件為key,B+樹record地址為value的hash index。
AHI的大小為buffer pool size的1/64,在buf_pool_init中調(diào)用btr_search_sys_create初始化。為了避免AHI的鎖競爭壓力,innodb支持AHI分區(qū),可以使用innodb_adaptive_hash_index_parts參數(shù)配置分區(qū)個數(shù),默認(rèn)為8。當(dāng)buffer pool size動態(tài)調(diào)整大小為原大小的2倍以上或者1/2以下時,AHI也隨之調(diào)用btr_search_sys_resize調(diào)整大?。╞uf_pool_resize)。
2.5.2 創(chuàng)建過程

當(dāng)innodb_adaptive_hash_index參數(shù)打開,非臨時表、非空間類型的索引會累加索引被使用的次數(shù),當(dāng)使用次數(shù)小于17(BTR_SEARCH_HASH_ANALYSIS),當(dāng)前索引被認(rèn)為不夠熱,被自動忽略(btr_search_info_update)。索引次數(shù)達標(biāo)后,進入下一步驟。
AHI的key與用戶的查詢條件相關(guān),每個索引(dict_index_t)都包含用于存儲查詢條件的search_info(btr_search_t)。search_info主要用三項信息描述查詢條件:完整匹配的匹配列數(shù)n_fields,不完整匹配的列的前綴字節(jié)數(shù)n_bytes,是否為左側(cè)匹配left_side。如果查詢條件與上一次緩存的相同,則將search_info->n_hash_potential加1。否則清空search_info,重新設(shè)定index的查詢條件(btr_search_info_update_hash)。
即使索引通過了篩選,查詢條件通過了考核,AHI也不會對索引的每個頁面都建立hash index。對于需要創(chuàng)建AHI的頁面還需要經(jīng)過一輪篩選:某頁如果能通過當(dāng)前search_info命中,則對頁面的n_hash_helps加1(當(dāng)前search_info首次命中到頁面,還會將search_info信息記錄在頁面buf_block_t中)。如果頁面的n_hash_helps大于記錄數(shù)/16(BTR_SEARCH_PAGE_BUILD_LIMIT)并且search_info->n_hash_potential大于100(BTR_SEARCH_BUILD_LIMIT),則對該頁面創(chuàng)建AHI。對于已經(jīng)創(chuàng)建過AHI的頁面,只有頁面的n_hash_helps大于記錄數(shù)的兩倍,或者search_info發(fā)生改變了才會重新創(chuàng)建AHI(btr_search_update_block_hash_info)。
創(chuàng)建AHI之前,會先檢查AHI鎖的狀態(tài),如果其他線程正在持有AHI的X鎖,則先跳過本次AHI的創(chuàng)建。原因是當(dāng)前處于B+樹搜索的關(guān)鍵路徑,在此處等鎖會拖累其他B+樹的搜索效率,而本次search_info→n_hash_potential和頁面的n_hash_helps狀態(tài)并未清空,下一次該頁面被讀取時會再次觸發(fā)AHI創(chuàng)建流程。AHI的創(chuàng)建過程分為5步(btr_search_build_page_hash_index):


2.5.3 使用條件

AHI的使用在B+樹搜索的關(guān)鍵路徑上,使用AHI的幾個條件如下(btr_cur_search_to_nth_level):
滿足上述條件后,在正式搜索AHI之前,會再次對比索引的最新search_info是否已經(jīng)被重置了,即search_info→n_hash_potential是否等于0,以及當(dāng)前查詢條件的列數(shù)相比search_info是否完整。前者等于0意味著當(dāng)前舊AHI記錄已經(jīng)被刪除;后者查詢條件不足意味著當(dāng)前查詢條件不足以構(gòu)建AHI的key值進行查詢,兩種情況都只能放棄AHI查詢,轉(zhuǎn)而使用正常的B+樹搜索(btr_search_guess_on_hash)。接下來,將查詢條件轉(zhuǎn)化為AHI的key值,加AHI的S鎖從AHI中讀取數(shù)據(jù)(ha_search_and_get_data)。如果查詢失敗了,并且頁面上的search_info信息依舊與索引上的search_info相同,則會將新的記錄插入AHI中(btr_search_update_hash_ref)。
從上述AHI創(chuàng)建過程中可以看到,只有較為查詢模式較為固定的業(yè)務(wù)才能經(jīng)過層層篩選,創(chuàng)建出AHI,從該功能中受益。
2.5.4 自適應(yīng)維護

第三部分、頁面讀取過程解讀

3.1 頁面讀取堆棧

buf_page_get_gen  Buf_fetch<T>::single_page    Buf_fetch_normal::get      lookup      read_page        buf_read_page_low          buf_page_init_for_read            buf_LRU_get_free_block              buf_LRU_get_free_only              buf_LRU_scan_and_free_block                buf_LRU_free_from_unzip_LRU_list                  buf_LRU_free_page                    buf_LRU_block_remove_hashed                buf_LRU_free_from_common_LRU_list              buf_flush_single_page_from_LRU            buf_LRU_add_block              buf_LRU_add_block_low          fil_io          buf_page_io_complete
復(fù)制
3.2 核心函數(shù)概述
buf_page_get_gen

Buf_fetch<T>::single_page

Buf_fetch_normal::get

Buf_fetch_other::get

Buf_fetch<T>::lookup

Buf_fetch<T>::read_page

buf_read_page_low

此函數(shù)的第四參數(shù)mode并非Page_fetch,而是BUF_READ_IBUF_PAGES_ONLY和BUF_READ_ANY_PAGE二選一,前者是指讀取ibuf頁面。
buf_page_init_for_read

buf_LRU_get_free_block

?buf_LRU_get_free_only

buf_LRU_scan_and_free_block

buf_LRU_free_from_unzip_LRU_list

buf_LRU_free_from_common_LRU_list

從buf_pool->lru_scan_itr.start()開始掃描LRU_list,如果掃描個數(shù)沒達到BUF_LRU_SEARCH_SCAN_THRESHOLD或者是全表掃描則一直向下掃描。使用buf_flush_ready_for_replace判斷頁面是否能立即替換:先使用buf_page_in_file判斷頁面是否為文件頁面類型之一:BUF_BLOCK_FILE_PAGE、BUF_BLOCK_ZIP_DIRTY、BUF_BLOCK_ZIP_PAGE;再調(diào)用buf_LRU_free_page判斷頁面是否能淘汰成功。此輪淘汰將淘汰壓縮頁原數(shù)據(jù)。
buf_LRU_free_page

嘗試從LRU list中釋放一個頁面,中間有臨時釋放鎖再加鎖的邏輯,實現(xiàn)比較復(fù)雜。
buf_LRU_free_page有一個參數(shù)配置是否釋放解壓頁的原壓縮數(shù)據(jù)。buf_LRU_free_from_unzip_LRU_list中這個參數(shù)是false,即默認(rèn)不刪除壓縮頁的原數(shù)據(jù)。buf_LRU_free_from_common_LRU_list中這個參數(shù)是true,即將壓縮頁原數(shù)據(jù)刪除。
buf_LRU_add_block_low

buf_flush_single_page_from_LRU

用來淘汰一個頁面,將其刷盤。并將頁面從page_hash和LRU_list中刪除,放入free_list。
buf_relocate

將壓縮頁的bpage從LRU中刪除,重新分配block,把bpage中的內(nèi)容拷貝到block->page中,把dpage (block->page)插入bpage的位置。
注意:
       目前騰訊云數(shù)據(jù)庫 MySQL 8.0特惠活動,不限新老用戶,最低5折起

-----------------------------




歡迎光臨 愛鋒貝 (http://m.7gfy2te7.cn/) Powered by Discuz! X3.4