數(shù)據(jù)庫為了高效讀取和存儲物理數(shù)據(jù)卤恳,通常都會采用緩存的方式來彌補磁盤IO與CPU運算速度差。InnoDB 作為一個具有高可靠性和高性能的通用存儲引擎也不例外吕朵,Buffer Pool就是其用來在內存中緩存數(shù)據(jù)頁面的結構衅澈。本文將基于MySQL-8.0.22源碼键菱,從buffer pool結構、buffer pool初始化今布、buffer pool管理经备、頁面讀取過程、頁面淘汰過程部默、buffer pool加速等方面介紹buffer pool的實現(xiàn)原理侵蒙。
第一部分、Buffer pool結構
Buffer pool不僅僅緩存了磁盤的數(shù)據(jù)頁傅蹂,也存儲了鎖信息纷闺、change buffer信息、adaptive hash index份蝴、double write buffer等信息犁功。本文將從物理與邏輯兩方面介紹buffer pool的結構。
1.1? Buffer pool的物理結構
Buffer pool的物理結構自上而下分instance婚夫、chunk和page三層浸卦,如下圖所示:
Buffer pool instance
Buffer pool instance對應的結構體是buf_pool_t。整個buffer pool由多個instance組成案糙,個數(shù)等于innodb_buffer_pool_size/innodb_buffer_pool_instances限嫌。instances是為并發(fā)讀取與寫入而設計靴庆,各instance之間沒有鎖競爭關系。當 innodb_buffer_pool_size小于1GB時為防止instances太小而出現(xiàn)性能問題怒医,innodb_buffer_pool_instances會被重置為1炉抒。instance之內包含完整鎖、信號量稚叹、chunks端礼、為方便數(shù)據(jù)頁管理而設計的邏輯鏈表(lru list、free list入录、flush list)以及一個用于快速找到指定space_id和page_no的數(shù)據(jù)頁的的page hash鏈表蛤奥。
Buffer pool chunk
Buffer pool chunk對應的結構體是buf_chunk_t。每個buffer pool instance被均勻劃分為多個chunk僚稿,buffer pool resize以chunk為粒度凡桥。chunk分為數(shù)據(jù)頁和數(shù)據(jù)頁對應的控制體,控制體中有指針指向數(shù)據(jù)頁蚀同。遍歷所有instance的chunk可以幾乎訪問innodb所有緩存的數(shù)據(jù)缅刽,只有部分諸如尚未解壓的壓縮頁等除外。
Buffer pool page
Buffer pool page對應的結構體包括塊描述符buf_block_t和頁面描述符buf_page_t蠢络。Buffer pool page大小為16KB衰猛,默認與磁盤上數(shù)據(jù)頁的大小相同(innodb的文件結構的介紹,請參考淺析InnoDB文件結構)刹孔,其緩存的不僅僅是數(shù)據(jù)頁啡省,緩存的對象還包括:undo log頁面、change buffer(插入緩存信息)髓霞、AHI(自適應哈希)卦睹、SDI(結構化字典信息)、行鎖等方库。所有緩存對象在buffer pool中都是以頁面為單位存儲结序。
Innodb支持數(shù)據(jù)頁壓縮,壓縮頁的大小在建表的時候指定纵潦,目前支持的范圍包括16K徐鹤,8K,4K邀层,2K返敬,1K等(由于壓縮頁的管理方式與普通頁面不同,即使指定16K的壓縮頁被济,也能對數(shù)據(jù)量大的類型有一定益處)救赐。壓縮頁面在buffer pool中使用伙伴系統(tǒng)管理涧团,不論壓縮頁面在磁盤上的大小是多少只磷,解壓后都為16K经磅。當buffer pool空閑頁面不足時,innodb會優(yōu)先淘汰壓縮頁面的解壓頁(buf_LRU_free_from_unzip_LRU_list)钮追,當前者操作后仍不能為innodb提供足夠的空閑頁面時预厌,會接著淘汰LRU list上的正常頁面和壓縮頁面(buf_LRU_free_from_common_LRU_list)。
1.1.1 Buffer pool page結構
buf_block_t主要包含以下信息:
描述頁所屬space ID元媚、文件內偏移的page no轧叽、頁面大小、IO狀態(tài)刊棕、最新修改的LSN炭晒、最老修改的LSN、zip壓縮頁面原始數(shù)據(jù)甥角、用于鏈接到page hash節(jié)點等信息的結構buf_page_t网严。buf_page_t處于buf_block_t的第一項,方便二者之間靈活轉化嗤无。
指向存儲頁面數(shù)據(jù)的內存地址frame震束。
頁面mutex,用于保護buf_fix_count当犯、io_fix等頁面狀態(tài)等垢村。
是否處于unzip LRU list、withdraw list嚎卫、unzip CLOCK_list等信息嘉栓,這些鏈表的含義將在1.2節(jié)中介紹。
其他信息拓诸。
每個buffer pool page的buf_block_t和frame等信息于buf_chunk_init函數(shù)中被初始化胸懈。在使用allocate_chunk分配到chunk的空間后,chunk內所有頁面的buf_block_t連在一起從內存前向后初始化恰响,chunk內所有frame連在一起趣钱,從內存后向前初始化,最左側的buf_block_t控制最左側的frame胚宦,如下圖所示:
1.1.2 Buffer pool page分類
所有buffer pool page分為以下類別:
BUF_BLOCK_POOL_WATCH:用于purge操作異步讀取磁盤頁面的一種類型首有。每個buf_pool_t結構體中都有一個名為watch的數(shù)組,元素類型為buf_page_t枢劝,大小為purge線程數(shù)+1井联。當purge操作需要讀取一個不在buffer pool中的頁面時,會將watch數(shù)組中一個BUF_BLOCK_POOL_WATCH狀態(tài)的頁面設置為BUF_BLOCK_ZIP_PAGE您旁,設置對應space id烙常,page id,設置buf_fix_count設置為1防止其被淘汰出buffer pool,并將其加入page hash中(buf_pool_watch_set)蚕脏。當磁盤數(shù)據(jù)被讀取進入buffer pool時侦副,會將watch數(shù)組對應的頁面狀態(tài)恢復為BUF_BLOCK_POOL_WATCH,將watch頁面從page hash中刪除(buf_pool_watch_remove)驼鞭,后續(xù)會將新頁面加入page hash中秦驯。通過較為tricky地判斷存在于page hash的page地址是否在watch數(shù)組范圍內,可以巧妙地判斷目標頁面是否成功讀入buffer pool挣棕。
BUF_BLOCK_ZIP_PAGE:壓縮頁未解壓的對應狀態(tài)译隘。從磁盤讀取壓縮頁時,用buf_page_alloc_descriptor分配一個臨時頁面描述符buf_page_t洛心,再調用buf_buddy_alloc從伙伴系統(tǒng)中分配一個空間存放壓縮頁原始數(shù)據(jù)固耘,臨時的bpage會被加入LRU list和page hash(buf_page_init_for_read)。這個臨時buf_page_t等到頁面被解壓時词身,innodb會使用從free_list中申請到的狀態(tài)為BUF_BLOCK_FILE_PAGE的buf_page_t替換掉臨時buf_page_t玻驻,放在LRU list相同的位置,并把解壓頁面的塊描述符buf_block_t放入unzip LRU list中偿枕;刪除page hash中的臨時buf_page_t璧瞬,在其中加入新的buf_page_t;最后通過buf_zip_decompress解壓頁面(zip_page_handler)渐夸。如果解壓頁被淘汰嗤锉,而壓縮頁本身未被淘汰,并且頁面未被修改墓塌,則此頁面會再次被標記為BUF_BLOCK_ZIP_PAGE瘟忱。關于BUF_BLOCK_ZIP_PAGE的另一個用法如前所述,為在watch操作中被用來標記尚未讀入的頁面苫幢,不再復述访诱。
BUF_BLOCK_ZIP_DIRTY:壓縮頁的解壓頁被釋放時,如果頁面被修改過(oldest_modification非0)韩肝,則頁面從BUF_BLOCK_FILE_PAGE狀態(tài)變?yōu)锽UF_BLOCK_ZIP_DIRTY狀態(tài)触菜,未被修改過則為BUF_BLOCK_ZIP_PAGE(buf_LRU_free_page/buf_CLOCK_free_page)。解壓頁被釋放后哀峻,BUF_BLOCK_ZIP_PAGE/BUF_BLOCK_ZIP_DIRTY壓縮頁的頁面描述符也會在buf_LRU_free_page/buf_CLOCK_free_page臨時分配涡相。BUF_BLOCK_ZIP_DIRTY狀態(tài)的頁面無法從LRU/CLOCK list中淘汰,只能在flush_list中等待刷盤剩蟀。在flush_list中催蝗,也只存在BUF_BLOCK_FILE_PAGE和BUF_BLOCK_ZIP_DIRTY這兩種頁面。
BUF_BLOCK_NOT_USED:頁面在free_list中時的狀態(tài)育特,此類頁面較為常見丙号。
BUF_BLOCK_READY_FOR_USE:當頁面從free list取下,準備放入LRU/CLOCK list(buf_LRU_get_free_block/buf_CLOCK_get_free_block)時處于的臨時狀態(tài)。
BUF_BLOCK_FILE_PAGE:非壓縮頁面的頁面狀態(tài)犬缨,壓縮頁解壓頁的頁面狀態(tài)喳魏。最常見的頁面狀態(tài)。
BUF_BLOCK_MEMORY:用于存儲內存對象遍尺,包括innodb行鎖截酷、AHI(自適應哈希)涮拗、壓縮頁伙伴系統(tǒng)等乾戏。此類頁面不存在任何邏輯鏈表中。
BUF_BLOCK_REMOVE_HASH:頁面從page hash刪除后三热,被放入free_list前處于的臨時狀態(tài)鼓择。
1.1.3 頁面讀取方法
頁面讀取方法包括:NORMAL、SCAN就漾、IF_IN_POOL呐能、PEEK_IF_IN_POOL、NO_LATCH抑堡、IF_IN_POOL_OR_WATCH摆出、POSSIBLY_FREED。在詳細介紹頁面讀取方法之前首妖,先介紹隨機預讀偎漫、make young、線性預讀等概念:
隨機預讀(buf_read_ahead_random):發(fā)生在磁盤頁面讀取函數(shù)buf_read_page_low之后有缆,所有非SCAN的頁面讀取模式都會嘗試隨機預讀象踊。隨機預讀的作用是當一個extent(通常為1M,64個連續(xù)的物理頁面)內處于LRU list前1/4的熱點頁面(buf_page_peek_if_young返回為true)個數(shù)超過13時(BUF_READ_AHEAD_RANDOM_THRESHOLD)時棚壁,會采用異步IO和IO合并的方式將該extent內所有頁面都讀入buffer pool杯矩。隨機預讀是可選的,可以使用innodb_random_read_ahead參數(shù)關閉袖外。
make young(buf_page_make_young_if_needed):用于修改頁面的位置史隆,除SCAN和PEEK_IF_IN_POOL之外的所有模式都會嘗試make young。對于LRU list中的頁面曼验,如果頁面處于LRU末尾逆害,并且距離上一次讀取超過一定時長(buf_LRU_old_threshold_ms),則將頁面放入LRU list的頭部蚣驼。對于CLOCK list頁面魄幕,如果頁面被沒有independent標記的正常流量訪問,則需按innodb_independent_buffer_pool_list_move_action的配置決定是否移動到LRU list中颖杏。
線性預讀(buf_read_ahead_linear):發(fā)生在buf_page_make_young_if_needed之后纯陨,除SCAN和PEEK_IF_IN_POOL之外的模式中,第一次讀取頁面(access_time為0)時才會嘗試線性預讀。線性預讀條件比較苛刻翼抠,只有extent的數(shù)據(jù)邊界頁面咙轩,即extent的第一個頁面或者最后一個頁面讀取結束后才可能觸發(fā),并且要求在extent范圍內超過innodb_read_ahead_threshold(默認值為56)個頁面被順序訪問(判定方法是檢查頁面的訪問時間access_time)阴颖,滿足條件后會采用異步IO和IO合并的方式將下一個extent的數(shù)據(jù)都讀入buffer pool活喊。
下面來了解各種頁面讀取方法的具體作用:
NORMAL:默認獲取數(shù)據(jù)頁的方式,如果數(shù)據(jù)頁不在 buffer pool中量愧,則從磁盤讀取钾菊,如果已經(jīng)在buffer pool中,則直接返回偎肃。會嘗試進行隨機預讀煞烫、make young和線性預讀。正常加讀寫鎖累颂。
SCAN:用于掃描類的頁面讀取滞详。掃描類的特點為大批量讀取頁面,但短時間不再需要此類頁面紊馏,因此頁面讀取應盡可能不影響buffer pool的狀態(tài)料饥。所以SCAN不會進行隨機預讀、make young和線性預讀朱监。正常加讀寫鎖岸啡。
IF_IN_POOL:只在buffer pool查找目標數(shù)據(jù)頁,如果不在則直接返回為空赌朋。隨機預讀發(fā)生在磁盤頁面讀取之后凰狞,因此不會嘗試隨機預讀。會嘗試make young和線性預讀沛慢。正常加讀寫鎖赡若。
PEEK_IF_IN_POOL:與IF_IN_POOL模式相似,但只是窺探頁面是否在buffer pool中团甲,不會修改頁面的位置和干擾buffer pool狀態(tài)逾冬,因此不會隨機預讀、make young和線性預讀躺苦。正常加讀寫鎖身腻。
NO_LATCH:除了不加讀寫鎖之外埠巨,其他與NORMAL模式相似椿每,從磁盤或者buffer pool讀取頁面口叙。會嘗試進行隨機預讀肌蜻、make young和線性預讀。
IF_IN_POOL_OR_WATCH:用于purge操作邮绿,與IF_IN_POOL模式相似床未,頁面在buffer pool則直接返回飘庄,但如果頁面不在buffer pool,則會設置page watch酌泰,等待目標頁面被其他線程異步讀入buffer pool媒佣。會嘗試make young和線性預讀。正常加讀寫鎖陵刹。
POSSIBLY_FREED:用于數(shù)據(jù)統(tǒng)計操作默伍,與NORMAL模式相似,但不在乎頁面是否被freed衰琐。從磁盤或者buffer pool讀取頁面也糊。會嘗試進行隨機預讀、make young和線性預讀碘耳。正常加讀寫鎖显设。????????????????
1.1.4 Page hash
innodb為加速buffer pool中頁面的查找框弛,在每個buffer pool instance(buf_pool_t)中提供了page hash辛辨。page hash對應的結構體為hash_table_t。page hash中只存儲對應到物理文件的頁面(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ù)為默認16個狞山,只有在debug模式下才能通過innodb_page_hash_locks參數(shù)更改。
1.1.5 Zip hash
innodb的壓縮頁存儲空間由伙伴系統(tǒng)管理叉寂。為加速伙伴系統(tǒng)所有頁面的查找萍启,其頁面都會存入zip hash中。
1.2 Buffer Pool的邏輯結構
為方便管理buffer pool page屏鳍,innodb用多個邏輯鏈表將相同屬性的buffer pool頁面描述符(通常都是buf_page_t)串聯(lián)在一起勘纯。
1.2.1 Free list
free list對應暫時沒有被使用的節(jié)點,對應結構為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) free钓瞭,如前所述驳遵,其頁面的類型為BUF_BLOCK_NOT_USED。LRU list山涡、CLOCK list等鏈表需要新頁面時就會向free list申請堤结,當后者空閑頁面不足時(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最重要的鏈表媒惕,對應的結構為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) LRU,除被independent標記的頁面外来庭,所有讀進來的頁面都放在LRU list上妒蔚。LRU list頁面被用改良后的LRU算法管理,除LRU算法的基本特點之外月弛,還包括以下特點:
所有新頁面被插入距離隊尾3/8 LRU list長度的位置肴盏,此位置稱為midpoint,前5/8稱為young list帽衙,瀕臨淘汰的3/8稱為old list菜皂。
處于midpoint的頁面在超過一定時間間隔(buf_LRU_old_threshold_ms)后再次被讀取時才會被移入LRU list的頭部。
處于LRU list前1/4的的頁面屬于熱點頁面厉萝,不會被移動到LRU list的頭部(buf_page_peek_if_young)恍飘。
如上方式管理LRU list的主要原因是擔心buffer pool中經(jīng)常被使用的頁面被預讀的頁面以及全表掃描類操作淘汰,影響數(shù)據(jù)庫的性能谴垫。
1.2.3 Unzip LRU list
unzip LRU list是用于存儲LRU list中壓縮頁的解壓頁的鏈表章母,對應結構為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_block_t) unzip_LRU,存儲單元是塊描述符buf_block_t翩剪。讀取磁盤中的壓縮頁時乳怎,會調用buf_page_alloc_descriptor臨時分配一個頁面描述符buf_page_t,再調用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ù)內剃根,先通過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又能互相轉化渣慕,因此unzip LRU list實際上是LRU list的子集嘶炭。
當free list空間不足時抱慌,innodb會優(yōu)先淘汰unzip LRU list,并且只淘汰解壓頁而不淘汰壓縮頁眨猎。如果從unzip LRU list中沒能淘汰出頁面抑进,則會嘗試從LRU list中淘汰,此時如果遇到解壓頁睡陪,則會連同壓縮頁本身一起淘汰(buf_LRU_scan_and_free_block)寺渗。當解壓頁被淘汰,而壓縮頁未被淘汰時兰迫,innodb會重新為壓縮頁分配臨時頁面描述符buf_page_t信殊,將其插入LRU list中與解壓頁相同的位置,并且從page hash中刪除解壓頁汁果,將臨時buf_page_t加入page hash(buf_LRU_free_page)涡拘。
1.2.4 CLOCK list
雖然innodb小心翼翼地設計了midpoint的buffer pool page讀取方案,8.0還引入了SCAN讀取模式盡可能減少全表掃描與正常query的數(shù)據(jù)頁面間的沖突据德,但并未根除鳄乏。假設buffer pool有限,正常query需要的頁面可以恰好完整緩存到buffer pool或者還無法緩存到buffer pool晋控,若此時發(fā)起類似全表掃描的操作汞窗,全表掃描需要的頁面不論需要多少姓赤,都會導致以下問題:
LRU list隊尾中正常query的舊頁面會快速被全表掃描淘汰赡译。
全表掃描頁面和正確query競爭free buffer pool page,正常query讀取新頁面速度受影響不铆。
正常query讀入的新頁面放在midpoint處蝌焚,全表掃描讀入的頁面也是如此,如果正常query沒及時讀取新頁面誓斥,新頁面會被全表掃描流量快速推向隊尾并淘汰只洒。
上述問題的根本原因在于全表掃描頁面與正常query頁面存在耦合,能夠互相影響劳坑。CLOCK list是CDB為接觸全表掃描頁面與正常query頁面耦合而引入的新鏈表結構毕谴,對應結構為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) CLOCK。所有用independent hint標記過的頁面都會存入CLOCK list距芬,而非LRU list涝开。CLOCK list采用類似優(yōu)化過的CLOCK算法進行管理:
buf_page_t新增use_times參數(shù),記錄頁面被訪問的次數(shù)框仔,每次被讀取時use_times加1舀武。
CLOCK list上限用innodb_txsql_independent_buffer_pool_size_pct配置,其表示CLOCK list能使用的BP最大比例离斩。
CLOCK list未達到上限時银舱,需要新數(shù)據(jù)頁時直接從free list申請瘪匿。CLOCK list達到上限時,掃描CLOCK list寻馏,被掃描的頁面use_times減1棋弥,直至找到use_times為0的能順利淘汰的頁面為止(buf_CLOCK_scan_and_free_block)。
新增后臺線程buf_independent_buffer_pool_evict_thread诚欠,每隔innodb_txsql_independent_buffer_pool_evict_interval時長嘁锯,對CLOCK list進行掃描,將每個頁面的use_times減1聂薪,主動淘汰CLOCK list頁面
淘汰的CLOCK list頁面歸還給free list家乘,沒有全表掃描時,可以給正常query使用藏澳。
使用CLOCK list的方法很簡單仁锯,只需要在INSERT、DELETE翔悠、UPDATE等DML關鍵詞后添加名為independent的hint即可业崖,例如select /*+ independent */ id from t。CLOCK list的長度蓄愁、頁面被使用次數(shù)分布等信息可以使用show engine innodb status命令觀察双炕。如下圖所示:
1.2.5 Unzip CLOCK list
unzip CLOCK list與unzip LRU list類似,都是用于存儲解壓的壓縮頁撮抓,對應的結構為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ù)內,再用從free list申請的新頁面替換臨時buf_page_t咬像,存放到CLOCK list和unzip CLOCK list對應的位置中算撮,最后用buf_zip_decompress進行數(shù)據(jù)解壓。與unzip LRU list相似县昂,unzip CLOCK list也是CLOCK list的子集肮柜。
unzip CLOCK list的淘汰分兩種,一種與unzip LRU list相似:當independent hint訪問需要的頁面不足時七芭,先掃描unzip CLOCK list素挽,后掃CLOCK list,直至找到use_times為0的頁面并淘汰復用(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中所有修改過的頁面旨袒,對應結構是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模式下赡勘,用于調試buffer pool的頁壓縮功能,對應結構是buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) zip_clean捞镰。zip clean list用于存儲還沒解壓的闸与,類型為BUF_BLOCK_ZIP_PAGE的干凈壓縮頁。從源碼中觀察曼振,zip clean list中的頁面來源以下三類:
從磁盤讀取壓縮頁時(buf_page_init_for_read)
解壓頁淘汰時几迄,壓縮頁沒有一起淘汰并且壓縮頁未被修改時(buf_LRU_free_page和buf_CLOCK_free_page)
flush list中類型的頁面完成刷盤,壓縮頁重新處于干凈狀態(tài)時(buf_flush_remove)
壓縮頁完成解壓后冰评,就會從zip clean鏈表中刪除,然后根據(jù)請求是否帶有independent hint來判斷木羹,解壓頁是加入unzip LRU list還是unzip CLOCK list甲雅。
1.2.8 Zip free list
zip free list是用來管理壓縮頁伙伴系統(tǒng)的鏈表,對應結構為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對應結構可以看出妖枚,其由多個對應page size不同大小(16K苍在、8K绝页、4K荠商、2K、1K)的鏈表組成续誉。構成zip free list的所有page的類似為BUF_BLOCK_MEMORY莱没,所有頁面存入zip hash中。
壓縮頁讀取時酷鸦,需要從zip free list中申請空間到block→page.zip.data饰躲,用于存儲壓縮頁原始數(shù)據(jù)(buf_buddy_alloc),下面舉例說明其申請過程臼隔,假設申請2K的空間:
如果沒有在size為2K的鏈表上找到空閑空間嘹裂,則去4K鏈表上尋找;找到則會進行伙伴分裂摔握,高地址2K空間插入到2K鏈表中焦蘑,低地址的2K空間返回。
如果沒有在size為4K的鏈表上找到空閑空間盒发,則去8K鏈表上尋找例嘱;找到則對8K空間進行2次伙伴分裂,將高地址空間的4K和2K分別插入對應鏈表宁舰,將最低地址的2K返回拼卵。
如果沒有在size為8K的鏈表上找到空閑空間,則去16K鏈表上尋找蛮艰;找到則對16K空間進行3次伙伴分裂腋腮,將高地址空間的8K、4K和2K分別插入對應鏈表壤蚜,將最低地址的2K返回即寡。
如果沒有size為16K的鏈表,則通過buf_LRU_get_free_block從free list上申請新的頁面袜刷,通過buf_buddy_block_register函數(shù)將頁面設置為BUF_BLOCK_MEMORY聪富,插入zip hash。之后進行4次伙伴分裂著蟹,將高地址空間的8K墩蔓、4K和2K分別插入對應鏈表,將最低地址的2K返回萧豆。
伙伴系統(tǒng)調用buf_buddy_alloc釋放空間奸披。對于size為16K的空間回收,則直接清空后掛回到16K鏈表即可涮雷。對于size低于16K的空間回收阵面,會先遞歸尋找其伙伴空間,如果伙伴空間也處于空閑狀態(tài),則合并至更大的size样刷;如果合并失敗仑扑,則將釋放的空間掛至zip free對應鏈表中。通過這樣的方式颂斜,盡可能減少伙伴系統(tǒng)中的內存碎片夫壁。
1.2.9 Withdraw list
withdraw list僅僅用于buffer pool縮小過程(bp resize),對應的結構為buf_pool_t中的UT_LIST_BASE_NODE_T(buf_page_t) withdraw沃疮。下面介紹buffer pool resize的過程:
用戶設置innodb_buffer_pool_size參數(shù)會觸發(fā)buffer pool size的更新函數(shù)innodb_buffer_pool_size_update盒让,該函數(shù)設置srv_buf_resize_event,借此方式通知buffer pool更新事件司蔬。
專門用于處理buffer pool resize的后臺線程buf_resize_thread接到srv_buf_resize_event事件邑茄,調用buf_pool_resize開啟resize實際過程。
如果開啟adaptive hash index(AHI)俊啼,則暫時關閉并清空肺缕。
如果目標空間小于當前空間,則計算withdraw_target授帕。當前buffer pool instance的最后部分多余的chunks將被回收同木。接下來借用withdraw list匯總收縮空間的頁面(buf_pool_withdraw_blocks),buf_pool_withdraw_blocks詳細邏輯如下:
合并伙伴系統(tǒng)(zip free list)中空閑的頁面(buf_buddy_condense_free)跛十。
掃描free list彤路,將待收縮空間的頁面從free list摘下,存入withdraw list芥映。先處理free list的原因是避免后續(xù)LRU/CLOCK list重新分配頁面的時候使用收縮空間的頁面洲尊。
將LRU list進行一次刷盤,flush LRU長度受innodb_lru_scan_depth奈偏,innodb_buffer_pool_size變化量坞嘀,當前LRU list長度、當前CLOCK list長度等因素影響惊来。
掃描LRU list丽涩,將處于收縮空間的壓縮頁在伙伴系統(tǒng)中重新分配空間(buf_buddy_realloc),處于收縮空間的普通頁在free list重新申請頁面(buf_page_realloc)唁盏。
掃描CLOCK list内狸,將處于收縮空間的壓縮頁在伙伴系統(tǒng)中重新分配空間(buf_buddy_realloc),處于收縮空間的普通頁在free list重新申請頁面(buf_page_realloc)厘擂。
如果withdraw list的長度未達到withdraw_target,則循環(huán)掃描buffer pool锰瘸,單次循環(huán)10次沒有達成目標刽严,返回true表示本次回收不成功。
withdraw list的長度達到withdraw_targe后,逐一校驗所有收縮空間的頁面是否都在withdraw list中(檢查block→in_withdraw_list狀態(tài))舞萄。
停止正在進行的buffer pool load(頁面預熱的一種方式眨补,后續(xù)將詳細介紹)。
如果上一次buf_pool_withdraw_blocks回收失敗倒脓,則打印所有非尚未啟動狀態(tài)的事務(trx_sys→mysql_trx_list)到錯誤日志告警撑螺,然后休眠一段時間后再次進入buf_pool_withdraw_blocks掃描buffer pool。休息時間規(guī)則為:重試次數(shù)5次及以內崎弃,休眠時間為重試次數(shù)*2s甘晤,重試次數(shù)大于5次,則只休眠10s饲做。
上述所有過程允許并發(fā)負載线婚,接下來將獲取buffer pool所有mutex,鎖定整個buffer pool盆均,不再允許任何并發(fā)塞弊。
刪除或者新增chunks:如果是收縮buffer pool,使用deallocate_chunk將多余的chunk泪姨,釋放withdraw list游沿;如果是擴展buffer pool,使用ut_zalloc_nokey_nofatal分配新內存肮砾,使用buf_pool_register_chunk初始化新chunk诀黍。
根據(jù)調整的結果,將buf_pool→curr_size調整到chunks的倍數(shù)唇敞,再重新設置innodb_buffer_pool_size蔗草。
如果新的buffer pool size是舊size的2倍或者不到1/2,將重新設置page hash和zip hash(buf_pool_resize_hash)疆柔。
釋放buffer pool所有鎖咒精,允許并發(fā)負載。
如果新的buffer pool size是舊size的2倍或者不到1/2旷档,進行l(wèi)ock_sys_resize和dict_resize模叙,重新設置lock_sys->rec_hash、lock_sys->prdt_hash鞋屈、lock_sys->prdt_page_hash范咨、dict_sys->table_hash和dict_sys→table_id_hash等結構的大小。
重新開啟AHI厂庇。
1.2.10 Hazard point
hazard point是用來避免逆向掃描鏈表過程的迭代器渠啊。hazard point與普通迭代器不同,當掃描過程的下一節(jié)點被其他操作移除或替換了時权旷,hazard point能調整位置并指向被移除或替換的節(jié)點的下一有效節(jié)點替蛉,避免逆向掃描過程失敗苛预。使用hazard point逆向掃描鏈表的時間復雜度為O(N)俺亮,可以避免逆向掃描失敗時從頭開始掃描而導致的最高 O(N*N)的時間復雜度该窗。因此hazard point廣泛應用于flush list荠察、LRU list、CLOCK list等鏈表的批量操作中的鏈表掃描中镣煮。
hazard point的實現(xiàn)也不復雜姐霍,對應的基類為HazardPointer,包括get典唇、set镊折、is_hp、adjust蚓聘、move等方法腌乡。使用于不同的鏈表時,基于基類派生出不同的子類夜牡,并添加定制化的方法与纽。當逆向掃描鏈表時,每次循環(huán)通過get函數(shù)獲取下一個節(jié)點塘装,并將節(jié)點的下一節(jié)點設置為hazard point急迂。并發(fā)的負載如果修改了鏈表,并且節(jié)點是hazard point(is_hp返回為true)蹦肴,則使用adjust函數(shù)將被修改的節(jié)點的下一節(jié)點設置為hazard point僚碎。
1.2.11 總結
上述介紹的鏈表都是用于管理需要持久化的頁面的緩存。從內存來源來看阴幌,除未解壓的壓縮頁的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ī)鏈表,服務于數(shù)據(jù)庫正常服務芍锚,zip clean list和withdraw list屬于特殊用途的list震捣,zip clean list用于debug模式下壓縮頁面的調試荔棉,withdraw list僅僅服務于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的結構體(buf_pool_t)的buf_pool_ptr渣触,接著為每個instance創(chuàng)建buf_pool_create線程來初始化instance中的鏈表羡棵、chunks、mutex嗅钻、page_hash皂冰、zip_hash、hazard_point养篓、用于purge異步讀取頁面的watch結構體等秃流。chunks初始化結束后,逐一對每個block調用buf_block_init柳弄,對block的io舶胀、是否在邏輯鏈表、是否在page hash碧注、zip hash等信息進行初始化嚣伐,并添加到buf_pool->free鏈表中。
Buffer pool初始化過程中萍丐,內存申請比較常見的是使用減少了用戶空間與內核空間地址拷貝(零拷貝)的mmap轩端。在chunks初始化時,也會調用os_mem_alloc_large分配內存逝变,嘗試在Linux支持的情況下采用HUGETLB的方式分配內存基茵。
2.2 單機buffer pool預熱
MySQL官方為了讓實例能夠在重啟后快速恢復到重啟前的狀態(tài),在MySQL 5.6版本開始支持了buffer pool單機預熱的功能壳影。單機預熱功能分為dump和load兩個步驟拱层。
dump將實例重啟前的所有buffer pool instance的LRU list中每個頁面的space_id和page_no記錄到本地innodb_buffer_pool_filename文件中。(space_id是數(shù)據(jù)文件的編號态贤,page_no數(shù)據(jù)文件內的偏移舱呻。space_id與page_no可以唯一確定一個頁面,詳見淺析InnoDB文件結構)悠汽。為了盡可能避免掃描過程對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ù)預熱假抄,將實例快速恢復成重啟前的狀態(tài)怎栽。load操作會遍歷innodb_buffer_pool_filename文件兩次(由于每個頁面的信息只包括space_id和page_no丽猬,一共只占8個字節(jié),正常情況下文件只有幾百K到幾G的級別熏瞄,兩次掃描帶來的IO量尚可接受)脚祟。第一次遍歷整個文件以確定其中總共包含的page記錄數(shù),如果page記錄數(shù)超過當前buffer pool size强饮,調整待讀入的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ù)預熱的特點如下:
能夠精準高效地恢復實例重啟前的狀態(tài)斗蒋。
應用場景比較有限捌斧,只能用于單機預熱,無法解決主從切換吹泡、跨機遷移場景的預熱需求骤星。
2.3 主從buffer pool同步
MySQL官方提供的dump和load操作只能用于單機預熱的根本原因是:MySQL的主從復制是邏輯復制,非物理復制爆哑。數(shù)據(jù)雖然是一致的洞难,但是主從數(shù)據(jù)的分布是不一樣的。InnoDB的所有數(shù)據(jù)都存儲于B+樹中揭朝,也可簡單理解為主從所對應數(shù)據(jù)的B+樹的結構是不一樣的队贱,在從庫按部就班加載B+樹相同位置的頁面無法解決主從切換、跨機遷移等多場景的預熱問題潭袱。
主從緩存同步是CDB針對主從多場景下快速buffer pool預熱的解決方案柱嫌,其從邏輯復制的思路出發(fā),通過主從邏輯數(shù)據(jù)一致的角度解決了多場景的預熱問題屯换。主從緩存同步分為snapshot编丘、transmit、recover三個步驟:
snapshot將主庫buffer pool狀態(tài)邏輯導出到主庫本地的ib_bp_info文件中彤悔,具體過程如下(buf_snapshot):
逐一掃描每個所有buffer pool instance的LRU list和CLOCK list中的每個頁面嘉抓,將頁面的space_id和page_no匯總到以space_id為key,unordered_set為value的lru_maps中晕窑。掃描結束后抑片,所有頁面按照space_id進行了初步歸類。
逐一處理每個space_id下的頁面杨赤。通過頁面的前后節(jié)點指針敞斋,將相鄰的頁面串聯(lián)成頁面鏈表截汪,并用最左頁面、最左頁面的第一條用戶記錄植捎、最右頁面衙解、最右頁面最后一條用戶記錄代表該頁面鏈表,存入multi_ranges中(link_page)鸥跟。
將multi_ranges中所有record range按照一定格式落盤到本地ib_bp_info文件中丢郊。
針對如何傳輸ib_bp_info文件,有很多備選方案:采用scp來傳輸文件誤操作的可能性比較高医咨,并且經(jīng)常修改傳輸腳本;采用binlog傳輸會加重binlog的負擔架诞,加大主從延遲的風險拟淮,方案復雜,風險大谴忧;采用內建表的方式同步文件又擔心引入兼容性的問題很泊。主從緩存通過最終選擇了transmit方案,即在slave新創(chuàng)建一種與IO線程相似沾谓,需要與master建立tcp連接的transmit線程(handle_slave_transmit)委造,該線程負責向master發(fā)送ib_bp_info文件傳輸申請,并負責接收ib_bp_info文件均驶。transmit方案不侵入主從復制的原有邏輯昏兆,對于不支持transmit的主庫,也僅僅是無法從主庫獲取ib_bp_info文件妇穴,不存在兼容問題爬虱。
recover將ib_bp_info文件中代表的數(shù)據(jù)加載到從庫buffer pool中。recover可以在跨機遷移前腾它、版本升級前跑筝、主從切換前或切換過程中進行,也可以定期將主庫的buffer pool同步到從庫瞒滴,隨時做好切換準備曲梗。recover具體過程如下(buf_recover):
按固定格式識別ib_bp_info中的record_range信息。
在從庫通過B+樹搜索record_range最左頁面的第一條用戶記錄和最右頁面的最后一條用戶記錄對應的兩個頁面妓忍。
將上述兩個頁面之間的所有頁面加載到buffer pool中虏两。為了避免新加載的頁面互相淘汰,recover操作還支持將新頁面直接加載到LRU list的頭部而不是midpoint单默。
重復1-3步驟碘举,直至ib_bp_info中所有recored_range被處理完成。
2.4 Change buffer優(yōu)化
2.4.1 原理
innodb的二級索引與cluster index相同搁廓,也存儲在B+樹中引颈。cluster index與二級索引的B+樹節(jié)點散落在數(shù)據(jù)文件中耕皮。在此架構下,一條普通的DML都可能因為需要修改多個二級索引蝙场,而帶來大量隨機IO凌停,對innodb的性能帶來巨大挑戰(zhàn)。
change buffer就是為了解決二級索引隨機IO問題而引入的緩存售滤。當待插入罚拟、刪除、修改的記錄所在的二級索引頁面不在buffer pool中時完箩,修改內容會以記錄的形式緩存到change buffer中赐俗。當二級索引頁面最終被讀入buffer pool中時,需要檢查change buffer中是否有該頁面的修改記錄弊知,如果存在需要將修改記錄合并到新讀入的二級索引頁面上阻逮,再返回。change buffer適用于buffer pool大小有限秩彤,無法將所有數(shù)據(jù)緩存到buffer pool叔扼,并且有大量二級索引需要更新的場景。但如果buffer pool足夠大到能緩存所有索引頁面漫雷,或者隨機IO的速度和順序IO的速度所差無幾瓜富,也可以考慮關閉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)妹蔽,根結點的page_no固定為4(FSP_IBUF_TREE_ROOT_PAGE_NO)椎眯,所有innodb用戶表的二級索引變更都緩存在同一顆B+樹上。一條change buffer record中的信息包括:
IBUF_REC_FIELD_SPACE:二級索引的space_id胳岂。
IBUF_REC_FIELD_MARKER:用于以4.1.x版本為界區(qū)分新舊版本编整,新版本該值為0。
IBUF_REC_FIELD_PAGE:二級索引的 page_no乳丰。
IBUF_REC_FIELD_METADATA分為以下三項信息:
IBUF_REC_OFFSET_COUNTER:相同space_id和page_no頁面內的操作序號掌测,單調遞增。
IBUF_REC_OFFSET_TYPE:緩存的操作的類型产园,IBUF_OP_INSERT(插入)汞斧;IBUF_OP_DELETE_MARK (用戶刪除夜郁,二級索引的刪除都是delete_mark + insert);IBUF_OP_DELETE(purge操作)粘勒。
IBUF_REC_OFFSET_FLAGS:待操作的用戶記錄格式竞端,REDUNDANT / COMPACT。
IBUF_REC_FIELD_USER:用戶列數(shù)據(jù)庙睡。
change buffer的B+樹以(space_id事富、page_no、counter)為主鍵乘陪,來確保記錄的唯一性以及apply時的順序统台。change buffer在正常運行過程中必須保證緩存合并到二級索引后,二級索引不能發(fā)生分裂或合并操作暂刘,否則緩存到主鍵將對應到未知的頁面而失效饺谬。為此需要實時獲取目標二級索引頁面的剩余空間,innodb的方案是在數(shù)據(jù)文件中(ibdata或ibd)谣拣。page_size是默認大小16KB的情況下,page_no為1族展、1+16384*N的頁面都是FIL_PAGE_IBUF_BITMAP類型森缠,用于記錄其后16384個頁面change buffer的信息。FIL_PAGE_IBUF_BITMAP頁面用4個bit來描述一個頁面:
IBUF_BITMAP_FREE占2個bit仪缸,表示頁面空閑的空間范圍贵涵。其值是頁面當前剩余空間max_ins_size/512字節(jié)的結果。0表示max_ins_size處于[0,512)字節(jié)中間恰画,1表示max_ins_size處于[512,1024)字節(jié)中間宾茂,2表示max_ins_size處于[1024,2048)字節(jié)中間,3表示max_ins_size大于2048字節(jié)(ibuf_index_page_calc_free_bits)拴还。
IBUF_BITMAP_BUFFERED占1個bit:表示頁面是否有操作緩存跨晴,準備向change buffer B+tree插入前會將對應二級索引頁面的IBUF_BITMAP_BUFFERED設置為true(ibuf_insert_low)。二級索引頁面物理頁面被讀入buffer pool時會根據(jù)該標記判斷是否需要進行change buffer 合并操作(buf_page_io_complete)片林。
IBUF_BITMAP_IBUF占1個bit:表示頁面是否為change buffer B+tree 的節(jié)點端盆。
DML運行過程中會實時將二級索引信息變化更新到FIL_PAGE_IBUF_BITMAP頁面中。在change buffer插入過程中费封,如果發(fā)現(xiàn)準備插入的change buffer record可能會導致二級索引頁面分裂焕妙,則插入失敗并調用ibuf_get_merge_page_nos觸發(fā)一次從當前cursor位置附近開始的異步的change buffer 合并操作,目的是盡量將當前頁面的緩存操作做一次合并(ibuf_insert_low)弓摘。此外為避免出現(xiàn)空頁面焚鹊,需要在purge線程真正刪除記錄時使用ibuf_get_volume_buffered預估合并完頁面所有change buffer record之后的記錄數(shù),如果二級索引頁面上記錄等于1(加上purge線程這次緩存的刪除操作將變?yōu)榭枕撁妫┤拖祝瑒t索引插入失敗末患,改走正常讀入物理頁面邏輯研叫。
二級索引記錄沒有trx_id這一系統(tǒng)列,這為緩存purge線程刪除記錄操作帶來了困難阻塑,難以區(qū)分真正要刪除掉的記錄蓝撇。為避免purge線程刪除操作誤刪用戶reinsert的二級索引記錄,如果purge操作先進入ibuf_insert陈莽,則用戶后續(xù)的insert操作將放棄緩存插入渤昌,轉而讀取物理頁面;如果insert操作先進入ibuf_insert走搁,則purge操作也放棄緩存刪除独柑。
change buffer頁面的與普通索引頁面相同,其修改也是先記錄redo日志后刷盤私植,在crash recover時忌栅,同樣需要redo來保證一致性。change buffer可以理解為記錄在index page上的二級索引更改日志曲稼。
2.4.3 緩存條件
只有滿足一定條件時索绪,二級索引變更才會被change buffer緩存,判斷條件如下(ibuf_should_try):
用戶設置了innodb_change_buffering贫悄,innodb_change_buffer_max_size非0瑞驱。
非數(shù)據(jù)字典表空間,非聚簇索引窄坦,非空間索引唤反。
對IBUF_OP_INSERT操作要求二級索引為非unique類型,對于其他操作不限制二級索引是否為unique類型鸭津。
只緩存二級索引葉子結點(不包括B+樹只有一層時的根節(jié)點)彤侍。
表上沒有flush 操作,例如執(zhí)行flush table for export時逆趋,不允許對表進行緩存 (通過dict_table_t::quiesce進行標識)盏阶。
二級索引不能只包含降序列。
2.4.4 合并時機
有以下幾種場景會觸發(fā)change buffer合并操作:
用戶線程觸發(fā)二級索引頁面的讀取時父泳,例如用戶通過二級索引查詢數(shù)據(jù)(buf_page_io_complete)般哼。
嘗試將緩存插入change buffer,但預估二級索引頁面空間不足惠窄,可能導致頁面分裂時蒸眠,會調用ibuf_get_merge_page_nos從當前cursor位置附近開始的異步的change buffer merge(ibuf_insert_low)。
change buffer B+tree size空間變化(ibuf_contract_after_insert):
size小于innodb_change_buffer_max_size配置大小時杆融,不做處理楞卡。
size介于innodb_change_buffer_max_size配置大小與innodb_change_buffer_max_size配置大小 + 5之間,執(zhí)行一次異步change buffer merge,位置隨機蒋腮。
size大于innodb_change_buffer_max_size配置大小 + 5 (IBUF_CONTRACT_ON_INSERT_SYNC)時淘捡,執(zhí)行一次同步change buffer merge(ibuf_contract),位置隨機池摧。
size大于innodb_change_buffer_max_size配置大小 + 10(IBUF_CONTRACT_DO_NOT_INSERT)時焦除,執(zhí)行一次同步change buffer merge(ibuf_contract),不再允許change buffer插入作彤。
innodb master線程調用ibuf_merge_in_background 合并change buffer膘魄。master線程較為空閑時,會以100%的io capacity合并change buffer(srv_master_do_idle_tasks)竭讳,系統(tǒng)活躍時則根據(jù)change buffer size來控制io capacity创葡,size越大io capacity比例越高(srv_master_do_active_tasks)。
slow shutdown時绢慢,會調用ibuf_merge_in_background全量合并change buffer(srv_master_do_shutdown_tasks)灿渴。
對某個表執(zhí)行flush table for export操作時,會調用ibuf_merge_space強制合并change buffer(row_quiesce_table_start)胰舆。合并change buffer是為flush for export準備的骚露,避免準備拷貝的ibd文件對change buffer有依賴。
2.5 Adaptive hash index優(yōu)化
2.5.1 原理
innodb的數(shù)據(jù)存儲于B+樹中缚窿,B+樹通常不會太高荸百,通常只有3到5層。從根節(jié)點到葉節(jié)點的尋路涉及到多層頁面內記錄的比較滨攻,即使所有路徑上的頁面都在內存中,也是比較消耗CPU的操作蓝翰。
對尋路到CPU開銷優(yōu)化分兩部分光绕,第一部分為盡可能避免尋路次數(shù),innodb為此設計了多個優(yōu)化畜份,例如一次尋路多緩存幾條記錄到fetch_cache中诞帐;尋路結束將cursor緩存到row_prebuilt_t::pcur,方便下次查詢復用爆雹。第二部分為盡可能避免單詞尋路的開銷停蕉。Adaptive hash index(AHI)便是為此而設計。AHI可以理解為建立在B+樹上的索引钙态,它采用自適應的方式管理慧起,為B+樹尋路建立以查詢條件為key,B+樹record地址為value的hash index册倒。
AHI的大小為buffer pool size的1/64蚓挤,在buf_pool_init中調用btr_search_sys_create初始化。為了避免AHI的鎖競爭壓力,innodb支持AHI分區(qū)灿意,可以使用innodb_adaptive_hash_index_parts參數(shù)配置分區(qū)個數(shù)估灿,默認為8。當buffer pool size動態(tài)調整大小為原大小的2倍以上或者1/2以下時缤剧,AHI也隨之調用btr_search_sys_resize調整大邢谠(buf_pool_resize)。
2.5.2 創(chuàng)建過程
當innodb_adaptive_hash_index參數(shù)打開荒辕,非臨時表汗销、非空間類型的索引會累加索引被使用的次數(shù),當使用次數(shù)小于17(BTR_SEARCH_HASH_ANALYSIS)兄纺,當前索引被認為不夠熱大溜,被自動忽略(btr_search_info_update)。索引次數(shù)達標后估脆,進入下一步驟钦奋。
AHI的key與用戶的查詢條件相關,每個索引(dict_index_t)都包含用于存儲查詢條件的search_info(btr_search_t)疙赠。search_info主要用三項信息描述查詢條件:完整匹配的匹配列數(shù)n_fields付材,不完整匹配的列的前綴字節(jié)數(shù)n_bytes,是否為左側匹配left_side圃阳。如果查詢條件與上一次緩存的相同厌衔,則將search_info->n_hash_potential加1。否則清空search_info捍岳,重新設定index的查詢條件(btr_search_info_update_hash)富寿。
即使索引通過了篩選,查詢條件通過了考核锣夹,AHI也不會對索引的每個頁面都建立hash index页徐。對于需要創(chuàng)建AHI的頁面還需要經(jīng)過一輪篩選:某頁如果能通過當前search_info命中,則對頁面的n_hash_helps加1(當前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)建锣险。原因是當前處于B+樹搜索的關鍵路徑吼和,在此處等鎖會拖累其他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):
再次判斷innodb_adaptive_hash_index參數(shù)打開,并且當前索引非臨時表索引崖咨。
加AHI的S鎖锻拘,如果當前頁面已經(jīng)創(chuàng)建AHI并且與當前search_info不同,則解鎖后調用btr_search_drop_page_hash_index刪除舊AHI索引击蹲,否則直接解開S鎖署拟。
根據(jù)當前search_info挑選出具有代表性的記錄,計算hash fold值歌豺,并記錄在folds和recs兩個數(shù)組中推穷。舉例,假設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)
重新加上AHI的X鎖痕惋,檢查block上的search_info区宇,如果發(fā)生變化則放棄本次AHI構建,否則插入上一步準備好的folds和recs兩個數(shù)組值戳,插入AHI中议谷。
解開AHI的X鎖,釋放folds和recs兩個數(shù)組的內存堕虹。
2.5.3 使用條件
AHI的使用在B+樹搜索的關鍵路徑上卧晓,使用AHI的幾個條件如下(btr_cur_search_to_nth_level):
AHI處于非X鎖狀態(tài)。AHI查詢過程中要加AHI的S鎖赴捞,等鎖會導致AHI得不償失禀崖。
加鎖模式為BTR_SEARCH_LEAF或BTR_MODIFY_LEAF,即本次查詢不變更B+樹結構螟炫,只在葉子節(jié)點查詢或者修改頁面內的單條記錄。
索引上的search_info顯示上一次AHI使用成功艺晴。
當前索引非空間索引昼钻。
滿足上述條件后,在正式搜索AHI之前封寞,會再次對比索引的最新search_info是否已經(jīng)被重置了然评,即search_info→n_hash_potential是否等于0,以及當前查詢條件的列數(shù)相比search_info是否完整狈究。前者等于0意味著當前舊AHI記錄已經(jīng)被刪除碗淌;后者查詢條件不足意味著當前查詢條件不足以構建AHI的key值進行查詢,兩種情況都只能放棄AHI查詢,轉而使用正常的B+樹搜索(btr_search_guess_on_hash)亿眠。接下來碎罚,將查詢條件轉化為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è)務才能經(jīng)過層層篩選,創(chuàng)建出AHI岔帽,從該功能中受益玫鸟。
2.5.4 自適應維護
自動插入與更新:葉節(jié)點的新記錄如果未產(chǎn)生頁面重組,則新記錄插入或更新到AHI中(btr_search_update_hash_on_insert犀勒、btr_search_update_hash_node_on_insert)屎飘。
自動記錄刪除:記錄刪除時,如果記錄的fold存在于AHI中账蓉,則將其刪除(btr_search_update_hash_on_delete)枚碗。
自動全頁面記錄刪除:AHI作用是加速內存中B+數(shù)的搜索。當頁面從內存淘汰(buf_LRU_free_page或buf_CLOCK_free_page)時铸本,AHI會自動刪除肮雨。此外,頁面發(fā)生合并箱玷、分裂時記錄地址發(fā)生變更怨规,調整后的頁面相當于新讀入內存的頁面,舊AHI信息會刪除(btr_search_drop_page_hash_index)锡足。刪除方法為找到頁面所有具有代表性的folds波丰,調用ha_remove_all_nodes_to_page刪除。
自動全索引AHI刪除:當索引被刪除時則需要所有此索引相關AHI刪除舶得。刪除方法為遍歷LRU list和CLOCK list上的所有頁面掰烟,將該索引的頁面存入數(shù)組中。最后注意刪除每個頁面的AHI記錄(btr_drop_ahi_for_index)沐批。
自動全表記錄AHI刪除:當表被刪除時纫骑,觸發(fā)全表記錄的AHI刪除。刪除方法為逐一刪除每個索引的AHI記錄(btr_drop_ahi_for_table)九孩。不過當表非常大時先馆,全表記錄的AHI刪除非常緩慢,刪表過程持有dict_sys->mutex躺彬,將長時間阻塞此實例其他DDL操作煤墙,其他DDL操作也會長期持有更多資源梅惯,進而阻塞整個系統(tǒng)。一種tradeoff的做法是當改表的AHI記錄數(shù)量足夠多或者比例足夠大時將整個AHI都清空仿野。AHI的清空不影響數(shù)據(jù)查詢的正確性铣减,而AHI最終還能再次創(chuàng)建。上述方法可以用此暫時的效率降低換來系統(tǒng)的穩(wěn)定设预。
第三部分徙歼、頁面讀取過程解讀
3.1 頁面讀取堆棧
buf_page_get_genBuf_fetch::single_pageBuf_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
復制
3.2 核心函數(shù)概述
buf_page_get_gen
檢查頁面獲取mode是否是7類獲取模式之一,不是則報錯鳖枕。
如果頁面獲取類型是NORMAL并且頁面不是系統(tǒng)臨時系統(tǒng)表魄梯,則使用Buf_fetch_normal的single_page函數(shù)獲取頁面;否則頁面采用Buf_fetch_other的single_page函數(shù)獲取頁面宾符。
Buf_fetch<T>::single_page
調用Buf_fetch子類的的get函數(shù)(例如Buf_fetch_normal::get酿秸、Buf_fetch_other::get)獲取頁面,如果get函數(shù)結果為DB_NOT_FOUND魏烫,則向上返回nullptr辣苏,否則繼續(xù)往下。
檢查頁面獲取類型是否為樂觀類型(IF_IN_POOL或PEEK_IF_IN_POOL)哄褒,持鎖獲取block的io_fix(表示IO操作狀態(tài))的狀態(tài)稀蟋,如果是BUF_IO_READ狀態(tài),說明頁面正在讀到BP的過程中呐赡,不繼續(xù)等待其完成退客,將block的buf_fix_count(表示當前對該頁面操作的次數(shù))減1,返回nullptr链嘀。
用check_state和debug_check檢查block的狀態(tài)萌狂,如果狀態(tài)是DB_NOT_FOUND,則返回nullptr怀泊;如果狀態(tài)是DB_FAIL則從1重新開始茫藏。
調用buf_page_is_accessed獲取block的第一次讀取時間access_time,如果access_time=0霹琼,表示這是block第一次被讀取务傲。
如果頁面獲取模式非scan類型,則為access_time為0的頁面設置access_time枣申,假設頁面獲取模式非scan且非PEEK_IF_IN_POOL树灶,則調用buf_page_make_young_if_needed在需要的時候make young,即將頁面移入lru的young list頭部糯而,避免其快速被淘汰。
使用buf_wait_for_read函數(shù)等待頁面完成讀入泊窘。
如果讀取模式不是PEEK_IF_IN_POOL或者SCAN熄驼,并且是第一次讀入的話調用buf_read_ahead_linear進行線性預讀像寒。結束后向上一層函數(shù)返回block。
Buf_fetch_normal::get
使用normal模式讀取頁面瓜贾。先調用look_up函數(shù)page_hash中查找诺祸,找到則用buf_block_fix對page的buf_fix_count++,表示增加了一個線程在讀取這個頁面祭芦,解鎖page_hash分區(qū)鎖筷笨。
lookup失敗則使用read_page函數(shù)在磁盤中讀取該頁面。失敗則再次回到1嘗試(異步IO的緣故)龟劲,讀取成功后返回block胃夏。
Buf_fetch_other::get
使用非normal模式讀取頁面。先調用look_up函數(shù)page_hash中查找昌跌。調用buf_block_fix對buf_fix_count++仰禀,表示增加了一個線程在讀取這個頁面,解鎖page_hash分區(qū)鎖蚕愤。(臨時表空間使用block->mutex在用戶線程和刷盤線程之間同步答恶,需要額外加block->mutex)
如果讀取模式是IF_IN_POOL_OR_WATCH,調用is_on_watch等待block被讀取并得到block萍诱。
如果block此時非空悬嗓,從循環(huán)中退出并返回找到block。
如果頁面獲取類型是樂觀類型(IF_IN_POOL或PEEK_IF_IN_POOL)或者為IF_IN_POOL_OR_WATCH裕坊,返回nullptr包竹。
使用read_page函數(shù)在磁盤中讀取該頁面。失敗則再次回到1嘗試(異步IO的緣故)碍庵,讀取成功后返回block映企。
Buf_fetch<T>::lookup
在hash_scan中查找頁面。先獲取分區(qū)鎖静浴。
加鎖后堰氓,擔心page_hash情況有變,用buf_page_hash_lock_s_confirm再次確認苹享,如果確認鎖變化了双絮,則在循環(huán)中釋放舊鎖加上新鎖,再次檢測得问,直到變化結束囤攀。
如果guess block不在bp中,或者不屬于BUF_BLOCK_FILE_PAGE類型說明猜測失敗宫纬。嘗試用buf_page_hash_get_low從page_hash中讀取頁面焚挠,如果還是讀取失敗則解鎖page_hash的分區(qū)鎖然后返回nullptr,如果從buf_page_hash_get_low讀到頁面漓骚,從buf_pool->watch檢查這個頁面是否是被watch的頁面蝌衔,是的話解鎖page_hash的分區(qū)鎖然后返回nullptr榛泛,否的話返回頁面。
Buf_fetch<T>::read_page
scan模式異步調用buf_read_page_low異步讀取頁面噩斟。其他模式調用buf_read_page同步讀取頁面曹锨,buf_read_page底層也是調用buf_read_page_low,但是輸入的是同步讀取參數(shù)剃允。讀取失敗則最多重試BUF_PAGE_READ_MAX_RETRIES次沛简,直至成功。
同步讀取的話還會嘗試隨機預讀斥废,隨機預讀為:一個extent中超過一定數(shù)量的頁面被讀取則將整個extent全部讀取到內存椒楣。
buf_read_page_low
此函數(shù)的第四參數(shù)mode并非Page_fetch,而是BUF_READ_IBUF_PAGES_ONLY和BUF_READ_ANY_PAGE二選一营袜,前者是指讀取ibuf頁面撒顿。
支持同步和異步讀取,如果是ibuf_bitmap或者系統(tǒng)表空間事務頭頁面(0荚板,5)則直接不論傳入的參數(shù)是同步讀取還是異步讀取凤壁,都改為同步讀取。
使用buf_page_init_for_read準備一個空block跪另,函數(shù)邏輯后續(xù)有詳細介紹拧抖。
如果是同步模式,調用thd_wait_begin免绿,準備同步等待io完成唧席。
如果是壓縮頁面的話,讀入的內存位置是bpage->zip.data嘲驾,否則是((buf_block_t *)bpage)->frame淌哟。調用fil_io讀取頁面。
如果是同步模式辽故,調用thd_wait_end徒仓,表示同步io結束。
如果出現(xiàn)表空間不存在等情況導致io不成功誊垢,調用buf_read_page_handle_error報錯掉弛。
如果是同步模式,調用buf_page_io_complete喂走。buf_page_io_complete簡單分兩類BUF_IO_READ殃饿、BUF_IO_WRITE。對于BUF_IO_READ芋肠,通過頁面前后的checksum檢查頁面是否損壞乎芳,損壞的話,得看srv_force_recovery的級別,如果低于SRV_FORCE_IGNORE_CORRUPT奈惑,則使用報錯谬晕。沒有報錯則修改頁面狀態(tài)為BUF_IO_NONE,更新統(tǒng)計信息携取;對于BUF_IO_WRITE則是加鎖后調用buf_flush_write_complete將頁面移除flush_list,更新相應統(tǒng)計信息帮孔。
返回雷滋。
buf_page_init_for_read
BUF_READ_IBUF_PAGES_ONLY是ibuf的預讀路徑。該類型下文兢,recv_no_ibuf_operations為false(沒有進行crash recovery)晤斩,并且目標頁面不是ibuf的層次結構中的2層或者3層頁面(ibuf_page函數(shù)范圍false),則返回nullptr姆坚。
如果page_size是壓縮頁面澳泵,而請求的是非壓縮頁面,并且沒有進行crash recovery時兼呵,將block設置為nullptr兔辅。否則使用buf_LRU_get_free_block獲取一個free_block。buf_LRU_get_free_block邏輯后續(xù)有詳細介紹击喂,大概內容為從LRU list中找到合適的block维苔,如果沒有從LRU list獲取到則嘗試刷盤。
如果block為nullptr懂昂,說明準備讀取的是一個壓縮頁介时,用buf_page_alloc_descriptor分配一個臨時頁面描述符bpage,再調用buf_buddy_alloc從伙伴系統(tǒng)中分配一個空間存放壓縮頁原始數(shù)據(jù)凌彬。這個臨時bpage會等到頁面被解壓成功沸柔,使用buf_relocate函數(shù)從free_list中申請到的bpage替換掉臨時bpage,放在LRU list相同的位置铲敛。
調用buf_page_hash_get_low嘗試從page_hash中讀取該block褐澎。如果讀到頁面,并且該block不是被watch的頁面原探,說明這個頁面已經(jīng)在buffer pool中了乱凿。釋放臨時資源:釋放臨時分配的描述符(如果分配了)、釋放伙伴系統(tǒng)中分配的空間(如果申請了)咽弦、將空閑block插入free_list(如果block不為空)徒蟆,跳到函數(shù)結束處準備返回。
block不為空型型,則調用buf_page_init:初始化該block段审,并調用HASH_INSERT該頁面插入page_hash。調用buf_LRU_add_block將block插入LRU闹蒜。參數(shù)is_old為true的話寺枉,插入old_list頭部抑淫,參數(shù)is_old為false的話,插入young list頭部姥闪。如果LRU_list很短始苇,LRU_list將不再劃分young、old筐喳,新block將直接被插入LRU_list頭部催式。如果頁面是壓縮頁面的解壓頁,還會先設置block->page.zip.data避归,然后將該頁面加入unzip_LRU_list中荣月。如果頁面是臨時表頁面并且開啟了srv_temp_tablespace_fast_cleanup,還會調用temp_tablespace_pages_map_add梳毙,將頁面插入buf_pool->temp_tablespace_pages哺窄。
如果block為空,說明是壓縮頁账锹。將從伙伴系統(tǒng)申請到的空間設置到bpage->zip.data中萌业。調用buf_page_init_low初始化臨時頁面描述符bpage。調用buf_LRU_add_block將block插入LRU牌废。參數(shù)is_old為true的話咽白,插入old_list頭部,參數(shù)is_old為false的話鸟缕,插入young list頭部晶框。如果LRU_list很短,LRU_list將不再劃分young懂从、old授段,新block將直接被插入LRU_list頭部。DEBUG模式下番甩,將臨時描述符bpage加入通過buf_LRU_insert_zip_clean函數(shù)加入zip_clean鏈表中侵贵。將bpage狀態(tài)設置為BUF_IO_READ。
如果是BUF_READ_IBUF_PAGES_ONLY類型缘薛,調用ibuf_mtr_commit提交mtr窍育。否則返回頁面。
buf_LRU_get_free_block
調用buf_LRU_get_free_only:如果free_list有空閑的block則將block->page.zip設置為空后返回宴胧。
如果沒有從free_list得到空閑的block漱抓,則判斷多次從lru_list中淘汰block并獲取。如果設置了try_LRU_scan恕齐,則調用buf_LRU_scan_and_free_block開始第一輪掃描LRU list乞娄,最多從LRU list尾部掃描srv_LRU_scan_depth個block,如果還沒成功的話調用buf_flush_single_page_from_LRU將flush list中最尾部的頁面刷入磁盤。如果還是沒有獲取到空閑block則跳轉到步驟2重新開始第二輪掃描仪或。
第二輪掃描确镊,即使沒有設置try_LRU_scan,也會掃描整個lru list范删。沒有獲取到空閑block則再調用buf_flush_single_page_from_LRU將flush list中最尾部的頁面刷入磁盤蕾域。還是沒獲取空閑頁面則進入第三輪。
第三輪掃描開始到旦,每輪中間間隔10ms束铭,其他和第二輪掃描相同。如果超過20輪都沒找到合適的block厢绝,則向用戶告警:調大BP或升級操作系統(tǒng)版本。
buf_LRU_get_free_only
負責從buffer_pool的free_list中獲取一個空閑的block带猴。獲取的方法是用封裝好的list操作接口:UT_LIST_GET_FIRST昔汉。
獲取不到空閑block直接返回nullptr。如果順利獲取的空閑的block拴清,需要判斷此block是否準備被resize buffer pool操作回收:使用buf_block_will_withdrawn判斷當前頁面是否準備回收的頁面(最后一個chunk的block)靶病,使用buf_get_withdraw_depth判斷withdraw list的頁面是否達到釋放目標大小。如果經(jīng)上述判斷頁面不需要被resize回收則返回block口予,否則將block插入withdraw list中娄周,循環(huán)使用UT_LIST_GET_FIRST從free_list嘗試再獲取一個block,循環(huán)判斷是否有空閑block沪停,以及block是否恰好需要被回收煤辨。
buf_LRU_scan_and_free_block
如果是用了壓縮頁(use_unzip_list為true),則調用buf_LRU_free_from_unzip_LRU_list從unzip_LRU_list中淘汰空閑block木张。這一步只釋放壓縮頁的解壓頁众辨,壓縮頁本身并不需要釋放。
如果上一步?jīng)]有成功釋放空閑block舷礼,則再調用buf_LRU_free_from_common_LRU_list從lru_list中淘汰空閑block鹃彻。
如果前兩步驟沒有成功釋放空閑block,則釋放LRU_list_mutex鎖妻献,否則退出時不釋放buf_pool->LRU_list_mutex鎖蛛株。
buf_LRU_free_from_unzip_LRU_list
調用buf_LRU_evict_from_unzip_LRU判斷是需要從unzip_LRU_list中淘汰頁面。如果unzip_LRU_list是空或者unzip_LRU_list的空間小于LRU_list空間的10%育拨,則拒絕從unzip_LRU_list淘汰谨履。此外,通過公式判斷當前業(yè)務類型至朗,如果是IO bound類型屉符,則只淘汰解壓頁,不淘汰壓縮頁;CPU bound類型則連壓縮頁也淘汰矗钟。
使用for循環(huán)從尾到頭掃描(或者最多掃描srv_LRU_scan_depth個頁面)unzip_LRU_list唆香,逐一調用buf_LRU_free_page嘗試釋放頁面,釋放解壓的壓縮頁不會釋放壓縮頁原數(shù)據(jù)吨艇,最后返回釋放狀態(tài)freed躬它。成功釋放為true。
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组贺;再調用buf_LRU_free_page判斷頁面是否能淘汰成功。此輪淘汰將淘汰壓縮頁原數(shù)據(jù)祖娘。
buf_LRU_free_page
嘗試從LRU list中釋放一個頁面失尖,中間有臨時釋放鎖再加鎖的邏輯,實現(xiàn)比較復雜渐苏。
先調用buf_page_can_relocate判斷頁面是否正在被讀取掀潮,判斷方法是查看頁面是否正在被其他頁面讀取,如果在被讀取則直接返回釋放失敗琼富。
查看當前頁面是否為被修改過的壓縮頁面仪吧,是的話頁返回失敗。
調用buf_LRU_block_remove_hashed將頁面從page_hash表中刪除鞠眉。
如果是壓縮頁面的解壓頁薯鼠,則再將壓縮頁面插入LRU_list(如果前一個頁面不為空,則調用UT_LIST_INSERT_AFTER插入械蹋,如果前一個頁面為空人断,則調用buf_LRU_add_block_low插入midpoint或者young_list頭部),page_hash(使用HASH_INSERT)朝蜘,如果有必要的話還需要插入flush_list恶迈。
如果不是BUF_BLOCK_ZIP_PAGE類型,說明是臟頁(BUF_BLOCK_ZIP_DIRTY)谱醇,使用buf_flush_relocate_on_flush_list插入放入flush_list合適的位置暇仲。
檢查頁面是否有自適應hash(adaptive hash index),有的話則刪除副渴,對應函數(shù)為btr_search_drop_page_hash_index奈附。
調用buf_LRU_block_free_hashed_page將釋放成功的,不再被page_hash索引的頁面煮剧,放回free_list上斥滤。
buf_LRU_free_page有一個參數(shù)配置是否釋放解壓頁的原壓縮數(shù)據(jù)将鸵。buf_LRU_free_from_unzip_LRU_list中這個參數(shù)是false,即默認不刪除壓縮頁的原數(shù)據(jù)佑颇。buf_LRU_free_from_common_LRU_list中這個參數(shù)是true顶掉,即將壓縮頁原數(shù)據(jù)刪除。
buf_LRU_add_block_low
如果old參數(shù)為false挑胸,或者LRU_list的總長度小于BUF_LRU_OLD_MIN_LEN痒筒,將頁面插入young_list的開頭。否則插入old_list的頭部之后茬贵。
調整 LRU_list長度簿透,old_list頭部位置等。如果LRU_list現(xiàn)在長于BUF_LRU_OLD_MIN_LEN解藻,則將其初始化為young_list和old_list兩部分老充。
用buf_page_belongs_to_unzip_LRU判斷:如果當前頁面是一個解壓縮頁面,則也同時將此頁面插入unzip_LRU_list(buf_unzip_LRU_add_block)螟左。
buf_flush_single_page_from_LRU
用來淘汰一個頁面蚂维,將其刷盤。并將頁面從page_hash和LRU_list中刪除路狮,放入free_list。
加LRU_list鎖進入for循環(huán)
從尾巴逐一先用buf_flush_ready_for_replace判斷頁面當前頁面是否可以直接釋放(檢查oldest_modification==0蔚约,buf_fix_count==0奄妨,處于BUF_IO_NONE狀態(tài)),是則調用buf_LRU_free_page將頁面釋放苹祟。
上一步驟沒有成功砸抛,則調用buf_flush_ready_for_flush來將一個可以被刷盤的頁面刷入磁盤(檢查oldest_modification是否不等于0,頁面是否在被讀取树枫,刷盤模式是否正確等)直焙。是則調用buf_flush_page將頁面單獨同步刷盤。
上述兩個步驟成功一個即可退出循環(huán)砂轻,釋放鎖奔誓,返回刷盤是否成功。
buf_relocate
將壓縮頁的bpage從LRU中刪除搔涝,重新分配block厨喂,把bpage中的內容拷貝到block->page中,把dpage (block->page)插入bpage的位置庄呈。
注意:
目前騰訊云數(shù)據(jù)庫?MySQL 8.0特惠活動蜕煌,不限新老用戶,最低5折起