久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

MySQ的Buffer pool是什么

139次閱讀
沒有評論

共計 18104 個字符,預計需要花費 46 分鐘才能閱讀完成。

這篇文章主要介紹“MySQ 的 Buffer pool 是什么”的相關知識,丸趣 TV 小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“MySQ 的 Buffer pool 是什么”文章能幫助大家解決問題。

緩存的重要性

通過前邊的嘮叨我們知道,對于使用 InnoDB 作為存儲引擎的表來說,不管是用于存儲用戶數據的索引(包括聚簇索引和二級索引),還是各種系統數據,都是以頁的形式存放在表空間中的,而所謂的表空間只不過是 InnoDB 對文件系統上一個或幾個實際文件的抽象,也就是說我們的數據說到底還是存儲在磁盤上的。但是各位也都知道,磁盤的速度慢的跟烏龜一樣,怎么能配得上“快如風,疾如電”的 CPU 呢?所以 InnoDB 存儲引擎在處理客戶端的請求時,當需要訪問某個頁的數據時,就會把完整的頁的數據全部加載到內存中,也就是說即使我們只需要訪問一個頁的一條記錄,那也需要先把整個頁的數據加載到內存中。將整個頁加載到內存中后就可以進行讀寫訪問了,在進行完讀寫訪問之后并不著急把該頁對應的內存空間釋放掉,而是將其緩存起來,這樣將來有請求再次訪問該頁面時,就可以省去磁盤 IO 的開銷了。

InnoDB 的 Buffer Pool 啥是個 Buffer Pool

設計 InnoDB 的大叔為了緩存磁盤中的頁,在 MySQL 服務器啟動的時候就向操作系統申請了一片連續的內存,他們給這片內存起了個名,叫做 Buffer Pool(中文名是緩沖池)。那它有多大呢?這個其實看我們機器的配置,如果你是土豪,你有 512G 內存,你分配個幾百 G 作為 Buffer Pool 也可以啊,當然你要是沒那么有錢,設置小點也行呀~ 默認情況下 Buffer Pool 只有 128M 大小。當然如果你嫌棄這個 128M 太大或者太小,可以在啟動服務器的時候配置 innodb_buffer_pool_size 參數的值,它表示 Buffer Pool 的大小,就像這樣:

[server]
innodb_buffer_pool_size = 268435456

其中,268435456 的單位是字節,也就是我指定 Buffer Pool 的大小為 256M。需要注意的是,Buffer Pool 也不能太小,最小值為 5M(當小于該值時會自動設置成 5M)。

Buffer Pool 內部組成

Buffer Pool 中默認的緩存頁大小和在磁盤上默認的頁大小是一樣的,都是 16KB。為了更好的管理這些在 Buffer Pool 中的緩存頁,設計 InnoDB 的大叔為每一個緩存頁都創建了一些所謂的控制信息,這些控制信息包括該頁所屬的表空間編號、頁號、緩存頁在 Buffer Pool 中的地址、鏈表節點信息、一些鎖信息以及 LSN 信息(鎖和 LSN 我們之后會具體嘮叨,現在可以先忽略),當然還有一些別的控制信息,我們這就不全嘮叨一遍了,挑重要的說嘛~

每個緩存頁對應的控制信息占用的內存大小是相同的,我們就把每個頁對應的控制信息占用的一塊內存稱為一個控制塊吧,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 后邊,所以整個 Buffer Pool 對應的內存空間看起來就是這樣的:

咦?控制塊和緩存頁之間的那個碎片是個什么玩意兒?你想想啊,每一個控制塊都對應一個緩存頁,那在分配足夠多的控制塊和緩存頁后,可能剩余的那點兒空間不夠一對控制塊和緩存頁的大小,自然就用不到嘍,這個用不到的那點兒內存空間就被稱為碎片了。當然,如果你把 Buffer Pool 的大小設置的剛剛好的話,也可能不會產生碎片~

小貼士:每個控制塊大約占用緩存頁大小的 5%,在 MySQL5.7.21 這個版本中,每個控制塊占用的大小是 808 字節。而我們設置的 innodb_buffer_pool_size 并不包含這部分控制塊占用的內存空間大小,也就是說 InnoDB 在為 Buffer Pool 向操作系統申請連續的內存空間時,這片連續的內存空間一般會比 innodb_buffer_pool_size 的值大 5% 左右。

free 鏈表的管理

當我們最初啟動 MySQL 服務器的時候,需要完成對 Buffer Pool 的初始化過程,就是先向操作系統申請 Buffer Pool 的內存空間,然后把它劃分成若干對控制塊和緩存頁。但是此時并沒有真實的磁盤頁被緩存到 Buffer Pool 中(因為還沒有用到),之后隨著程序的運行,會不斷的有磁盤上的頁被緩存到 Buffer Pool 中。那么問題來了,從磁盤上讀取一個頁到 Buffer Pool 中的時候該放到哪個緩存頁的位置呢?或者說怎么區分 Buffer Pool 中哪些緩存頁是空閑的,哪些已經被使用了呢?我們最好在某個地方記錄一下 Buffer Pool 中哪些緩存頁是可用的,這個時候緩存頁對應的控制塊就派上大用場了,我們可以把所有空閑的緩存頁對應的控制塊作為一個節點放到一個鏈表中,這個鏈表也可以被稱作 free 鏈表(或者說空閑鏈表)。剛剛完成初始化的 Buffer Pool 中所有的緩存頁都是空閑的,所以每一個緩存頁對應的控制塊都會被加入到 free 鏈表中,假設該 Buffer Pool 中可容納的緩存頁數量為 n,那增加了 free 鏈表的效果圖就是這樣的:

