1. 程式人生 > 其它 >16.innodb的buffer pool

16.innodb的buffer pool

1.前言

  其實早就想說說innodb的快取技術了,但是一直感覺自己可能說不下來,因此這一節我就慢慢的說

2.快取重要性

  innodb的快取是為了彌補了cpu和磁碟之間執行速度的巨大鴻溝,應該cpu的執行速度比磁碟讀寫速度要遠遠高於,因此需要在cpu和磁碟之間弄一個快取,計算機可以把磁碟的資料先載入到快取中,然後再有cpu去快取中去取然後執行,這樣可以大大提高計算機的處理速度。對於mysql的innodb來說,innodb的buffer pool就是充當這個功能的。

3.Buffer pool是啥?

  為了快取磁碟中的頁,innodb設計者在Mysql伺服器啟動時就向作業系統申請了一片連續的記憶體,他們在這片記憶體起了一個名字叫Buffer pool(緩衝池)。

4.Buffer pool內部組成

  buffer pool對應的一片連續的記憶體被劃分為若干個頁面,頁面大小與innodb表空間用的頁面大小一致,預設是16KB,為了與磁碟中的頁面區分開來,我們這裡把這些Buffer Pool中的頁面稱為緩衝頁,為了更好地管理Buffer Pool中的這些緩衝頁,設計者為每一個緩衝頁都建立了一些控制資訊。這些控制資訊包括該頁所屬的表空間編號、頁號、緩衝頁在Buffer Pool中的地址、連結串列節點資訊等。

  每個緩衝頁對應的控制資訊佔用的記憶體大小是相同的,我們把每個頁對應的控制資訊佔用的一塊記憶體一個控制塊,控制塊和緩衝頁是一一對應的,它們都存放到buffer pool中,其中控制塊存放到buffer pool的前面,緩衝頁存放到Buffer pool的後面,所以整個buffer pool對應的記憶體空間看起來如下圖:

  

  這裡可以看到控制塊和緩衝頁之間有個碎片,那麼碎片是什麼?每一個控制塊都對應一個緩衝頁,那麼在分配足夠多的控制塊和緩衝頁後,剩餘的那點兒空間可能不夠一對控制塊和緩衝頁的大小,自然也就用不到了。這個用不到的記憶體空間就被稱為碎片。在debug模式下,每個控制塊大約佔用緩衝頁大小的5%(非debug模式下會更小一點),在mysql5.7.22版本的debug模式下,每個控制塊佔用的大小是808位元組,而我們設定的innodb_buffer_pool_size並不包含這部分控制塊佔用的記憶體空間大小。也就是說innodb在為buffer pool向作業系統申請連續的記憶體空間時,這片連續的空間會比innodb_buffer_pool_size的值大5%左右。

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申請的一大片連續記憶體空間之內。而是一塊單獨申請的記憶體空間。

  有了這個free連結串列之後事情就好辦了,每當需要從磁碟中載入一個頁到buffer pool中的時,就衝free連結串列中取一個空閒的緩衝頁,並且把該緩衝頁對應的控制塊的資訊填上(就是該頁所在的表空間、頁號之類的資訊),然後把緩衝頁對應的free連結串列節點(也就是對應的控制塊從連結串列中移除,表示該緩衝頁已經被使用了)。這裡要清楚我們真正從連結串列中獲取的是控制塊,通過控制塊可以訪問到真正的頁。同理,"遍歷buffer pool中的緩衝頁"的意思是"遍歷buffer pool中各個緩衝頁對應的緩衝塊"。

  緩衝頁的雜湊處理

  當我們需要訪問某個頁中的資料時,就會把該頁從磁碟載入到buffer中,如果該頁已經在buffer pool的話,直接使用就可以了,那麼問題也就來了,我們怎麼知道該頁在不在buffer pool中呢?難不成需要依次遍歷buffer pool中的各個緩衝頁麼?一個buffer pool中的緩衝頁這麼多,都遍歷豈不是要累死?

  再回頭想想,我們其實是根據表空間號+頁號來定位一個頁的,也就相當於表空間+頁號是一個key(鍵),緩衝頁控制塊就是對應的value(值)。怎麼通過一個key來快速找到一個value呢? 這裡用的是雜湊表。。。

  所以我們可以用表空間號+頁號作為可以,用緩衝頁控制塊的地址作為value來建立一個雜湊表。在需要訪問某個頁的資料時,先從雜湊表中根據表空間號+頁號看看是否有對應的緩衝頁。如果有,直接使用該緩衝頁就好;如果有,就從free連結串列中選一個空閒的緩衝頁,然後把磁碟中對應的頁載入到該緩衝頁的位置。

