天天看點

《DBNotes:Buffer Pool刷髒頁細節以及改進》

本筆記知識沿用之前DBNotes: Buffer Pool對于緩沖頁的連結清單式管理的部分知識

目錄

  • ​​擷取一個空閑頁的源碼邏輯​​
  • ​​Page_Cleaner_Thread​​
  • ​​LRU_Manager_Thread​​
  • ​​Hazard Pointer作為驅逐算法改進​​
  • ​​參考​​

擷取一個空閑頁的源碼邏輯

任何一個讀寫請求都需要從Buffer pool來擷取所需頁面。如果需要的頁面已經存在于Buffer pool,那麼直接利用目前頁面進行操作就行。但是如果所需頁面不在Buffer pool,比如UPDATE操作,那麼就需要從Buffer pool中新申請空閑頁面,将需要讀取的資料放到Buffer pool中進行操作。

如何從buffer pool中擷取一個頁面呢?這依賴于buf_LRU_get_free_block函數,該函數會循環嘗試去淘汰LRU list上的頁面。每次循環都會通路freelist,檢視是否有足夠的空閑頁面,如果沒有,就繼續從LRUlist去淘汰。這樣的循環在負載較高的時候會加劇對freelist以及LRUlist的mutex的競争。可以設定buf_pool->try_LRU_scan是做了一個優化,如果目前使用者線程掃描的時候 發現沒有空閑頁面,那麼其他使用者線程就不需要進行同樣的掃描。

MySQL的free頁面的擷取依賴于Page_Cleaner_Thread的重新整理能力,如果重新整理不及時,那麼系統就會使用上面所說的循環邏輯來為使用者線程申請空閑頁面,可以看出是十分耗時間的。而如果重新整理過快,也會導緻性能問題,因為重新整理是需要io操作的。

是以引入獨立的線程負責LRU list的刷髒。目的是為了讓獨立線程根據系統負載動态調整LRU的刷髒能力。由于LRU list的刷髒從page cleaner線程中脫離出來,調整LRU list的刷髒能力不再會影響到page cleaner。

同時由于單線程LRUlist刷髒存在問題,設計者進行了改進。繼續将LRU list獨立于page cleaner threads并将LRU list單線程刷髒增加為多線程刷髒。page cleaner隻負責flush list的刷髒,lru_manager_thread隻負責LRU List刷髒。這樣的分離,可以使得LRU list刷髒和Flush List刷髒并行執行。

Page_Cleaner_Thread

主要負責flushlist的刷髒,避免使用者線程同步刷髒頁。

也是每隔一定時間刷一次髒頁,sleep time是自适應的,依賴于目前的lsn,flushlist中的oldest_modification以及目前的同步刷髒點。

與LRU_Manager_Thread不同,該線程每次執行刷的髒頁數量也是自适應的,依賴于目前系統中髒頁的比率,日志産生的速度以及幾個參數。

LRU_Manager_Thread

一個系統線程,随着InnoDB啟動而work,作用是定期清理出空閑的資料頁(數量為innodb_LRU_scan_depth)并加入到Freelist中,防止使用者線程去做同步刷髒影響效率。

該線程每隔一段時間就去FLUSH。先嘗試從LRU中驅逐部分資料頁,如果數量不夠就從Flushlist中驅逐。

線程執行頻率是自适應的:

設定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances。

如果Freelist中的數量小于max_free_len 的1%,則sleep time = 0,表示這時候空閑頁太少了,需要一直執行buf_flush_LRU_tail操作,進而騰出空閑的資料頁。

如果Free List中的數量介于max_free_len的1%-5%,則sleep time減少50ms(預設為1000ms),如果Free List中的數量介于max_free_len的5%-20%,則sleep time不變,如果Free List中的數量大于max_free_len的20%,則sleep time增加50ms,但是最大值不超過​

​rds_cleaner_max_lru_time​

​。

Hazard Pointer作為驅逐算法改進

在學術上,Hazard Pointer是一個指針,如果這個指針被一個線程所占有,在它釋放之前,其他線程不能對他進行修改,但是在InnoDB裡面,概念剛好相反,一個線程可以随時通路Hazard Pointer,但是在通路後,他需要調整指針到一個有效的值,便于其他線程使用。我們用Hazard Pointer來加速逆向的邏輯連結清單周遊。 先來說一下這個問題的背景,我們知道InnoDB中可能有多個線程同時作用在Flush List上進行刷髒,例如LRU_Manager_Thread和Page_Cleaner_Thread。同時,為了減少鎖占用的時間,InnoDB在進行寫盤的時候都會把之前占用的鎖給釋放掉。這兩個因素疊加在一起導緻同一個刷髒線程刷完一個資料頁A,就需要回到Flush List末尾(因為A之前的髒頁可能被其他線程給刷走了,之前的髒頁可能已經不在Flush list中了),重新掃描新的可刷盤的髒頁。另一方面,資料頁刷盤是異步操作,在刷盤的過程中,我們會把對應的資料頁IO_FIX住,防止其他線程對這個資料頁進行操作。我們假設某台機器使用了非常緩慢的機械硬碟,目前Flush List中所有頁面都可以被刷盤(​

​buf_flush_ready_for_replace​

​傳回true)。我們的某一個刷髒線程拿到隊尾最後一個資料頁,IO fixed,發送給IO線程,最後再從隊尾掃描尋找可刷盤的髒頁。在這次掃描中,它發現最後一個資料頁(也就是剛剛發送到IO線程中的資料頁)狀态為IO fixed(磁盤很慢,還沒處理完)是以不能刷,跳過,開始刷倒數第二個資料頁,同樣IO fixed,發送給IO線程,然後再次重新掃描Flush List。它又發現尾部的兩個資料頁都不能重新整理(因為磁盤很慢,可能還沒刷完),直到掃描到倒數第三個資料頁。是以,存在一種極端的情況,如果磁盤比較緩慢,刷髒算法性能會從O(N)退化成O(N*N)。 要解決這個問題,最本質的方法就是當刷完一個髒頁的時候不要每次都從隊尾重新掃描。我們可以使用Hazard Pointer來解決,方法如下:周遊找到一個可刷盤的資料頁,在鎖釋放之前,調整Hazard Pointer使之指向Flush List中下一個節點,注意一定要在持有鎖的情況下修改。然後釋放鎖,進行刷盤,刷完盤後,重新擷取鎖,讀取Hazard Pointer并設定下一個節點,然後釋放鎖,進行刷盤,如此重複。當這個線程在刷盤的時候,另外一個線程需要刷盤,也是通過Hazard Pointer來擷取可靠的節點,并重置下一個有效的節點。通過這種機制,保證每次讀到的Hazard Pointer是一個有效的Flush List節點,即使磁盤再慢,刷髒算法效率依然是O(N)。 這個解法同樣可以用到LRU List驅逐算法上,提高驅逐的效率。

參考

​​MySQL · 源碼分析 · Innodb緩沖池刷髒的多線程實作​​​​​​

​​MySQL · 源碼分析 · InnoDB LRU List刷髒改進之路​​

繼續閱讀