從圖中可以看出,我們為了管理好這個 free 鏈表,特意為這個鏈表定義了一個基節點,里邊兒包含著鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。這里需要注意的是,鏈表的基節點占用的內存空間并不包含在為 Buffer Pool 申請的一大片連續內存空間之內,而是單獨申請的一塊內存空間。

小貼士:鏈表基節點占用的內存空間并不大,在 MySQL5.7.21 這個版本里,每個基節點只占用 40 字節大小。后邊我們即將介紹許多不同的鏈表,它們的基節點和 free 鏈表的基節點的內存分配方式是一樣一樣的,都是單獨申請的一塊 40 字節大小的內存空間,并不包含在為 Buffer Pool 申請的一大片連續內存空間之內。

有了這個 free 鏈表之后事兒就好辦了,每當需要從磁盤中加載一個頁到 Buffer Pool 中時,就從 free 鏈表中取一個空閑的緩存頁,并且把該緩存頁對應的控制塊的信息填上(就是該頁所在的表空間、頁號之類的信息),然后把該緩存頁對應的 free 鏈表節點從鏈表中移除,表示該緩存頁已經被使用了~

緩存頁的哈希處理

我們前邊說過,當我們需要訪問某個頁中的數據時,就會把該頁從磁盤加載到 Buffer Pool 中,如果該頁已經在 Buffer Pool 中的話直接使用就可以了。那么問題也就來了,我們怎么知道該頁在不在 Buffer Pool 中呢?難不成需要依次遍歷 Buffer Pool 中各個緩存頁么?一個 Buffer Pool 中的緩存頁這么多都遍歷完豈不是要累死?

再回頭想想,我們其實是根據表空間號 + 頁號來定位一個頁的,也就相當于表空間號 + 頁號是一個 key,緩存頁就是對應的 value,怎么通過一個 key 來快速找著一個 value 呢?哈哈,那肯定是哈希表嘍~

小貼士:啥?你別告訴我你不知道哈希表是個啥?我們這個文章不是講哈希表的,如果你不會那就去找本數據結構的書看看吧~ 啥?外頭的書看不懂?別急,等我~

所以我們可以用表空間號 + 頁號作為 key,緩存頁作為 value 創建一個哈希表,在需要訪問某個頁的數據時,先從哈希表中根據表空間號 + 頁號看看有沒有對應的緩存頁,如果有,直接使用該緩存頁就好,如果沒有,那就從 free 鏈表中選一個空閑的緩存頁,然后把磁盤中對應的頁加載到該緩存頁的位置。

flush 鏈表的管理

如果我們修改了 Buffer Pool 中某個緩存頁的數據,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱為臟頁(英文名:dirty page)。當然,最簡單的做法就是每發生一次修改就立即同步到磁盤上對應的頁上,但是頻繁的往磁盤中寫數據會嚴重的影響程序的性能(畢竟磁盤慢的像烏龜一樣)。所以每次修改緩存頁后,我們并不著急立即把修改同步到磁盤上,而是在未來的某個時間點進行同步,至于這個同步的時間點我們后邊會作說明說明的,現在先不用管哈~

但是如果不立即同步到磁盤的話,那之后再同步的時候我們怎么知道 Buffer Pool 中哪些頁是臟頁,哪些頁從來沒被修改過呢?總不能把所有的緩存頁都同步到磁盤上吧,假如 Buffer Pool 被設置的很大,比方說 300G,那一次性同步這么多數據豈不是要慢死!所以,我們不得不再創建一個存儲臟頁的鏈表,凡是修改過的緩存頁對應的控制塊都會作為一個節點加入到一個鏈表中,因為這個鏈表節點對應的緩存頁都是需要被刷新到磁盤上的,所以也叫 flush 鏈表。鏈表的構造和 free 鏈表差不多,假設某個時間點 Buffer Pool 中的臟頁數量為 n,那么對應的 flush 鏈表就長這樣:

LRU 鏈表的管理

緩存不夠的窘境

Buffer Pool 對應的內存大小畢竟是有限的,如果需要緩存的頁占用的內存大小超過了 Buffer Pool 大小,也就是 free 鏈表中已經沒有多余的空閑緩存頁的時候豈不是很尷尬,發生了這樣的事兒該咋辦?當然是把某些舊的緩存頁從 Buffer Pool 中移除,然后再把新的頁放進來嘍~ 那么問題來了,移除哪些緩存頁呢?

為了回答這個問題,我們還需要回到我們設立 Buffer Pool 的初衷,我們就是想減少和磁盤的 IO 交互,最好每次在訪問某個頁的時候它都已經被緩存到 Buffer Pool 中了。假設我們一共訪問了 n 次頁,那么被訪問的頁已經在緩存中的次數除以 n 就是所謂的緩存命中率,我們的期望就是讓緩存命中率越高越好~ 從這個角度出發,回想一下我們的微信聊天列表,排在前邊的都是最近很頻繁使用的,排在后邊的自然就是最近很少使用的,假如列表能容納下的聯系人有限,你是會把最近很頻繁使用的留下還是最近很少使用的留下呢?廢話,當然是留下最近很頻繁使用的了~

