溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

MySQL性能優(yōu)化InnoDB buffer pool flush分析

發(fā)布時(shí)間:2021-11-10 11:38:45 來(lái)源:億速云 閱讀:236 作者:iii 欄目:MySQL數(shù)據(jù)庫(kù)

這篇文章主要講解了“MySQL性能優(yōu)化InnoDB buffer pool flush分析”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“MySQL性能優(yōu)化InnoDB buffer pool flush分析”吧!

背景

我們知道InnoDB使用buffer pool來(lái)緩存從磁盤讀取到內(nèi)存的數(shù)據(jù)頁(yè)。buffer pool通常由數(shù)個(gè)內(nèi)存塊加上一組控制結(jié)構(gòu)體對(duì)象組成。內(nèi)存塊的個(gè)數(shù)取決于buffer pool instance的個(gè)數(shù),不過(guò)在5.7版本中開(kāi)始默認(rèn)以128M(可配置)的chunk單位分配內(nèi)存塊,這樣做的目的是為了支持buffer pool的在線動(dòng)態(tài)調(diào)整大小。

Buffer pool的每個(gè)內(nèi)存塊通過(guò)mmap的方式分配內(nèi)存,因此你會(huì)發(fā)現(xiàn),在實(shí)例啟動(dòng)時(shí)虛存很高,而物理內(nèi)存很低。這些大片的內(nèi)存塊又按照16KB劃分為多個(gè)frame,用于存儲(chǔ)數(shù)據(jù)頁(yè)。

雖然大多數(shù)情況下buffer pool是以16KB來(lái)存儲(chǔ)數(shù)據(jù)頁(yè),但有一種例外:使用壓縮表時(shí),需要在內(nèi)存中同時(shí)存儲(chǔ)壓縮頁(yè)和解壓頁(yè),對(duì)于壓縮頁(yè),使用Binary buddy allocator算法來(lái)分配內(nèi)存空間。例如我們讀入一個(gè)8KB的壓縮頁(yè),就從buffer pool中取一個(gè)16KB的block,取其中8KB,剩下的8KB放到空閑鏈表上;如果緊跟著另外一個(gè)4KB的壓縮頁(yè)讀入內(nèi)存,就可以從這8KB中分裂4KB,同時(shí)將剩下的4KB放到空閑鏈表上。

為了管理buffer pool,每個(gè)buffer pool instance 使用如下幾個(gè)鏈表來(lái)管理:

  • LRU鏈表包含所有讀入內(nèi)存的數(shù)據(jù)頁(yè);

  • Flush_list包含被修改過(guò)的臟頁(yè);

  • unzip_LRU包含所有解壓頁(yè);

  • Free list上存放當(dāng)前空閑的block。


另外為了避免查詢數(shù)據(jù)頁(yè)時(shí)掃描LRU,還為每個(gè)buffer pool instance維護(hù)了一個(gè)page hash,通過(guò)space id 和page no可以直接找到對(duì)應(yīng)的page。

一般情況下,當(dāng)我們需要讀入一個(gè)Page時(shí),首先根據(jù)space id 和page no找到對(duì)應(yīng)的buffer pool instance。然后查詢page hash,如果page hash中沒(méi)有,則表示需要從磁盤讀取。在讀盤前首先我們需要為即將讀入內(nèi)存的數(shù)據(jù)頁(yè)分配一個(gè)空閑的block。當(dāng)free list上存在空閑的block時(shí),可以直接從free list上摘取;如果沒(méi)有,就需要從unzip_lru 或者 lru上驅(qū)逐page。

這里需要遵循一定的原則(參考函數(shù)buf_LRU_scan_and_free_block , 5.7.5):

  1. 首先嘗試從unzip_lru上驅(qū)逐解壓頁(yè);

  2. 如果沒(méi)有,再嘗試從Lru鏈表上驅(qū)逐Page;

  3. 如果還是無(wú)法從Lru上獲取到空閑block,用戶線程就會(huì)參與刷臟,嘗試做一次SINGLE PAGE FLUSH,單獨(dú)從Lru上刷掉一個(gè)臟頁(yè),然后再重試。

Buffer pool中的page被修改后,不是立刻寫入磁盤,而是由后臺(tái)線程定時(shí)寫入,和大多數(shù)數(shù)據(jù)庫(kù)系統(tǒng)一樣,臟頁(yè)的寫盤遵循日志先行WAL原則,因此在每個(gè)block上都記錄了一個(gè)最近被修改時(shí)的Lsn,寫數(shù)據(jù)頁(yè)時(shí)需要確保當(dāng)前寫入日志文件的redo不低于這個(gè)Lsn。

