本篇文章給大家帶來了mysql中關于Buffer pool的相關知識,其中包括了數據頁、緩存頁free鏈表、 flush鏈表、 LRU鏈表Chunk等等,希望對大家有幫助。
緩存的重要性
通過前邊的嘮叨我們知道,對于使用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中。根據觸發方式的不同,預讀又可以細分為下邊兩種:
-
線性預讀
設計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。
預讀本來是個好事兒,如果預讀到Buffer Pool中的頁成功的被使用到,那就可以極大的提高語句執行的效率。可是如果用不到呢?這些預讀的頁都會放到LRU鏈表的頭部,但是如果此時Buffer Pool的容量不太大而且很多預讀的頁面都沒有用到的話,這就會導致處在LRU鏈表尾部的一些緩存頁會很快的被淘汰掉,也就是所謂的劣幣驅逐良幣,會大大降低緩存命中率。
-
-
情況二:有的小伙伴可能會寫一些需要掃描全表的查詢語句(比如沒有建立合適的索引或者壓根兒沒有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 STATUSG (...省略前邊的許多狀態) ---------------------- 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 STATUSG
推薦學習:mysql視頻教程