簡單的 LRU 鏈表

管理 Buffer Pool 的緩存頁其實也是這個道理,當 Buffer Pool 中不再有空閑的緩存頁時,就需要淘汰掉部分最近很少使用的緩存頁。不過,我們怎么知道哪些緩存頁最近頻繁使用,哪些最近很少使用呢?呵呵,神奇的鏈表再一次派上了用場,我們可以再創建一個鏈表,由于這個鏈表是為了按照最近最少使用的原則去淘汰緩存頁的,所以這個鏈表可以被稱為 LRU 鏈表(LRU 的英文全稱:Least Recently Used)。當我們需要訪問某個頁時,可以這樣處理 LRU 鏈表:

如果該頁不在 Buffer Pool 中,在把該頁從磁盤加載到 Buffer Pool 中的緩存頁時,就把該緩存頁對應的控制塊作為節點塞到鏈表的頭部。

如果該頁已經緩存在 Buffer Pool 中,則直接把該頁對應的控制塊移動到 LRU 鏈表的頭部。

也就是說:只要我們使用到某個緩存頁,就把該緩存頁調整到 LRU 鏈表的頭部,這樣 LRU 鏈表尾部就是最近最少使用的緩存頁嘍~ 所以當 Buffer Pool 中的空閑緩存頁使用完時,到 LRU 鏈表的尾部找些緩存頁淘汰就 OK 啦,真簡單,嘖嘖 …

劃分區域的 LRU 鏈表

高興的太早了,上邊的這個簡單的 LRU 鏈表用了沒多長時間就發現問題了,因為存在這兩種比較尷尬的情況:

情況一:InnoDB 提供了一個看起來比較貼心的服務——預讀(英文名:read ahead)。所謂預讀,就是 InnoDB 認為執行當前的請求可能之后會讀取某些頁面,就預先把它們加載到 Buffer Pool 中。根據觸發方式的不同,預讀又可以細分為下邊兩種:

預讀本來是個好事兒,如果預讀到 Buffer Pool 中的頁成功的被使用到,那就可以極大的提高語句執行的效率。可是如果用不到呢?這些預讀的頁都會放到 LRU 鏈表的頭部,但是如果此時 Buffer Pool 的容量不太大而且很多預讀的頁面都沒有用到的話,這就會導致處在 LRU 鏈表尾部的一些緩存頁會很快的被淘汰掉,也就是所謂的劣幣驅逐良幣,會大大降低緩存命中率。

線性預讀

設計 InnoDB 的大叔提供了一個系統變量 innodb_read_ahead_threshold,如果順序訪問了某個區(extent)的頁面超過這個系統變量的值,就會觸發一次異步讀取下一個區中全部的頁面到 Buffer Pool 的請求,注意異步讀取意味著從磁盤中加載這些被預讀的頁面并不會影響到當前工作線程的正常執行。這個 innodb_read_ahead_threshold 系統變量的值默認是 56,我們可以在服務器啟動時通過啟動參數或者服務器運行過程中直接調整該系統變量的值,不過它是一個全局變量,注意使用 SET GLOBAL 命令來修改哦。

小貼士:InnoDB 是怎么實現異步讀取的呢?在 Windows 或者 Linux 平臺上,可能是直接調用操作系統內核提供的 AIO 接口,在其它類 Unix 操作系統中,使用了一種模擬 AIO 接口的方式來實現異步讀取,其實就是讓別的線程去讀取需要預讀的頁面。如果你讀不懂上邊這段話,那也就沒必要懂了,和我們主題其實沒太多關系,你只需要知道異步讀取并不會影響到當前工作線程的正常執行就好了。其實這個過程涉及到操作系統如何處理 IO 以及多線程的問題,找本操作系統的書看看吧,什么?操作系統的書寫的都很難懂?沒關系,等我~

隨機預讀

如果 Buffer Pool 中已經緩存了某個區的 13 個連續的頁面,不論這些頁面是不是順序讀取的,都會觸發一次異步讀取本區中所有其的頁面到 Buffer Pool 的請求。設計 InnoDB 的大叔同時提供了 innodb_random_read_ahead 系統變量,它的默認值為 OFF,也就意味著 InnoDB 并不會默認開啟隨機預讀的功能,如果我們想開啟該功能,可以通過修改啟動參數或者直接使用 SET GLOBAL 命令把該變量的值設置為 ON。

情況二:有的小伙伴可能會寫一些需要掃描全表的查詢語句(比如沒有建立合適的索引或者壓根兒沒有 WHERE 子句的查詢)。

掃描全表意味著什么?意味著將訪問到該表所在的所有頁!假設這個表中記錄非常多的話,那該表會占用特別多的頁,當需要訪問這些頁時,會把它們統統都加載到 Buffer Pool 中,這也就意味著吧唧一下,Buffer Pool 中的所有頁都被換了一次血,其他查詢語句在執行時又得執行一次從磁盤加載到 Buffer Pool 的操作。而這種全表掃描的語句執行的頻率也不高,每次執行都要把 Buffer Pool 中的緩存頁換一次血,這嚴重的影響到其他查詢對 Buffer Pool 的使用,從而大大降低了緩存命中率。

總結一下上邊說的可能降低 Buffer Pool 的兩種情況:

