本筆記知識沿用之前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刷臟改進之路
MySQL · 引擎特性 · InnoDB Buffer Pool