今天填一下之前的坑,盤一盤 mysql 相關的 buffer。
我們來看一下官網的一張圖:
這張圖畫的是 mysql innodb 的架構,從圖中可以看到有很多 buffer,這篇我們就一個一個盤過去。
發車?。ㄎ臋n基于mysql8.0,以下描述的存儲引擎基于 mysql innodb)
buffer pool
首先,我們來看下 buffer pool。
其實 buffer pool 就是內存中的一塊緩沖池,用來緩存表和索引的數據。
我們都知道 mysql 的數據最終是存儲在磁盤上的,但是如果讀存數據都直接跟磁盤打交道的話,這速度就有點慢了。
所以 innodb 自己維護了一個 buffer pool,在讀取數據的時候,會把數據加載到緩沖池中,這樣下次再獲取就不需要從磁盤讀了,直接訪問內存中的 buffer pool 即可。
包括修改也是一樣,直接修改內存中的數據,然后到一定時機才會將這些臟數據刷到磁盤上。
看到這肯定有小伙伴有疑惑:直接就在內存中修改數據,假設服務器突然宕機了,這個修改不就丟了?
別怕,有個 redolog 的存在,它會持久化這些修改,恢復時可以讀取 redolog 來還原數據,這個我們后面的文章再詳盤,今天的主角是 buffer 哈。
回到 buffer pool,其實緩沖池維護的是頁數據,也就是說,即使你只想從磁盤中獲取一條數據,但是 innodb 也會加載一頁的數據到緩沖池中,一頁默認是 16k。
當然,緩沖池的大小是有限的。按照 mysql 官網所說,在專用服務器上,通常會分配給緩沖池高達 80% 的物理內存,不管分配多少,反正內存大小正常來說肯定不會比磁盤大。
也就是說內存放不下全部的數據庫數據,那說明緩沖池需要有淘汰機制,淘汰那些不常被訪問的數據頁。
按照這個需求,我們很容易想到 LRU 機制,最近最少使用的頁面將被淘汰,即維護一個鏈表,被訪問的頁面移動到頭部,新加的頁面也加到頭部,同時根據內存使用情況淘汰尾部的頁面。
通過這樣一個機制來維持內存且盡量讓最近訪問的數據留在內存中。
看起來這個想法不錯,但 innodb 的實現并不是樸素的 LRU,而是一種變型的 LRU。
從圖中我們可以看出 buffer pool 分為了老年代(old sublist)和新生代(new sublist)。
老年代默認占 3/8,當然,可以通過 innodb_old_blocks_pct 參數來調整比例。
當有新頁面加入 buffer pool 時,插入的位置是老年代的頭部,同時新頁面在 1s 內再次被訪問的話,不會移到新生代,等 1s 后,如果該頁面再次被訪問才會被移動到新生代。
這和我們正常了解的 LRU 不太一樣,正常了解的 LRU 實現是新頁面插入到頭部,且老頁面只要被訪問到就會被移動到頭部,這樣保證最近訪問的數據都留存在頭部,淘汰的只會是尾部的數據。
那為什么要實現這樣改造的 LRU 呢?
innodb 有預讀機制,簡單理解就是讀取連續的多個頁面后,innodb 認為后面的數據也會被讀取,于是異步將這些數據載入 buffer pool 中,但是這只是一個預判,也就是說預讀的頁面不一定會被訪問。所以如果直接將新頁面都加到新生代,可能會污染熱點數據,但是如果新頁面是加到老年代頭部,就沒有這個問題。
同時大量數據的訪問,例如不帶 where 條件的 select 或者 mysqldump 的操作等,都會導致同等數量的數據頁被淘汰,如果簡單加到新生代的話,可能會一次性把大量熱點數據淘汰了,所以新頁面加到老年代頭部就沒這個問題。
那 1s 機制是為了什么呢?
這個機制其實也是為了處理大量數據訪問的情況,因為基本上大數據掃描之后,可能立馬又再次訪問,正常這時候需要把頁移到新生代了,但等這波操作結束了,后面還有可能再也沒有請求訪問這些頁面了,但因為這波掃描把熱點數據都淘汰了,這就不太美麗了。
于是乎搞了個時間窗口,新頁面在 1s 內的訪問,并不會將其移到新生代,這樣就不會淘汰熱點數據了,然后 1s 后如果這個頁面再次被訪問,才會被移到新生代,這次訪問大概率已經是別的業務請求,也說明這個數據確實可能是熱點數據。
經過這兩個改造, innodb 就解決了預讀失效和一次性大批量數據訪問的問題。
至此,對 buffer pool 的了解就差不多了。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
change buffer
從上面的圖我們可以看到, buffer pool 里面其實有一塊內存是留給 change buffer 用的。
那 change buffer 是個啥玩意呢?
還記得我在 buffer pool 寫的一句話嗎:innodb 直接修改內存中的數據,然后到一定時機才會將這些臟數據刷到磁盤上。
也就是修改的時候直接修改的是 buffer pool 中的數據,但這個前提是 buffer pool 中已經存在你要修改的數據。
假設我們就直接執行一條 update table set name = 'yes' where id = 1,如果此時 buffer pool 里沒有 id 為 1 的這條數據,那怎么辦?
難道把這條數據先加載到 buffer pool 中,然后再執行修改嗎?
當然不是,這時候 change buffer 就上場了。
如果當前數據頁不在 buffer pool 中,那么 innodb 會把更新操作緩存到 change buffer 中,當下次訪問到這條數據后,會把數據頁加載到 buffer pool 中,并且應用上 change buffer 里面的變更,這樣就保證了數據的一致性。
所以 change buffer 有什么好處?
當二級索引頁不在 buffer pool 中時,change buffer 可以避免立即從磁盤讀取對應索引頁導致的昂貴的隨機I/O ,對應的更改可以在后面當二級索引頁讀入 buffer pool 時候被批量應用。
看到我加粗的字體沒,二級索引頁,沒錯 change buffer 只能用于二級索引的更改,不適用于主鍵索引,空間索引以及全文索引。
還有,唯一索引也不行,因為唯一索引需要讀取數據然后檢查數據的一致性。
看到這肯定又有小伙伴關心:更改先緩存在 change buffer 中,假如數據庫掛了,更改不是丟了嗎?
別怕,change buffer 也是要落盤存儲的,從上圖我們看到 change buffer 會落盤到系統表空間里面,然后 redo log 也會記錄 chang buffer 的修改來保證數據一致性。
至此,想必你對 change buffer 已經有一定了解了吧。
這玩意主要用來避免于二級索引頁修改產生的隨機I/O,如果你的內存夠大能裝下所有數據,或者二級索引很少,或者你的磁盤是固態的對隨機訪問影響不大,其實可以關閉 change buffer,這玩意也增加了復雜度,當然最終還是得看壓測結果。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
Log Buffer
接下來,我們看看 Log Buffer。
從上面的圖我們可以得知,這玩意就是給 redo log 做緩沖用的。
redo log 我們都知道是重做日志,用來保證崩潰恢復數據的正確性,innodb 寫數據時是先寫日志,再寫磁盤數據,即 WAL (Write-Ahead Logging),把數據的隨機寫入轉換成日志的順序寫。
但,即使是順序寫 log ,每次都調用 write 或者 fsync 也是有開銷的,畢竟也是系統調用,涉及上下文切換。
于是乎,搞了個 Log Buffer 來緩存 redo log 的寫入。
即寫 redo log 先寫到 Log Buffer 中,等一定時機再寫到 redo log 文件里。
我們每次事務可能涉及多個更改,這樣就會產生多條 redo log,這時會都先寫到 Log Buffer 中,等事務提交的時候,一起將這些 redo log 寫到文件里。
或者當 Log Buffer 超過總量的一半(默認總量是 16mb),也會將數據刷到 redo log 文件中。
也有個后臺線程,每隔 1s 就會將 Log Buffer 刷到 redo log 文件中。
從上面這些我們得知,Log Buffer 其實就是一個寫優化操作,把多次 write 優化成一次 write,一次處理多數據,減少系統調用。
看到這肯定有小伙伴說,數據先寫 Log Buffer 而不刷盤,這數據不會丟嗎?
innodb 其實給了個配置,即 innodb_flush_log_at_trx_commit 來控制 redo log 寫盤時機。
當值為 0,提交事務不會刷盤到 redo log,需要等每隔一秒的后臺線程,將 log buffer 寫到操作系統的 cache,并調用 fsync落盤,性能最好,但是可能會丟 1s 數據。
當值為 1,提交事務會將 log buffer 寫到操作系統的 cache,并調用 fsync 落盤,保證數據正確,性能最差,這也是默認配置。
當值為 2,提交事務會將 log buffer 寫到操作系統的 cache,但不調用 fsync,而是等每隔 1s 調用 fsync 落盤,性能折中,如果數據庫掛了沒事,如果服務器宕機了,會丟 1s 數據。
具體如何配置看你的業務了。
至此,想必你應該了解 Log Buffer 是干啥用了的吧。
Doublewrite Buffer
現在我們來看最后一個 buffer,即 Doublewrite Buffer。
這玩意又是啥用呢?
我們都知道 innodb 默認一頁是 16K,而操作系統 Linux 內存頁是 4K,那么一個 innodb 頁對應 4 個系統頁。
所以 innodb 的一頁數據要刷盤等于需要寫四個系統頁,那假設一頁數據落盤的時候,只寫了一個系統頁 就斷電了,那 innodb 一頁數據就壞了,然后就 g 了,恢復不了。
即產生了部分頁面寫問題,因為寫 innodb 的一頁無法保證原子性,所以引入了 Doublewrite Buffer。
其實就是當 innodb 要將數據落盤的時候,先將頁數據拷貝到 Doublewrite Buffer 中,然后 Doublewrite Buffer 再刷盤到 Doublewrite Buffer Files,這時候數據已經落盤了。
然后再將數據頁刷盤到本該到文件上。
從這個步驟我們得知,數據是寫了兩次磁盤,所以這玩意叫 double write。
之所以這樣操作就是先找個地方暫存這次刷盤的完整數據,如果出現斷電這種情況導致的部分頁面寫而損壞原先的完整頁,可以從 Doublewrite Buffer Files 恢復數據。
但雖然是兩次寫,性能的話也不會低太多,因此數據拷貝到 Doublewrite Buffer 是內存拷貝操作,然后寫到 Doublewrite Buffer Files 也是批量寫,且是順序寫盤,所以整體而已,性能損失不會太多。
有了這玩意,在崩潰恢復的時候,如果發現頁損壞,就可以從 Doublewrite Buffer Files 里面找到頁副本,然后恢復即可。
完美。
最后
好了,關于 innodb buffer 們介紹完了,我們來總結一下:
buffer pool,緩存數據頁,避免頻繁地磁盤交互,內部利用定制的 LRU 來淘汰緩存頁,LRU分老年代和新生代。
change buffer,用于二級非唯一索引的新增、修改或刪除操作,當 buffer pool 沒有數據的時候,不需要加載數據頁到緩沖池中,而是通過 change buffer 來記錄此變更,減少隨機 I/O。
log buffer,用于 redo log 的緩沖,減少 I/O,批處理減少系統調用。
doublewrite Buffer,用于部分頁面寫問題,雙寫數據頁,壞頁時可從 doublewrite buffer files 進行頁恢復,保證頁的完整和數據的正確。
-
服務器
+關注
關注
12文章
9253瀏覽量
85745 -
buffer
+關注
關注
2文章
120瀏覽量
30082 -
MySQL
+關注
關注
1文章
819瀏覽量
26649
原文標題:一網打盡總結 Mysql 的所有 Buffer
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論