加載到 Buffer Pool 中的頁不一定被用到。

如果非常多的使用頻率偏低的頁被同時加載到 Buffer Pool 時,可能會把那些使用頻率非常高的頁從 Buffer Pool 中淘汰掉。

因為有這兩種情況的存在,所以設計 InnoDB 的大叔把這個 LRU 鏈表按照一定比例分成兩截,分別是:

一部分存儲使用頻率非常高的緩存頁,所以這一部分鏈表也叫做熱數據,或者稱 young 區域。

另一部分存儲使用頻率不是很高的緩存頁,所以這一部分鏈表也叫做冷數據,或者稱 old 區域。

為了方便大家理解,我們把示意圖做了簡化,各位領會精神就好:

大家要特別注意一個事兒:我們是按照某個比例將 LRU 鏈表分成兩半的,不是某些節點固定是 young 區域的,某些節點固定是 old 區域的,隨著程序的運行,某個節點所屬的區域也可能發生變化。那這個劃分成兩截的比例怎么確定呢?對于 InnoDB 存儲引擎來說,我們可以通過查看系統變量 innodb_old_blocks_pct 的值來確定 old 區域在 LRU 鏈表中所占的比例,比方說這樣:

mysql  SHOW VARIABLES LIKE  innodb_old_blocks_pct 
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
1 row in set (0.01 sec)

從結果可以看出來,默認情況下,old 區域在 LRU 鏈表中所占的比例是 37%,也就是說 old 區域大約占 LRU 鏈表的 3 /8。這個比例我們是可以設置的,我們可以在啟動時修改 innodb_old_blocks_pct 參數來控制 old 區域在 LRU 鏈表中所占的比例,比方說這樣修改配置文件:

[server]
innodb_old_blocks_pct = 40

這樣我們在啟動服務器后,old 區域占 LRU 鏈表的比例就是 40%。當然,如果在服務器運行期間,我們也可以修改這個系統變量的值,不過需要注意的是,這個系統變量屬于全局變量,一經修改,會對所有客戶端生效,所以我們只能這樣修改:

SET GLOBAL innodb_old_blocks_pct = 40;

有了這個被劃分成 young 和 old 區域的 LRU 鏈表之后,設計 InnoDB 的大叔就可以針對我們上邊提到的兩種可能降低緩存命中率的情況進行優化了:

針對預讀的頁面可能不進行后續訪問情況的優化

設計 InnoDB 的大叔規定,當磁盤上的某個頁面在初次加載到 Buffer Pool 中的某個緩存頁時,該緩存頁對應的控制塊會被放到 old 區域的頭部。這樣針對預讀到 Buffer Pool 卻不進行后續訪問的頁面就會被逐漸從 old 區域逐出,而不會影響 young 區域中被使用比較頻繁的緩存頁。

針對全表掃描時,短時間內訪問大量使用頻率非常低的頁面情況的優化

在進行全表掃描時,雖然首次被加載到 Buffer Pool 的頁被放到了 old 區域的頭部,但是后續會被馬上訪問到,每次進行訪問的時候又會把該頁放到 young 區域的頭部,這樣仍然會把那些使用頻率比較高的頁面給頂下去。有同學會想:可不可以在第一次訪問該頁面時不將其從 old 區域移動到 young 區域的頭部,后續訪問時再將其移動到 young 區域的頭部。回答是:行不通!因為設計 InnoDB 的大叔規定每次去頁面中讀取一條記錄時,都算是訪問一次頁面,而一個頁面中可能會包含很多條記錄,也就是說讀取完某個頁面的記錄就相當于訪問了這個頁面好多次。

咋辦?全表掃描有一個特點,那就是它的執行頻率非常低,誰也不會沒事兒老在那寫全表掃描的語句玩,而且在執行全表掃描的過程中,即使某個頁面中有很多條記錄,也就是去多次訪問這個頁面所花費的時間也是非常少的。所以我們只需要規定,在對某個處在 old 區域的緩存頁進行第一次訪問時就在它對應的控制塊中記錄下來這個訪問時間,如果后續的訪問時間與第一次訪問的時間在某個時間間隔內,那么該頁面就不會被從 old 區域移動到 young 區域的頭部,否則將它移動到 young 區域的頭部。上述的這個間隔時間是由系統變量 innodb_old_blocks_time 控制的,你看:

mysql  SHOW VARIABLES LIKE  innodb_old_blocks_time 
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 |
+------------------------+-------+
1 row in set (0.01 sec)

這個 innodb_old_blocks_time 的默認值是 1000,它的單位是毫秒,也就意味著對于從磁盤上被加載到 LRU 鏈表的 old 區域的某個頁來說,如果第一次和最后一次訪問該頁面的時間間隔小于 1s(很明顯在一次全表掃描的過程中,多次訪問一個頁面中的時間不會超過 1s),那么該頁是不會被加入到 young 區域的~ 當然,像 innodb_old_blocks_pct 一樣,我們也可以在服務器啟動或運行時設置 innodb_old_blocks_time 的值,這里就不贅述了,你自己試試吧~ 這里需要注意的是,如果我們把 innodb_old_blocks_time 的值設置為 0,那么每次我們訪問一個頁面時就會把該頁面放到 young 區域的頭部。

