共計 4503 個字符,預計需要花費 12 分鐘才能閱讀完成。
這篇文章主要講解了“MySQL 性能優化 InnoDB buffer pool flush 分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著丸趣 TV 小編的思路慢慢深入,一起來研究和學習“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 分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關知識點的文章,歡迎關注!