然而基于WAL原則的刷臟策略可能帶來(lái)一個(gè)問(wèn)題:當(dāng)數(shù)據(jù)庫(kù)的寫入負(fù)載過(guò)高時(shí),產(chǎn)生redo log的速度極快,redo log可能很快到達(dá)同步checkpoint點(diǎn)。這時(shí)候需要進(jìn)行刷臟來(lái)推進(jìn)Lsn。由于這種行為是由用戶線程在檢查到redo log空間不夠時(shí)觸發(fā),大量用戶線程將可能陷入到這段低效的邏輯中,產(chǎn)生一個(gè)明顯的性能拐點(diǎn)。


Page Cleaner線程

在MySQL5.6中,開(kāi)啟了一個(gè)獨(dú)立的page cleaner線程來(lái)進(jìn)行刷lru list 和flush list。默認(rèn)每隔一秒運(yùn)行一次,5.6版本里提供了一大堆的參數(shù)來(lái)控制page cleaner的flush行為,包括:

innodb_adaptive_flushing_lwm, 
innodb_max_dirty_pages_pct_lwm
innodb_flushing_avg_loops
innodb_io_capacity_max
innodb_lru_scan_depth

這里我們不一一介紹,總的來(lái)說(shuō),如果你發(fā)現(xiàn)redo log推進(jìn)的非???,為了避免用戶線程陷入刷臟,可以通過(guò)調(diào)大innodb_io_capacity_max來(lái)解決,該參數(shù)限制了每秒刷新的臟頁(yè)上限,調(diào)大該值可以增加Page cleaner線程每秒的工作量。如果你發(fā)現(xiàn)你的系統(tǒng)中free list不足,總是需要驅(qū)逐臟頁(yè)來(lái)獲取空閑的block時(shí),可以適當(dāng)調(diào)大innodb_lru_scan_depth 。該參數(shù)表示從每個(gè)buffer pool instance的lru上掃描的深度,調(diào)大該值有助于多釋放些空閑頁(yè),避免用戶線程去做single page flush。

為了提升擴(kuò)展性和刷臟效率,在5.7.4版本里引入了多個(gè)page cleaner線程,從而達(dá)到并行刷臟的效果。目前Page cleaner并未和buffer pool綁定,其模型為一個(gè)協(xié)調(diào)線程 + 多個(gè)工作線程,協(xié)調(diào)線程本身也是工作線程。因此如果innodb_page_cleaners設(shè)置為4,那么就是一個(gè)協(xié)調(diào)線程,加3個(gè)工作線程,工作方式為生產(chǎn)者-消費(fèi)者。工作隊(duì)列長(zhǎng)度為buffer pool instance的個(gè)數(shù),使用一個(gè)全局slot數(shù)組表示。

協(xié)調(diào)線程在決定了需要flush的page數(shù)和lsn_limit后,會(huì)設(shè)置slot數(shù)組,將其中每個(gè)slot的狀態(tài)設(shè)置為PAGE_CLEANER_STATE_REQUESTED, 并設(shè)置目標(biāo)page數(shù)及l(fā)sn_limit,然后喚醒工作線程 (pc_request)

工作線程被喚醒后,從slot數(shù)組中取一個(gè)未被占用的slot,修改其狀態(tài),表示已被調(diào)度,然后對(duì)該slot所對(duì)應(yīng)的buffer pool instance進(jìn)行操作。直到所有的slot都被消費(fèi)完后,才進(jìn)入下一輪。通過(guò)這種方式,多個(gè)page cleaner線程實(shí)現(xiàn)了并發(fā)flush buffer pool,從而提升flush dirty page/lru的效率。


MySQL5.7的InnoDB flush策略優(yōu)化

在之前版本中,因?yàn)榭赡芡瑫r(shí)有多個(gè)線程操作buffer pool刷page (在刷臟時(shí)會(huì)釋放buffer pool mutex),每次刷完一個(gè)page后需要回溯到鏈表尾部,使得掃描bp鏈表的時(shí)間復(fù)雜度最差為O(N*N)。

在5.6版本中針對(duì)Flush list的掃描做了一定的修復(fù),使用一個(gè)指針來(lái)記錄當(dāng)前正在flush的page,待flush操作完成后,再看一下這個(gè)指針有沒(méi)有被別的線程修改掉,如果被修改了,就回溯到鏈表尾部,否則無(wú)需回溯。但這個(gè)修復(fù)并不完整,在最差的情況下,時(shí)間復(fù)雜度依舊不理想。