綜上所述,正是因為將 LRU 鏈表劃分為 young 和 old 區域這兩個部分,又添加了 innodb_old_blocks_time 這個系統變量,才使得預讀機制和全表掃描造成的緩存命中率降低的問題得到了遏制,因為用不到的預讀頁面以及全表掃描的頁面都只會被放到 old 區域,而不影響 young 區域中的緩存頁。

更進一步優化 LRU 鏈表

LRU 鏈表這就說完了么?沒有,早著呢~ 對于 young 區域的緩存頁來說,我們每次訪問一個緩存頁就要把它移動到 LRU 鏈表的頭部,這樣開銷是不是太大啦,畢竟在 young 區域的緩存頁都是熱點數據,也就是可能被經常訪問的,這樣頻繁的對 LRU 鏈表進行節點移動操作是不是不太好啊?是的,為了解決這個問題其實我們還可以提出一些優化策略,比如只有被訪問的緩存頁位于 young 區域的 1 / 4 的后邊,才會被移動到 LRU 鏈表頭部,這樣就可以降低調整 LRU 鏈表的頻率,從而提升性能(也就是說如果某個緩存頁對應的節點在 young 區域的 1 / 4 中,再次訪問該緩存頁時也不會將其移動到 LRU 鏈表頭部)。

小貼士:我們之前介紹隨機預讀的時候曾說,如果 Buffer Pool 中有某個區的 13 個連續頁面就會觸發隨機預讀,這其實是不嚴謹的(不幸的是 MySQL 文檔就是這么說的[攤手]),其實還要求這 13 個頁面是非常熱的頁面,所謂的非常熱,指的是這些頁面在整個 young 區域的頭 1 / 4 處。

還有沒有什么別的針對 LRU 鏈表的優化措施呢?當然有啊,你要是好好學,寫篇論文,寫本書都不是問題,可是這畢竟是一個介紹 MySQL 基礎知識的文章,再說多了篇幅就受不了了,也影響大家的閱讀體驗,所以適可而止,想了解更多的優化知識,自己去看源碼或者更多關于 LRU 鏈表的知識嘍~ 但是不論怎么優化,千萬別忘了我們的初心:盡量高效的提高 Buffer Pool 的緩存命中率。

其他的一些鏈表

為了更好的管理 Buffer Pool 中的緩存頁,除了我們上邊提到的一些措施,設計 InnoDB 的大叔們還引進了其他的一些鏈表,比如 unzip LRU 鏈表用于管理解壓頁,zip clean 鏈表用于管理沒有被解壓的壓縮頁,zip free 數組中每一個元素都代表一個鏈表,它們組成所謂的伙伴系統來為壓縮頁提供內存空間等等,反正是為了更好的管理這個 Buffer Pool 引入了各種鏈表或其他數據結構,具體的使用方式就不啰嗦了,大家有興趣深究的再去找些更深的書或者直接看源代碼吧,也可以直接來找我哈~

小貼士:我們壓根兒沒有深入嘮叨過 InnoDB 中的壓縮頁,對上邊的這些鏈表也只是為了完整性順便提一下,如果你看不懂千萬不要抑郁,因為我壓根兒就沒打算向大家介紹它們。

刷新臟頁到磁盤

后臺有專門的線程每隔一段時間負責把臟頁刷新到磁盤,這樣可以不影響用戶線程處理正常的請求。主要有兩種刷新路徑:

從 LRU 鏈表的冷數據中刷新一部分頁面到磁盤。

后臺線程會定時從 LRU 鏈表尾部開始掃描一些頁面,掃描的頁面數量可以通過系統變量 innodb_lru_scan_depth 來指定,如果從里邊兒發現臟頁,會把它們刷新到磁盤。這種刷新頁面的方式被稱之為 BUF_FLUSH_LRU。

從 flush 鏈表中刷新一部分頁面到磁盤。

后臺線程也會定時從 flush 鏈表中刷新一部分頁面到磁盤,刷新的速率取決于當時系統是不是很繁忙。這種刷新頁面的方式被稱之為 BUF_FLUSH_LIST。

有時候后臺線程刷新臟頁的進度比較慢,導致用戶線程在準備加載一個磁盤頁到 Buffer Pool 時沒有可用的緩存頁,這時就會嘗試看看 LRU 鏈表尾部有沒有可以直接釋放掉的未修改頁面,如果沒有的話會不得不將 LRU 鏈表尾部的一個臟頁同步刷新到磁盤(和磁盤交互是很慢的,這會降低處理用戶請求的速度)。這種刷新單個頁面到磁盤中的刷新方式被稱之為 BUF_FLUSH_SINGLE_PAGE。

當然,有時候系統特別繁忙時,也可能出現用戶線程批量的從 flush 鏈表中刷新臟頁的情況,很顯然在處理用戶請求過程中去刷新臟頁是一種嚴重降低處理速度的行為(畢竟磁盤的速度慢的要死),這屬于一種迫不得已的情況,不過這得放在后邊嘮叨 redo 日志的 checkpoint 時說了。

多個 Buffer Pool 實例

