天天看點

MySQL核心月報 2015.02-MySQL · 性能優化· InnoDB buffer pool flush政策漫談

<b>背景</b>

我們知道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空間不夠時觸發,大量使用者線程将可能陷入到這段低效的邏輯中,産生一個明顯的性能拐點。

<b>page cleaner線程</b>

在mysql5.6中,開啟了一個獨立的page cleaner線程來進行刷lru list 和flush list。預設每隔一秒運作一次,5.6版本裡提供了一大堆的參數來控制page cleaner的flush行為,包括:

這裡我們不一一介紹,總的來說,如果你發現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的效率。

<b>mysql5.7的innodb flush政策優化</b>

在之前版本中,因為可能同時有多個線程操作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。

<b>社群優化</b>

一如既往的,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這類耗時操作中。