深入了解MySQL原理篇之Buffer pool(圖文詳解)

WBOY
發布: 2022-01-18 16:52:20
轉載
3539 人瀏覽過

這篇文章為大家帶來了mysql中關於Buffer pool的相關知識,其中包含了資料頁、快取頁free鍊錶、 flush鍊錶、 LRU鍊錶Chunk等等,希望對大家有幫助。

深入了解MySQL原理篇之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對應的內存空間看起來就是這樣的:

深入了解MySQL原理篇之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鍊錶的效果圖就是這樣的:

深入了解MySQL原理篇之Buffer pool(圖文詳解)

從圖中可以看出,我們為了管理好這個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鍊錶就長這樣:

深入了解MySQL原理篇之Buffer pool(圖文詳解)

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區域

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

深入了解MySQL原理篇之Buffer pool(圖文詳解)

大家要特别注意一个事儿:我们是按照某个比例将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;
登入後複製

有了这个被划分成youngold区域的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鍊錶分割為youngold區域這兩個部分,又增加了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实例,示意图就是这样:

深入了解MySQL原理篇之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就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块,画个图表示就是这样:

深入了解MySQL原理篇之Buffer pool(圖文詳解)

上图代表的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的值是128Minnodb_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的值为2Ginnodb_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鍊錶中的頁的數量,包含youngold兩個區域的節點數量。

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

  • Modified db pages:代表髒頁數量,也就是flush鍊錶中節點的數量。

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

    當準備從磁碟載入某個頁面時,會先為這個頁面在Buffer Pool中分配一個快取頁以及它對應的控制區塊,然後把這個控制區塊加入到LRUold區域的頭部,但是這個時候真正的磁碟頁並沒有被載入進來,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 readcreatedwritten:代表讀取,創建,寫入了多少頁。後邊跟著讀取、創造、寫入的速率。

  • 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:正在解壓縮的頁數。

总结

  1. 磁盘太慢,用内存作为缓存很有必要。

  2. Buffer Pool本质上是InnoDB向操作系统申请的一段连续的内存空间,可以通过innodb_buffer_pool_size来调整它的大小。

  3. Buffer Pool向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后,Buffer Pool剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为碎片

  4. InnoDB使用了许多链表来管理Buffer Pool

  5. free链表中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到Buffer Pool时,会从free链表中寻找空闲的缓存页。

  6. 为了快速定位某个页是否被加载到Buffer Pool,使用表空间号 + 页号作为key,缓存页作为value,建立哈希表。

  7. Buffer Pool中被修改的页称为脏页,脏页并不是立即刷新,而是被加入到flush链表中,待之后的某个时刻同步到磁盘上。

  8. LRU链表分为youngold两个区域,可以通过innodb_old_blocks_pct来调节old区域所占的比例。首次从磁盘上加载到Buffer Pool的页会被放到old区域的头部,在innodb_old_blocks_time间隔时间内访问该页不会把它移动到young区域头部。在Buffer Pool没有可用的空闲缓存页时,会首先淘汰掉old区域的一些页。

  9. 我们可以通过指定innodb_buffer_pool_instances来控制Buffer Pool实例的个数,每个Buffer Pool实例中都有各自独立的链表,互不干扰。

  10. MySQL 5.7.5版本之后,可以在服务器运行过程中调整Buffer Pool大小。每个Buffer Pool实例由若干个chunk组成,每个chunk的大小可以在服务器启动时通过启动参数调整。

  11. 可以用下边的命令查看Buffer Pool的状态信息:

    SHOW ENGINE INNODB STATUS\G
    登入後複製

推荐学习:mysql视频教程

以上是深入了解MySQL原理篇之Buffer pool(圖文詳解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板