我們上邊說過,Buffer Pool 本質是 InnoDB 向操作系統申請的一塊連續的內存空間,在多線程環境下,訪問 Buffer Pool 中的各種鏈表都需要加鎖處理啥的,在 Buffer Pool 特別大而且多線程并發訪問特別高的情況下,單一的 Buffer Pool 可能會影響請求的處理速度。所以在 Buffer Pool 特別大的時候,我們可以把它們拆分成若干個小的 Buffer Pool,每個 Buffer Pool 都稱為一個實例,它們都是獨立的,獨立的去申請內存空間,獨立的管理各種鏈表,獨立的吧啦吧啦,所以在多線程并發訪問時并不會相互影響,從而提高并發處理能力。我們可以在服務器啟動的時候通過設置 innodb_buffer_pool_instances 的值來修改 Buffer Pool 實例的個數,比方說這樣:

[server]
innodb_buffer_pool_instances = 2

這樣就表明我們要創建 2 個 Buffer Pool 實例,示意圖就是這樣:

小貼士:為了簡便,我只把各個鏈表的基節點畫出來了,大家應該心里清楚這些鏈表的節點其實就是每個緩存頁對應的控制塊!

那每個 Buffer Pool 實例實際占多少內存空間呢?其實使用這個公式算出來的:

innodb_buffer_pool_size/innodb_buffer_pool_instances

也就是總共的大小除以實例的個數,結果就是每個 Buffer Pool 實例占用的大小。

不過也不是說 Buffer Pool 實例創建的越多越好,分別管理各個 Buffer Pool 也是需要性能開銷的,設計 InnoDB 的大叔們規定:當 innodb_buffer_pool_size 的值小于 1G 的時候設置多個實例是無效的,InnoDB 會默認把 innodb_buffer_pool_instances 的值修改為 1。而我們鼓勵在 Buffer Pool 大于或等于 1G 的時候設置多個 Buffer Pool 實例。

innodb_buffer_pool_chunk_size

在 MySQL 5.7.5 之前,Buffer Pool 的大小只能在服務器啟動時通過配置 innodb_buffer_pool_size 啟動參數來調整大小,在服務器運行過程中是不允許調整該值的。不過設計 MySQL 的大叔在 5.7.5 以及之后的版本中支持了在服務器運行過程中調整 Buffer Pool 大小的功能,但是有一個問題,就是每次當我們要重新調整 Buffer Pool 大小時,都需要重新向操作系統申請一塊連續的內存空間,然后將舊的 Buffer Pool 中的內容復制到這一塊新空間,這是極其耗時的。所以設計 MySQL 的大叔們決定不再一次性為某個 Buffer Pool 實例向操作系統申請一大片連續的內存空間,而是以一個所謂的 chunk 為單位向操作系統申請空間。也就是說一個 Buffer Pool 實例其實是由若干個 chunk 組成的,一個 chunk 就代表一片連續的內存空間,里邊兒包含了若干緩存頁與其對應的控制塊,畫個圖表示就是這樣:

上圖代表的 Buffer Pool 就是由 2 個實例組成的,每個實例中又包含 2 個 chunk。

正是因為發明了這個 chunk 的概念,我們在服務器運行期間調整 Buffer Pool 的大小時就是以 chunk 為單位增加或者刪除內存空間,而不需要重新向操作系統申請一片大的內存,然后進行緩存頁的復制。這個所謂的 chunk 的大小是我們在啟動操作 MySQL 服務器時通過 innodb_buffer_pool_chunk_size 啟動參數指定的,它的默認值是 134217728,也就是 128M。不過需要注意的是,innodb_buffer_pool_chunk_size 的值只能在服務器啟動時指定,在服務器運行過程中是不可以修改的。

小貼士:為什么不允許在服務器運行過程中修改 innodb_buffer_pool_chunk_size 的值?還不是因為 innodb_buffer_pool_chunk_size 的值代表 InnoDB 向操作系統申請的一片連續的內存空間的大小,如果你在服務器運行過程中修改了該值,就意味著要重新向操作系統申請連續的內存空間并且將原先的緩存頁和它們對應的控制塊復制到這個新的內存空間中,這是十分耗時的操作!另外,這個 innodb_buffer_pool_chunk_size 的值并不包含緩存頁對應的控制塊的內存空間大小,所以實際上 InnoDB 向操作系統申請連續內存空間時,每個 chunk 的大小要比 innodb_buffer_pool_chunk_size 的值大一些,約 5%。

配置 Buffer Pool 時的注意事項

innodb_buffer_pool_size 必須是 innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍數(這主要是想保證每一個 Buffer Pool 實例中包含的 chunk 數量相同)。

假設我們指定的 innodb_buffer_pool_chunk_size 的值是 128M,innodb_buffer_pool_instances 的值是 16,那么這兩個值的乘積就是 2G,也就是說 innodb_buffer_pool_size 的值必須是 2G 或者 2G 的整數倍。比方說我們在啟動 MySQL 服務器是這樣指定啟動參數的:

mysqld --innodb-buffer-pool-size=8G --innodb-buffer-pool-instances=16

默認的 innodb_buffer_pool_chunk_size 值是 128M,指定的 innodb_buffer_pool_instances 的值是 16,所以 innodb_buffer_pool_size 的值必須是 2G 或者 2G 的整數倍,上邊例子中指定的 innodb_buffer_pool_size 的值是 8G,符合規定,所以在服務器啟動完成之后我們查看一下該變量的值就是我們指定的 8G(8589934592 字節):

mysql  show variables like  innodb_buffer_pool_size 
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
1 row in set (0.00 sec)

