利用 WAL 技術,資料庫將隨機寫轉換成了順序寫,大大提升了資料庫的效能,由此也帶來了記憶體髒頁的問題。
髒頁會被後臺執行緒自動 flush,也會由於資料頁淘汰而觸發 flush,而刷髒頁的過程由於會佔用資源,可能會讓你的更新和查詢語句的響應時間長一些。
一、flush 髒頁
當記憶體資料頁跟磁碟資料頁內容不一致的時候,我們成這個記憶體頁為「髒頁」;記憶體資料寫入磁碟後,記憶體和磁碟上的資料頁內容就一致了,稱為「乾淨頁」。
InnoDB引擎以頁作為磁碟和記憶體之間互動的基本單位,資料庫 I/O 操作的最小單位是頁。也就是說,在資料庫中,不論讀一行,還是讀多行,都是將這些行所在的頁進行載入。
記錄是按照行來儲存的,但是資料庫的讀取並不以行為單位,否則一次讀取(也就是一次 I/O 操作)只能處理一行資料,效率會非常低。
Buffer Pool 中存的就是一頁一頁的資料,當我們要查詢的資料不在 Buffer Pool 中時,InnoDB 會將記錄所在的頁整個載入到 Buffer Pool 中去。
同樣的,將 Buffer Pool 中的髒頁刷入磁碟時,也是按照頁為單位刷入磁碟的。
1、Free List
你從磁碟中讀取一個資料頁,會先從Free List中找出一個空閒快取頁的描述資訊,然後將你讀出的資料頁中載入進快取頁中。同時將快取頁的描述資訊從Free List中剔除,此外該描述資訊塊還會被維護進LRU連結串列中。
資料頁被載入進Buffer Pool後你就可以對其進行變更操作了。
3、Flush List
如果我們修改了Buffer Pool中某個緩衝頁的資料,那麼它就與磁碟上的頁不一致了,這樣的緩衝頁也被稱之為髒頁(dirty page)。
為了效能問題,我們每次修改緩衝頁後,並不著急立刻把修改重新整理到磁碟上,而是在未來的某個時間點進行重新整理操作。
如果有了修復發生,不是立刻重新整理,那之後再重新整理的時,我們怎麼知道Buffer Pool中哪些頁是髒頁,哪些頁從來沒有被修改過呢?
建立一個儲存髒頁的 Flush list,凡是被修改過的緩衝頁對應的控制塊都會作為節點加入到這個連結串列中。
4、LRU List
除了以上,Buffer Pool還有另外一種LRU List,整體結構如下:
在BufferPool中,記憶體管理如下:
- 需要找 free 空閒資料塊:free list
- 需要找冷熱存取的資料塊:lru list
- 需要知道哪些資料塊是髒的:flush list
二、重新整理方式有哪幾種
1、從flush連結串列中重新整理一部分頁面到磁碟
後臺執行緒會根據當時系統的繁忙程度確定重新整理速率,定時從flush連結串列中重新整理一部分頁面到磁碟,即:BUF_FLUSH_LIST
有時後臺執行緒重新整理髒頁的進度比較慢,導致使用者準備載入一個磁碟頁到Buffer Pool中時沒有可用的緩衝頁。此時,就會嘗試檢視LRU連結串列尾部,看是否存在可以直接釋放掉的未修改緩衝頁。
如果沒有,則不得不將LRU連結串列尾部的一個髒頁同步重新整理到磁碟(與磁碟互動是很慢的,這會降低處理使用者請求的速度),即:BUF_FLUSH_SINGLE_PAGE
2、從LRU連結串列的冷資料中重新整理一部分頁面到磁碟,即:BUF_FLUSH_LRU
後臺執行緒會定時從LRU連結串列的尾部開始掃描一些頁面,掃描的頁面數量可以通過系統變數innodb_lru_scan_depth來指定,如果在LRU連結串列中發現髒頁,則把它們重新整理到磁碟。
控制塊裡會儲存該緩衝頁是否被修改的資訊,所以在掃描LRU連結串列時,可以很輕鬆地獲取到某個緩衝頁是否是髒頁的資訊。
三、flush效能問題
flush髒頁雖然是常態,但是出現以下這兩種情況,都是會明顯影響效能的:
- 一個查詢要淘汰的髒頁個數太多,會導致查詢的響應時間明顯變長;
- 紀錄檔寫滿,更新全部堵住,寫效能跌為 0,這種情況對敏感業務來說,是不能接受的。
InnoDB 會在後臺刷髒頁,而刷髒頁的過程是要將記憶體頁寫入磁碟。所以,無論是你的查詢語句在需要記憶體的時候可能要求淘汰一個髒頁,還是由於刷髒頁的邏輯會佔用 IO 資源並可能影響到了你的更新語句,都可能是造成你從業務端感知到 MySQL「抖」了一下的原因。
要儘量避免這種情況,你就要合理地設定 innodb_io_capacity 的值,並且平時要多關注髒頁比例,不要讓它經常接近 75%。
一旦一個查詢請求需要在執行過程中先 flush 掉一個髒頁時,這個查詢就可能要比平時慢了。
而 MySQL 中的一個機制,可能讓你的查詢會更慢:在準備刷一個髒頁的時候,如果這個資料頁旁邊的資料頁剛好是髒頁,就會把這個「鄰居」也帶著一起刷掉;而且這個把「鄰居」拖下水的邏輯還可以繼續蔓延,也就是對於每個鄰居資料頁,如果跟它相鄰的資料頁也還是髒頁的話,也會被放到一起刷。
在 InnoDB 中,innodb_flush_neighbors 引數就是用來控制這個行為的,值為 1 的時候會有上述的「連坐」機制,值為 0 時表示不找鄰居,自己刷自己的。
找「鄰居」這個優化在機械硬碟時代是很有意義的,可以減少很多隨機 IO。機械硬碟的隨機 IOPS 一般只有幾百,相同的邏輯操作減少隨機 IO 就意味著系統效能的大幅度提升。
而如果使用的是 SSD 這類 IOPS 比較高的裝置的話,建議你把 innodb_flush_neighbors 的值設定成 0。因為這時候 IOPS 往往不是瓶頸,而「只刷自己」,就能更快地執行完必要的刷髒頁操作,減少 SQL 語句響應時間。
在 MySQL 8.0 中,innodb_flush_neighbors 引數的預設值已經是 0 了。
參考資料:
https://hackmysql.com/post/book-6/
《MySQL實戰45講》