因此在5.7版本中對(duì)這個(gè)問(wèn)題進(jìn)行了徹底的修復(fù),使用多個(gè)名為hazard pointer的指針,在需要掃描LIST時(shí),存儲(chǔ)下一個(gè)即將掃描的目標(biāo)page,根據(jù)不同的目的分為幾類:

  • flush_hp: 用作批量刷FLUSH LIST

  • lru_hp: 用作批量刷LRU LIST

  • lru_scan_itr: 用于從LRU鏈表上驅(qū)逐一個(gè)可替換的page,總是從上一次掃描結(jié)束的位置開(kāi)始,而不是LRU尾部

  • single_scan_itr: 當(dāng)buffer pool中沒(méi)有空閑block時(shí),用戶線程會(huì)從FLUSH LIST上單獨(dú)驅(qū)逐一個(gè)可替換的page 或者 flush一個(gè)臟頁(yè),總是從上一次掃描結(jié)束的位置開(kāi)始,而不是LRU尾部。

后兩類的hp都是由用戶線程在嘗試獲取空閑block時(shí)調(diào)用,只有在推進(jìn)到某個(gè)buf_page_t::old被設(shè)置成true的page (大約從Lru鏈表尾部起至總長(zhǎng)度的八分之三位置的page)時(shí), 再將指針重置到Lru尾部。

這些指針在初始化buffer pool時(shí)分配,每個(gè)buffer pool instance都擁有自己的hp指針。當(dāng)某個(gè)線程對(duì)buffer pool中的page進(jìn)行操作時(shí),例如需要從LRU中移除Page時(shí),如果當(dāng)前的page被設(shè)置為hp,就要將hp更新為當(dāng)前Page的前一個(gè)page。當(dāng)完成當(dāng)前page的flush操作后,直接使用hp中存儲(chǔ)的page指針進(jìn)行下一輪flush。


社區(qū)優(yōu)化

一如既往的,Percona Server在5.6版本中針對(duì)buffer pool flush做了不少的優(yōu)化,主要的修改包括如下幾點(diǎn):

  • 優(yōu)化刷LRU流程buf_flush_LRU_tail
    該函數(shù)由page cleaner線程調(diào)用。

    • 原生的邏輯:依次flush 每個(gè)buffer pool instance,每次掃描的深度通過(guò)參數(shù)innodb_lru_scan_depth來(lái)配置。而在每個(gè)instance內(nèi),又分成多個(gè)chunk來(lái)調(diào)用;

    • 修改后的邏輯為:每次flush一個(gè)buffer pool的LRU時(shí),只刷一個(gè)chunk,然后再下一個(gè)instance,刷完所有instnace后,再回到前面再刷一個(gè)chunk。簡(jiǎn)而言之,把集中的flush操作進(jìn)行了分散,其目的是分散壓力,避免對(duì)某個(gè)instance的集中操作,給予其他線程更多訪問(wèn)buffer pool的機(jī)會(huì)。

  • 允許設(shè)定刷LRU/FLUSH LIST的超時(shí)時(shí)間,防止flush操作時(shí)間過(guò)長(zhǎng)導(dǎo)致別的線程(例如嘗試做single page flush的用戶線程)stall??;當(dāng)?shù)竭_(dá)超時(shí)時(shí)間時(shí),page cleaner線程退出flush。

  • 避免用戶線程參與刷buffer pool
    當(dāng)用戶線程參與刷buffer pool時(shí),由于線程數(shù)的不可控,將產(chǎn)生嚴(yán)重的競(jìng)爭(zhēng)開(kāi)銷,例如free list不足時(shí)做single page flush,以及在redo空間不足時(shí),做dirty page flush,都會(huì)嚴(yán)重影響性能。Percona Server允許選擇讓page cleaner線程來(lái)做這些工作,用戶線程只需要等待即可。出于效率考慮,用戶還可以設(shè)置page cleaner線程的cpu調(diào)度優(yōu)先級(jí)。
    另外在Page cleaner線程經(jīng)過(guò)優(yōu)化后,可以知道系統(tǒng)當(dāng)前處于同步刷新?tīng)顟B(tài),可以去做更激烈的刷臟(furious flush),用戶線程參與到其中,可能只會(huì)起到反作用。

  • 允許設(shè)置page cleaner線程,purge線程,io線程,master線程的CPU調(diào)度優(yōu)先級(jí),并優(yōu)先獲得InnoDB的mutex。

    • 使用新的獨(dú)立后臺(tái)線程來(lái)刷buffer pool的LRU鏈表,將這部分工作負(fù)擔(dān)從page cleaner線程剝離。
      實(shí)際上就是直接轉(zhuǎn)移刷LRU的代碼到獨(dú)立線程了。從之前Percona的版本來(lái)看,都是在不斷的強(qiáng)化后臺(tái)線程,讓用戶線程少參與到刷臟/checkpoint這類耗時(shí)操作中。

感謝各位的閱讀,以上就是“MySQL性能優(yōu)化InnoDB buffer pool flush分析”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)MySQL性能優(yōu)化InnoDB buffer pool flush分析這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI