目錄標題
- 為什么要有Buffer Pool
- buffer pool有多大
- buffer pool緩存什么
- 如何管理Buffer Pool
- 如何管理空閑頁
- 如何管理臟頁
- 如何提高緩存命中率
- 預讀失效
- buffer pool污染
- 臟頁什么時候會被刷入到磁盤
為什么要有Buffer Pool
雖然說MySQL的數據是存儲在磁盤中,但是也不能每次都從磁盤里面讀取數據,這樣性能是極差的。所以InnoDB存儲引擎設計了一個緩沖池(Buffer Pool),來提高數據庫的讀寫能力。
有了緩沖池后:
- 當讀取數據時,如果數據存在于 Buffer Pool 中,客戶端就會直接讀取 Buffer Pool 中的數據,否則再去磁盤中讀取。
- 當修改數據時,首先是修改 Buffer Pool 中數據所在的頁,然后將其頁設置為臟頁,最后由后臺線程將臟頁寫入到磁盤。
buffer pool有多大
Buffer Pool 是在 MySQL 啟動的時候,向操作系統申請的一片連續的內存空間,默認配置下 Buffer Pool 只有 128MB
。可以通過調整 innodb_buffer_pool_size
參數來設置 Buffer Pool 的大小,一般建議設置成可用物理內存的 60%~80%。
buffer pool緩存什么
InnoDB會把存儲的數據劃分為若干個頁,以頁作為磁盤和內存交互的基本單位,一個頁默認大小為16KB,MySQL啟動時,InnoDB會為buffer pool申請一篇連續的內存空間,然后按照默認的16KB的大小劃分出一個個的頁,buffer pool中的頁就叫做緩存頁。
Buffer Pool 除了緩存「索引頁」和「數據頁」,還包括了 undo 頁,插入緩存、自適應哈希索引、鎖信息等等。
為了更好的管理這些在 Buffer Pool 中的緩存頁,InnoDB 為每一個緩存頁都創建了一個控制塊,控制塊信息包括「緩存頁的表空間、頁號、緩存頁地址、鏈表節點」等等。
控制塊也是占有內存空間的,它是放在 Buffer Pool 的最前面,接著才是緩存頁,如下圖:
上圖中控制塊和緩存頁之間灰色部分稱為碎片空間。
如何管理Buffer Pool
如何管理空閑頁
為了能夠快速找到空閑的緩存頁,可以使用鏈表結構,將空閑緩存頁的「控制塊」作為鏈表的節點,這個鏈表稱為 Free 鏈表(空閑鏈表)。
Free 鏈表上除了有控制塊,還有一個頭節點,該頭節點包含鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。
Free 鏈表節點是一個一個的控制塊,而每個控制塊包含著對應緩存頁的地址,所以相當于 Free 鏈表節點都對應一個空閑的緩存頁。
有了 Free 鏈表后,每當需要從磁盤中加載一個頁到 Buffer Pool 中時,就從 Free鏈表中取一個空閑的緩存頁,并且把該緩存頁對應的控制塊的信息填上,然后把該緩存頁對應的控制塊從 Free 鏈表中移除。
如何管理臟頁
buffer pool不僅提高讀性能,還要提高寫性能。在更新數據的時候,不需要每次都要寫入磁盤,而是將buffer pool對應的緩存頁標記為臟頁,然后由后臺線程將臟頁寫入到磁盤。
那為了能快速知道哪些緩存頁是臟的,于是就設計出 Flush 鏈表,它跟 Free 鏈表類似的,鏈表的節點也是控制塊,區別在于 Flush 鏈表的元素都是臟頁。
如何提高緩存命中率
使用LRU算法,該算法的思路是,鏈表頭部的節點是最近使用的,而鏈表末尾的節點是最久沒被使用的。那么,當空間不夠了,就淘汰最久沒被使用的節點,從而騰出空間。
簡單的 LRU 算法并沒有被 MySQL 使用,因為簡單的 LRU 算法無法避免下面這兩個問題:
- 預讀失效;
- Buffer Pool 污染;
預讀失效
先來說說 MySQL 的預讀機制。程序是有空間局部性的,靠近當前被訪問數據的數據,在未來很大概率會被訪問到。所以,MySQL 在加載數據頁時,會提前把它相鄰的數據頁一并加載進來,目的是為了減少磁盤 IO。但是可能這些被提前加載進來的數據頁,并沒有被訪問,相當于這個預讀是白做了,這個就是預讀失效。
怎么解決預讀失效而導致緩存命中率降低的問題?
最好就是讓預讀的頁停留在 Buffer Pool 里的時間要盡可能的短,讓真正被訪問的頁才移動到 LRU 鏈表的頭部,從而保證真正被讀取的熱數據留在 Buffer Pool 里的時間盡可能長。MySQL 是這樣做的,它改進了 LRU 算法,將 LRU 劃分了 2 個區域:old 區域 和 young 區域。young 區域在 LRU 鏈表的前半部分,old 區域則是在后半部分,如下圖:
劃分這兩個區域后,預讀的頁就只需要加入到 old 區域的頭部,當頁被真正訪問的時候,才將頁插入 young 區域的頭部。如果預讀的頁一直沒有被訪問,就會從 old 區域移除,這樣就不會影響 young 區域中的熱點數據。
buffer pool污染
當某一個 SQL 語句掃描了大量的數據時,在 Buffer Pool 空間比較有限的情況下,可能會將 Buffer Pool 里的所有頁都替換出去,導致大量熱數據被淘汰了,等這些熱數據又被再次訪問的時候,由于緩存未命中,就會產生大量的磁盤 IO,MySQL 性能就會急劇下降,這個過程被稱為 Buffer Pool 污染。
怎么解決出現 Buffer Pool 污染而導致緩存命中率下降的問題?
MySQL 是這樣做的,進入到 young 區域條件增加了一個停留在 old 區域的時間判斷。具體是這樣做的,在對某個處在 old 區域的緩存頁進行第一次訪問時,就在它對應的控制塊中記錄下來這個訪問時間:
- 如果后續的訪問時間與第一次訪問的時間在某個時間間隔內,那么該緩存頁就不會被從 old 區域移動到 young 區域的頭部;
- 如果后續的訪問時間與第一次訪問的時間不在某個時間間隔內,那么該緩存頁移動到 young 區域的頭部;
這個間隔時間是由 innodb_old_blocks_time
控制的,默認是 1000 ms。也就說,只有同時滿足「被訪問」與「在 old 區域停留時間超過 1 秒」兩個條件,才會被插入到 young 區域頭部,這樣就解決了 Buffer Pool 污染的問題 。另外,MySQL 針對 young 區域其實做了一個優化,為了防止 young 區域節點頻繁移動到頭部。young 區域前面 1/4 被訪問不會移動到鏈表頭部,只有后面的 3/4被訪問了才會。
臟頁什么時候會被刷入到磁盤
- 當 redo log 日志滿了的情況下,會主動觸發臟頁刷新到磁盤;
- Buffer Pool 空間不足時,需要將一部分數據頁淘汰掉,如果淘汰的是臟頁,需要先將臟頁同步到磁盤;
- MySQL 認為空閑時,后臺線程會定期將適量的臟頁刷入到磁盤;
- MySQL 正常關閉之前,會把所有的臟頁刷入到磁盤;