6.flush連結串列的管理

  如果我們修改了buffer pool中的某個緩衝頁的資料,它就與磁碟上的頁不一致了,這樣的緩衝頁頁稱為髒頁。當然,我們可以每當修改完某個緩衝頁時,就立即將其重新整理到磁碟中對應的頁上。但是頻頻繁的往磁碟中寫資料會嚴重影響程式的效能,所以每次修改緩衝頁後,我們並不著急立即把修改重新整理到磁碟上,而是在未來的某個時間點進行重新整理。

  但是,如果不立即將修改的資料重新整理到磁碟,那之後再重新整理的時候我們怎麼知道buffer pool中哪些頁是髒頁,哪些是從來沒有用的頁呢?,所以這裡不得不再建立一個儲存髒頁的連結串列,凡是被修改過的緩衝頁對應的控制塊都會作為一個節點加入到這個連結串列中。因為這個連結串列節點對應的緩衝頁都是需要被重新整理到磁碟上的。所以也稱為flush連結串列。flush連結串列的構造與free連結串列差不多。假設buffer pool在某個時間點的髒頁數量為n,那麼對應的flush連結串列如下圖:

  

如果一個緩衝頁是空閒的,那它肯定不可能是髒頁,如果一個緩衝頁是髒頁,那它肯定就不是空閒的,也就是說,某個緩衝頁對應的控制塊不可能既是free連結串列的節點,也是flush連結串列的節點。

總結:這裡的free連結串列可以簡單地理解為在緩衝區中所欲空閒的頁組成的連結串列,而flush連結串列可以簡單地理解為所有被修改的頁(髒頁)所組成的連結串列。

7.LRU連結串列管理

  LRU連結串列其實對buffer pool緩衝區空間的一個管理方法,你可以想想看,如果一直載入資料頁到緩衝區中,遲早有一天緩衝區會被加載滿了的,這時肯定有一個機制用來釋放那麼沒用的資料頁的,該機制就是LRU演算法(最近最少使用)

  我們可以把該演算法看成一個連結串列,該連結串列分為兩個部分,一部分是用來儲存使用頻率非常高的緩衝頁,這一部分連結串列也被稱為熱資料,或者稱為young區域,另一部分是用來儲存使用頻率不是很高的緩衝頁,這一部分連結串列被稱為冷資料,或者稱為old區域。

  innodb設計通過某個比例將LRU連結串列分為兩半,這裡可以通過引數innodb_old_blocks_pct 看到舊的連結串列所佔的比例情況,

  有了這兩個區域,innodb設計者就可能針對buffer pool命中率的情況進行優化了。

  • 針對預讀的頁面可能不進行後續訪問的優化。設計者規定,當磁碟上的某個頁面在初次載入到buffer pool中某個緩衝頁時,該緩衝頁對應的控制塊會放到old區域的頭部。這樣一來,預讀到buffer pool卻不進行後續的頁面的訪問就會被逐漸從old區域逐出,而不會影響young區域中使用比較頻繁的緩衝區。
  • 針對全表掃描時,短時間內訪問大量使用頻率非常低的頁面的優化。在進行全表掃描時,雖然首次載入到buffer pool中的頁放到了old區域的頭部,但是後續會被馬上訪問到,每次進行訪問時又會把該頁放到young區域的頭部,這樣仍然會把哪些使用頻率比較高的頁面給排擠下去。因此設計者認為在執行全表掃描的過程中,即使某個頁面中有很多條記錄,儘管每讀取一條記錄都算是訪問一次頁面,但是這個過程所花費的時間也是非常少的。所以我們只需要規定,在對某個處於old區域的緩衝頁進行第一個訪問時,就在它對應的控制塊中記錄下這個訪問時間,如果後續的訪問時間與第一次訪問的時間再某個時間間隔內,那麼該頁面就不會從old區移動到young區域的頭部,否則將它移動到young區域的頭部,這個間隔時間是由系統變數innodb_old_bolcks_time控制的
root@localhost 11:52:  [(none)]> show variables like '%innodb_old_blocks_time%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
這個引數預設值是1000,單位是ms,也就意味著對於從磁碟載入到LRU連結串列中old區域的某個頁來說,如果第一次和最後一次訪問該頁面的時間間隔小於1ms,那麼該頁是不會加入到young區域的,很明顯,在一次全表掃描的過程中,多次訪問一個頁面(也就是讀取同一個頁面的多條記錄)的時間不會超過1s.