您好,登錄后才能下訂單哦!
這篇文章主要講解了“MySQL性能優化InnoDB buffer pool flush分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“MySQL性能優化InnoDB buffer pool flush分析”吧!
背景
我們知道InnoDB使用buffer pool來緩存從磁盤讀取到內存的數據頁。buffer pool通常由數個內存塊加上一組控制結構體對象組成。內存塊的個數取決于buffer pool instance的個數,不過在5.7版本中開始默認以128M(可配置)的chunk單位分配內存塊,這樣做的目的是為了支持buffer pool的在線動態調整大小。
Buffer pool的每個內存塊通過mmap的方式分配內存,因此你會發現,在實例啟動時虛存很高,而物理內存很低。這些大片的內存塊又按照16KB劃分為多個frame,用于存儲數據頁。
雖然大多數情況下buffer pool是以16KB來存儲數據頁,但有一種例外:使用壓縮表時,需要在內存中同時存儲壓縮頁和解壓頁,對于壓縮頁,使用Binary buddy allocator算法來分配內存空間。例如我們讀入一個8KB的壓縮頁,就從buffer pool中取一個16KB的block,取其中8KB,剩下的8KB放到空閑鏈表上;如果緊跟著另外一個4KB的壓縮頁讀入內存,就可以從這8KB中分裂4KB,同時將剩下的4KB放到空閑鏈表上。
為了管理buffer pool,每個buffer pool instance 使用如下幾個鏈表來管理:
LRU鏈表包含所有讀入內存的數據頁;
Flush_list包含被修改過的臟頁;
unzip_LRU包含所有解壓頁;
Free list上存放當前空閑的block。
另外為了避免查詢數據頁時掃描LRU,還為每個buffer pool instance維護了一個page hash,通過space id 和page no可以直接找到對應的page。
一般情況下,當我們需要讀入一個Page時,首先根據space id 和page no找到對應的buffer pool instance。然后查詢page hash,如果page hash中沒有,則表示需要從磁盤讀取。在讀盤前首先我們需要為即將讀入內存的數據頁分配一個空閑的block。當free list上存在空閑的block時,可以直接從free list上摘取;如果沒有,就需要從unzip_lru 或者 lru上驅逐page。
這里需要遵循一定的原則(參考函數buf_LRU_scan_and_free_block , 5.7.5):
首先嘗試從unzip_lru上驅逐解壓頁;
如果沒有,再嘗試從Lru鏈表上驅逐Page;
如果還是無法從Lru上獲取到空閑block,用戶線程就會參與刷臟,嘗試做一次SINGLE PAGE FLUSH,單獨從Lru上刷掉一個臟頁,然后再重試。
Buffer pool中的page被修改后,不是立刻寫入磁盤,而是由后臺線程定時寫入,和大多數數據庫系統一樣,臟頁的寫盤遵循日志先行WAL原則,因此在每個block上都記錄了一個最近被修改時的Lsn,寫數據頁時需要確保當前寫入日志文件的redo不低于這個Lsn。
然而基于WAL原則的刷臟策略可能帶來一個問題:當數據庫的寫入負載過高時,產生redo log的速度極快,redo log可能很快到達同步checkpoint點。這時候需要進行刷臟來推進Lsn。由于這種行為是由用戶線程在檢查到redo log空間不夠時觸發,大量用戶線程將可能陷入到這段低效的邏輯中,產生一個明顯的性能拐點。
Page Cleaner線程
在MySQL5.6中,開啟了一個獨立的page cleaner線程來進行刷lru list 和flush list。默認每隔一秒運行一次,5.6版本里提供了一大堆的參數來控制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
這里我們不一一介紹,總的來說,如果你發現redo log推進的非常快,為了避免用戶線程陷入刷臟,可以通過調大innodb_io_capacity_max來解決,該參數限制了每秒刷新的臟頁上限,調大該值可以增加Page cleaner線程每秒的工作量。如果你發現你的系統中free list不足,總是需要驅逐臟頁來獲取空閑的block時,可以適當調大innodb_lru_scan_depth 。該參數表示從每個buffer pool instance的lru上掃描的深度,調大該值有助于多釋放些空閑頁,避免用戶線程去做single page flush。
為了提升擴展性和刷臟效率,在5.7.4版本里引入了多個page cleaner線程,從而達到并行刷臟的效果。目前Page cleaner并未和buffer pool綁定,其模型為一個協調線程 + 多個工作線程,協調線程本身也是工作線程。因此如果innodb_page_cleaners設置為4,那么就是一個協調線程,加3個工作線程,工作方式為生產者-消費者。工作隊列長度為buffer pool instance的個數,使用一個全局slot數組表示。
協調線程在決定了需要flush的page數和lsn_limit后,會設置slot數組,將其中每個slot的狀態設置為PAGE_CLEANER_STATE_REQUESTED, 并設置目標page數及lsn_limit,然后喚醒工作線程 (pc_request)
工作線程被喚醒后,從slot數組中取一個未被占用的slot,修改其狀態,表示已被調度,然后對該slot所對應的buffer pool instance進行操作。直到所有的slot都被消費完后,才進入下一輪。通過這種方式,多個page cleaner線程實現了并發flush buffer pool,從而提升flush dirty page/lru的效率。
MySQL5.7的InnoDB flush策略優化
在之前版本中,因為可能同時有多個線程操作buffer pool刷page (在刷臟時會釋放buffer pool mutex),每次刷完一個page后需要回溯到鏈表尾部,使得掃描bp鏈表的時間復雜度最差為O(N*N)。
在5.6版本中針對Flush list的掃描做了一定的修復,使用一個指針來記錄當前正在flush的page,待flush操作完成后,再看一下這個指針有沒有被別的線程修改掉,如果被修改了,就回溯到鏈表尾部,否則無需回溯。但這個修復并不完整,在最差的情況下,時間復雜度依舊不理想。
因此在5.7版本中對這個問題進行了徹底的修復,使用多個名為hazard pointer的指針,在需要掃描LIST時,存儲下一個即將掃描的目標page,根據不同的目的分為幾類:
flush_hp: 用作批量刷FLUSH LIST
lru_hp: 用作批量刷LRU LIST
lru_scan_itr: 用于從LRU鏈表上驅逐一個可替換的page,總是從上一次掃描結束的位置開始,而不是LRU尾部
single_scan_itr: 當buffer pool中沒有空閑block時,用戶線程會從FLUSH LIST上單獨驅逐一個可替換的page 或者 flush一個臟頁,總是從上一次掃描結束的位置開始,而不是LRU尾部。
后兩類的hp都是由用戶線程在嘗試獲取空閑block時調用,只有在推進到某個buf_page_t::old被設置成true的page (大約從Lru鏈表尾部起至總長度的八分之三位置的page)時, 再將指針重置到Lru尾部。
這些指針在初始化buffer pool時分配,每個buffer pool instance都擁有自己的hp指針。當某個線程對buffer pool中的page進行操作時,例如需要從LRU中移除Page時,如果當前的page被設置為hp,就要將hp更新為當前Page的前一個page。當完成當前page的flush操作后,直接使用hp中存儲的page指針進行下一輪flush。
社區優化
一如既往的,Percona Server在5.6版本中針對buffer pool flush做了不少的優化,主要的修改包括如下幾點:
優化刷LRU流程buf_flush_LRU_tail
該函數由page cleaner線程調用。
原生的邏輯:依次flush 每個buffer pool instance,每次掃描的深度通過參數innodb_lru_scan_depth來配置。而在每個instance內,又分成多個chunk來調用;
修改后的邏輯為:每次flush一個buffer pool的LRU時,只刷一個chunk,然后再下一個instance,刷完所有instnace后,再回到前面再刷一個chunk。簡而言之,把集中的flush操作進行了分散,其目的是分散壓力,避免對某個instance的集中操作,給予其他線程更多訪問buffer pool的機會。
允許設定刷LRU/FLUSH LIST的超時時間,防止flush操作時間過長導致別的線程(例如嘗試做single page flush的用戶線程)stall住;當到達超時時間時,page cleaner線程退出flush。
避免用戶線程參與刷buffer pool
當用戶線程參與刷buffer pool時,由于線程數的不可控,將產生嚴重的競爭開銷,例如free list不足時做single page flush,以及在redo空間不足時,做dirty page flush,都會嚴重影響性能。Percona Server允許選擇讓page cleaner線程來做這些工作,用戶線程只需要等待即可。出于效率考慮,用戶還可以設置page cleaner線程的cpu調度優先級。
另外在Page cleaner線程經過優化后,可以知道系統當前處于同步刷新狀態,可以去做更激烈的刷臟(furious flush),用戶線程參與到其中,可能只會起到反作用。
允許設置page cleaner線程,purge線程,io線程,master線程的CPU調度優先級,并優先獲得InnoDB的mutex。
使用新的獨立后臺線程來刷buffer pool的LRU鏈表,將這部分工作負擔從page cleaner線程剝離。
實際上就是直接轉移刷LRU的代碼到獨立線程了。從之前Percona的版本來看,都是在不斷的強化后臺線程,讓用戶線程少參與到刷臟/checkpoint這類耗時操作中。
感謝各位的閱讀,以上就是“MySQL性能優化InnoDB buffer pool flush分析”的內容了,經過本文的學習后,相信大家對MySQL性能優化InnoDB buffer pool flush分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。