這篇文章為大家帶來了mysql中關於Buffer pool的相關知識,其中包含了資料頁、快取頁free鍊錶、 flush鍊錶、 LRU鍊錶Chunk等等,希望對大家有幫助。
#透過前邊的嘮叨我們知道,對於使用InnoDB
作為儲存引擎的表來說,不管是用來儲存使用者資料的索引(包括叢集索引和二級索引),或是各種系統數據,都是以頁
的形式存放在表空間
中的,而所謂的表空間
只不過是InnoDB
對檔案系統上一個或幾個實際檔案的抽象,也就是說我們的資料說到底還是儲存在磁碟上的。但各位也都知道,磁碟的速度慢的跟烏龜一樣,怎麼能配得上「快如風,疾如電」的CPU
呢?所以InnoDB
儲存引擎在處理客戶端的請求時,當需要存取某個頁的資料時,就會把完整的頁的資料全部載入到記憶體中,也就是說即使我們只需要存取一個頁的一筆記錄,那也需要先把整頁的資料載入記憶體。將整個頁載入記憶體後就可以進行讀寫存取了,在進行完讀寫存取之後並不著急把該頁對應的記憶體空間釋放掉,而是將其快取
起來,這樣將來有請求再次造訪該頁面時,就可以省去磁碟IO
的開銷了。
設計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
中預設的快取頁大小和在磁碟上預設的頁大小是一樣的,都是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 %左右。
當我們最初啟動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鍊錶
中選一個空閒的快取頁,然後把磁碟中對應的頁載入到該快取頁的位置。
如果我們修改了Buffer Pool
中某個快取頁的數據,那它就和磁碟上的頁不一致了,這樣的快取頁也稱為髒頁
(英文名:dirty page
)。當然,最簡單的做法就是每發生一次修改就立即同步到磁碟上對應的頁上,但是頻繁的往磁碟中寫資料會嚴重的影響程式的效能(畢竟磁碟慢的像烏龜一樣)。所以每次修改快取頁後,我們並不急著立即把修改同步到磁碟上,而是在未來的某個時間點進行同步,至於這個同步的時間點我們後邊會作說明說明的,現在先不用管哈~
但是如果不立即同步到磁碟的話,那之後再同步的時候我們怎麼知道Buffer Pool
中哪些頁是髒頁
,哪些頁從來沒被修改過呢?總不能把所有的快取頁都同步到磁碟上吧,假如Buffer Pool
被設定的很大,比方說300G
,那一次同步這麼多資料豈不是要慢死!所以,我們必須再創建一個儲存髒頁的鍊錶,凡是修改過的快取頁對應的控制塊都會作為一個節點加入到一個鍊錶節點對應的快取頁都是需要被刷新到磁碟上的,所以也叫flush鍊錶
。鍊錶的構造和free鍊錶
差不多,假設某個時間點Buffer Pool
中的髒頁數量為n
,那麼對應的flush鍊錶
就長這樣:
#快取不夠的窘境
##Buffer Pool對應的記憶體大小畢竟是有限的,如果需要快取的頁所佔用的記憶體大小超過了
Buffer Pool大小,也就是
free鍊錶中已經沒有多餘的空閒快取頁的時候豈不是很尷尬,發生了這樣的事兒該咋辦?當然是把某些舊的快取頁從
Buffer Pool中移除,然後再把新的頁放進來嘍~ 那麼問題來了,移除哪些緩存頁呢?
Buffer Pool的初衷,我們就是想減少和磁碟的
IO交互,最好每次在造訪某頁的時候它都已經被快取到
Buffer Pool中了。假設我們一共造訪了
n次頁,那麼被存取的頁已經在快取中的次數除以
n就是所謂的
快取命中率,我們的期望就是讓
快取命中率越高越好~ 從這個角度出發,回想一下我們的微信聊天列表,排在前邊的都是最近很頻繁使用的,排在後邊的自然就是最近很少使用的,假如列表能容納下的聯絡人有限,你是會把最近很頻繁使用的留下還是最近很少使用的留下呢?廢話,當然是留下最近很頻繁使用的了~
Buffer Pool的緩存頁其實也是這個道理,當
Buffer Pool中不再有空閒的快取頁時,就需要淘汰掉部分最近很少使用的快取頁。不過,我們怎麼知道哪些快取頁最近常使用,哪些最近很少使用呢?呵呵,神奇的鍊錶再一次派上了用場,我們可以再創建一個鍊錶,由於這個鍊錶是為了
按照最近最少使用的原則去淘汰緩存頁的,所以這個鍊錶可以被稱為
LRU鍊錶(LRU的英文全名:Least Recently Used)。當我們需要存取某個頁面時,可以這樣處理
LRU鍊錶:
Buffer Pool中,在把該頁從磁碟載入到
Buffer Pool中的快取頁時,就把該快取頁對應的
控制區塊當作節點塞到鍊錶的頭部。
Buffer Pool中,則直接把該頁對應的
控制區塊移到
LRU鍊錶的頭部。
LRU鍊錶的頭部,這樣
LRU鍊錶尾部就是最近最少使用的快取頁嘍~ 所以當
Buffer Pool中的空閒快取頁使用完時,到
LRU鍊錶的尾部找些快取頁淘汰就OK啦,真簡單,嘖嘖...
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
本质是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
实例。
在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%。
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
的缓存页除了用来缓存磁盘上的页面以外,还可以存储锁信息、自适应哈希索引等信息,这些内容等我们之后遇到了再详细讨论哈~
设计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
推荐学习:mysql视频教程
以上是深入了解MySQL原理篇之Buffer pool(圖文詳解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!