如果我們指定的 innodb_buffer_pool_size 大于 2G 并且不是 2G 的整數倍,那么服務器會自動的把 innodb_buffer_pool_size 的值調整為 2G 的整數倍,比方說我們在啟動服務器時指定的 innodb_buffer_pool_size 的值是 9G:

mysqld --innodb-buffer-pool-size=9G --innodb-buffer-pool-instances=16

那么服務器會自動把 innodb_buffer_pool_size 的值調整為 10G(10737418240 字節),不信你看:

mysql  show variables like  innodb_buffer_pool_size 
+-------------------------+-------------+
| Variable_name | Value |
+-------------------------+-------------+
| innodb_buffer_pool_size | 10737418240 |
+-------------------------+-------------+
1 row in set (0.01 sec)

如果在服務器啟動時,innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的值已經大于 innodb_buffer_pool_size 的值,那么 innodb_buffer_pool_chunk_size 的值會被服務器自動設置為 innodb_buffer_pool_size/innodb_buffer_pool_instances 的值。

比方說我們在啟動服務器時指定的 innodb_buffer_pool_size 的值為 2G,innodb_buffer_pool_instances 的值為 16,innodb_buffer_pool_chunk_size 的值為 256M:

mysqld --innodb-buffer-pool-size=2G --innodb-buffer-pool-instances=16 --innodb-buffer-pool-chunk-size=256M

由于 256M × 16 = 4G,而 4G 2G,所以 innodb_buffer_pool_chunk_size 值會被服務器改寫為 innodb_buffer_pool_size/innodb_buffer_pool_instances 的值,也就是:2G/16 = 128M(134217728 字節),不信你看:

mysql  show variables like  innodb_buffer_pool_size 
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 2147483648 |
+-------------------------+------------+
1 row in set (0.01 sec)
mysql  show variables like  innodb_buffer_pool_chunk_size 
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 134217728 |
+-------------------------------+-----------+
1 row in set (0.00 sec)

Buffer Pool 中存儲的其它信息

Buffer Pool 的緩存頁除了用來緩存磁盤上的頁面以外,還可以存儲鎖信息、自適應哈希索引等信息,這些內容等我們之后遇到了再詳細討論哈~

查看 Buffer Pool 的狀態信息

設計 MySQL 的大叔貼心的給我們提供了 SHOW ENGINE INNODB STATUS 語句來查看關于 InnoDB 存儲引擎運行過程中的一些狀態信息,其中就包括 Buffer Pool 的一些信息,我們看一下(為了突出重點,我們只把輸出中關于 Buffer Pool 的部分提取了出來):

mysql  SHOW ENGINE INNODB STATUS\G
(... 省略前邊的許多狀態)
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 13218349056;
Dictionary memory allocated 4014231
Buffer pool size 786432
Free buffers 8174
Database pages 710576
Old database pages 262143
Modified db pages 124941
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 6195930012, not young 78247510485
108.18 youngs/s, 226.15 non-youngs/s
Pages read 2748866728, created 29217873, written 4845680877
160.77 reads/s, 3.80 creates/s, 190.16 writes/s
Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 710576, unzip_LRU len: 118
I/O sum[134264]:cur[144], unzip sum[16]:cur[0]
--------------
(... 省略后邊的許多狀態)
mysql

我們來詳細看一下這里邊的每個值都代表什么意思:

Total memory allocated:代表 Buffer Pool 向操作系統申請的連續內存空間大小,包括全部控制塊、緩存頁、以及碎片的大小。

Dictionary memory allocated:為數據字典信息分配的內存空間大小,注意這個內存空間和 Buffer Pool 沒啥關系,不包括在 Total memory allocated 中。

Buffer pool size:代表該 Buffer Pool 可以容納多少緩存頁,注意,單位是頁!

Free buffers:代表當前 Buffer Pool 還有多少空閑緩存頁,也就是 free 鏈表中還有多少個節點。

Database pages:代表 LRU 鏈表中的頁的數量,包含 young 和 old 兩個區域的節點數量。

Old database pages:代表 LRU 鏈表 old 區域的節點數量。

Modified db pages:代表臟頁數量,也就是 flush 鏈表中節點的數量。

Pending reads:正在等待從磁盤上加載到 Buffer Pool 中的頁面數量。

當準備從磁盤中加載某個頁面時,會先為這個頁面在 Buffer Pool 中分配一個緩存頁以及它對應的控制塊,然后把這個控制塊添加到 LRU 的 old 區域的頭部,但是這個時候真正的磁盤頁并沒有被加載進來,Pending reads 的值會跟著加 1。

Pending writes LRU:即將從 LRU 鏈表中刷新到磁盤中的頁面數量。

Pending writes flush list:即將從 flush 鏈表中刷新到磁盤中的頁面數量。

Pending writes single page:即將以單個頁面的形式刷新到磁盤中的頁面數量。

Pages made young:代表 LRU 鏈表中曾經從 old 區域移動到 young 區域頭部的節點數量。

這里需要注意,一個節點每次只有從 old 區域移動到 young 區域頭部時才會將 Pages made young 的值加 1,也就是說如果該節點本來就在 young 區域,由于它符合在 young 區域 1 / 4 后邊的要求,下一次訪問這個頁面時也會將它移動到 young 區域頭部,但這個過程并不會導致 Pages made young 的值加 1。

