MySQL · 效能最佳化· InnoDB buffer pool flush策略漫談

來源:互聯網
上載者:User

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):

  1. 首先嘗試從unzip_lru上驅逐解壓頁;
  2. 如果沒有,再嘗試從Lru鏈表上驅逐Page;
  3. 如果還是無法從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_lwminnodb_flushing_avg_loopsinnodb_io_capacity_maxinnodb_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表--BTree基本資料結構

在MySQL的InnoDB儲存引擎中count(*)函數的最佳化

MySQL InnoDB儲存引擎鎖機制實驗

InnoDB儲存引擎的啟動、關閉與恢複

MySQL InnoDB獨立資料表空間的配置

MySQL Server 層和 InnoDB 引擎層 體繫結構圖

InnoDB 死結案例解析

本文永久更新連結地址:

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.