Page made not young:在將 innodb_old_blocks_time 設置的值大于 0 時,首次訪問或者后續訪問某個處在 old 區域的節點時由于不符合時間間隔的限制而不能將其移動到 young 區域頭部時,Page made not young 的值會加 1。

這里需要注意,對于處在 young 區域的節點,如果由于它在 young 區域的 1 / 4 處而導致它沒有被移動到 young 區域頭部,這樣的訪問并不會將 Page made not young 的值加 1。

youngs/s:代表每秒從 old 區域被移動到 young 區域頭部的節點數量。

non-youngs/s:代表每秒由于不滿足時間限制而不能從 old 區域移動到 young 區域頭部的節點數量。

Pages read、created、written:代表讀取,創建,寫入了多少頁。后邊跟著讀取、創建、寫入的速率。

Buffer pool hit rate:表示在過去某段時間,平均訪問 1000 次頁面,有多少次該頁面已經被緩存到 Buffer Pool 了。

young-making rate:表示在過去某段時間,平均訪問 1000 次頁面,有多少次訪問使頁面移動到 young 區域的頭部了。

需要大家注意的一點是,這里統計的將頁面移動到 young 區域的頭部次數不僅僅包含從 old 區域移動到 young 區域頭部的次數,還包括從 young 區域移動到 young 區域頭部的次數(訪問某個 young 區域的節點,只要該節點在 young 區域的 1 / 4 處往后,就會把它移動到 young 區域的頭部)。

not (young-making rate):表示在過去某段時間,平均訪問 1000 次頁面,有多少次訪問沒有使頁面移動到 young 區域的頭部。

需要大家注意的一點是,這里統計的沒有將頁面移動到 young 區域的頭部次數不僅僅包含因為設置了 innodb_old_blocks_time 系統變量而導致訪問了 old 區域中的節點但沒把它們移動到 young 區域的次數,還包含因為該節點在 young 區域的前 1 / 4 處而沒有被移動到 young 區域頭部的次數。

LRU len:代表 LRU 鏈表中節點的數量。

unzip_LRU:代表 unzip_LRU 鏈表中節點的數量(由于我們沒有具體嘮叨過這個鏈表,現在可以忽略它的值)。

I/O sum:最近 50s 讀取磁盤頁的總數。

I/O cur:現在正在讀取的磁盤頁數量。

I/O unzip sum:最近 50s 解壓的頁面數量。

I/O unzip cur:正在解壓的頁面數量。

總結

磁盤太慢,用內存作為緩存很有必要。

Buffer Pool 本質上是 InnoDB 向操作系統申請的一段連續的內存空間,可以通過 innodb_buffer_pool_size 來調整它的大小。

Buffer Pool 向操作系統申請的連續內存由控制塊和緩存頁組成,每個控制塊和緩存頁都是一一對應的,在填充足夠多的控制塊和緩存頁的組合后,Buffer Pool 剩余的空間可能產生不夠填充一組控制塊和緩存頁,這部分空間不能被使用,也被稱為碎片。

InnoDB 使用了許多鏈表來管理 Buffer Pool。

free 鏈表中每一個節點都代表一個空閑的緩存頁,在將磁盤中的頁加載到 Buffer Pool 時,會從 free 鏈表中尋找空閑的緩存頁。

為了快速定位某個頁是否被加載到 Buffer Pool,使用表空間號 + 頁號作為 key,緩存頁作為 value,建立哈希表。

在 Buffer Pool 中被修改的頁稱為臟頁,臟頁并不是立即刷新,而是被加入到 flush 鏈表中,待之后的某個時刻同步到磁盤上。

LRU 鏈表分為 young 和 old 兩個區域,可以通過 innodb_old_blocks_pct 來調節 old 區域所占的比例。首次從磁盤上加載到 Buffer Pool 的頁會被放到 old 區域的頭部,在 innodb_old_blocks_time 間隔時間內訪問該頁不會把它移動到 young 區域頭部。在 Buffer Pool 沒有可用的空閑緩存頁時,會首先淘汰掉 old 區域的一些頁。

我們可以通過指定 innodb_buffer_pool_instances 來控制 Buffer Pool 實例的個數,每個 Buffer Pool 實例中都有各自獨立的鏈表,互不干擾。

自 MySQL 5.7.5 版本之后,可以在服務器運行過程中調整 Buffer Pool 大小。每個 Buffer Pool 實例由若干個 chunk 組成,每個 chunk 的大小可以在服務器啟動時通過啟動參數調整。

可以用下邊的命令查看 Buffer Pool 的狀態信息:

SHOW ENGINE INNODB STATUS\G

關于“MySQ 的 Buffer pool 是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注丸趣 TV 行業資訊頻道,丸趣 TV 小編每天都會為大家更新不同的知識點。

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-07-15發表,共計18104字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 南乐县| 锦屏县| 北川| 紫金县| 来安县| 栾城县| 北海市| 定陶县| 东乡县| 开原市| 高台县| 舞钢市| 辉南县| 正宁县| 鄄城县| 施甸县| 会理县| 宁化县| 景洪市| 进贤县| 澎湖县| 莱阳市| 中方县| 芒康县| 台北县| 晋城| 德州市| 灵山县| 城固县| 迁安市| 阜平县| 昔阳县| 科技| 禄劝| 河源市| 惠东县| 光山县| 普兰店市| 苗栗县| 汝南县| 左云县|