「SQL」MySQL之緩衝池

明天的你會感謝今天努力的你

舉手之勞,加個關注

1、緩衝池的定義

應用系統分層架構,為了加速資料訪問,會把最常訪問的資料放在

快取

(cache)裡,避免每次都去訪問資料庫。作業系統會有

緩衝池

(buffer pool)機制,避免每次訪問磁碟,以加速資料的訪問。MySQL作為一個儲存系統,同樣具有緩衝池機制,以避免每次查詢資料都進行磁碟IO。

緩衝池簡單來說就是一塊記憶體區域,透過記憶體的速度來彌補磁碟速度較慢對資料庫效能的影響。

在資料庫當中讀取頁的操作,首先將從磁碟讀到的頁存放在快取池中。下一次再讀相同的頁時,首先判斷該頁是不是在緩衝池中。若在,直接讀取。否則,讀取磁碟上的頁。

對於資料庫中頁的修改操作,則首先修改快取池中的頁,然後再以一定的頻率重新整理到磁碟上。需要注意的是,緩衝池重新整理回磁碟並不是每次頁發生更新時觸發,而是透過一種稱為

Checkpoint

的機制重新整理回磁碟。

緩衝池中快取的資料頁型別有:索引頁、資料頁、undo頁、插入緩衝(insert buffer)、自適應雜湊索引(adaptive hash index)、InnoDB儲存的鎖資訊(lock info)、資料字典資訊(data dictionary)等。不能簡單地認為,緩衝池只是快取索引頁和資料頁,它們只是佔緩衝池很大的一部分而已。

下圖很好地顯示了InnoDB儲存引擎中記憶體的結構情況。

「SQL」MySQL之緩衝池

緩衝池中頁的大小預設為16KB

緩衝池大小可以透過

innodb_buffer_pool_size

引數來設定

mysql> show variables like ‘innodb_buffer_pool_size’\G;*************************** 1。 rowVariable_name: innodb_buffer_pool_sizeValue: 134217728

為了減少資料庫內部資源競爭,增加資料庫併發能力,可以使用多個緩衝例項,每個頁根據雜湊值平均分配道不同緩衝池例項中,設定引數為

innodb_buffer_poll_instances

,預設為1。

mysql> show variables like ‘innodb_buffer_pool_instances’\G;1。 row: innodb_buffer_pool_instancesValue: 1

2、預讀

2。1 基本概念

InnoDB在I/O的最佳化上有個比較重要的特性為

預讀

(Read-Ahead),它會非同步地在緩衝池中提前讀取多個預計很快就會用到的資料頁。

「SQL」MySQL之緩衝池

資料庫請求資料的時候,會將讀請求交給檔案系統,放入請求佇列中;相關程序從請求佇列中將讀請求取出,根據需求到相關資料區(記憶體、磁碟)讀取資料;取出的資料,放入響應佇列中,最後資料庫就會從響應佇列中將資料取走,完成一次資料讀操作過程。

接著程序繼續處理請求佇列,判斷後面幾個資料讀請求的資料是否相鄰,再根據自身系統IO頻寬處理量,進行預讀,進行讀請求的合併處理,一次性讀取多塊資料放入響應佇列中,再被資料庫取走。

2。2 兩種演算法

InnoDB使用兩種預讀演算法來提高I/O效能:

線性預讀

(linear read-ahead)和

隨機預讀

(randomread-ahead)

為了區分這兩種預讀的方式,我們可以把線性預讀放到以extent為單位,而隨機預讀放到以extent中的page為單位。線性預讀著眼於將下一個extent提前讀取到buffer pool中,而隨機預讀著眼於將當前extent中的剩餘的page提前讀取到buffer pool中。

2。2。1 線性預讀

線性預讀方式有一個很重要的變數控制是否將下一個extent預讀到buffer pool中,透過使用配置引數

innodb_read_ahead_threshold

控制觸發innodb執行預讀操作的時間。

如果一個extent中的被順序讀取的page超過或者等於該引數變數時,Innodb將會非同步的將下一個extent讀取到buffer pool中,可以設定為0-64的任何值(因為一個extent中也就只有64頁),預設值為56,值越高,訪問模式檢查越嚴格。

mysql> show variables like ‘innodb_read_ahead_threshold’;+——————————————-+————-+| Variable_name | Value || innodb_read_ahead_threshold | 56 |

例如,如果將值設定為48,則InnoDB只有在順序訪問當前extent中的48個pages時才觸發線性預讀請求,將下一個extent讀到記憶體中。如果值為8,InnoDB觸發非同步預讀,即使程式段中只有8頁被順序訪問。

在沒有該變數之前,當訪問到extent的最後一個page的時候,innodb會決定是否將下一個extent放入到buffer pool中。

3。2。2 隨機預讀

隨機預讀方式則是表示當同一個extent中的一些page在buffer pool中發現時,Innodb會將該extent中的剩餘page一併讀到buffer pool中。

mysql> show variables like ‘innodb_random_read_ahead’;+——————————————+————-+| Variable_name | Value || innodb_random_read_ahead | OFF |

由於隨機預讀方式給innodb code帶來了一些不必要的複雜性,同時在效能也存在不穩定性,在5。5中已經將這種預讀方式廢棄,預設是OFF。

3、緩衝重新整理策略

3。1 LRU演算法

通常來說,緩衝池是透過

LRU

Latest Recent Used

,最近最少使用)演算法來進行管理的。即最多使用頁在LRU列表前端,而最少使用頁在LRU列表後端。當緩衝池不能存放新讀取到的頁時,將首先釋放LRU列表中末端的頁。

這裡又分兩種情況:

頁已經在緩衝池裡,那就只做“移至”LRU頭部的動作,而沒有頁被淘汰;

頁不在緩衝池裡,除了做“放入”LRU頭部的動作,還要做“淘汰”LRU尾部頁的動作;

「SQL」MySQL之緩衝池

如上圖,假如管理緩衝池的LRU長度為10,緩衝了頁號為1,3,5…,40,7的頁。

假如,接下來要訪問的資料在頁號為4的頁中:

「SQL」MySQL之緩衝池

頁號為4的頁,本來就在緩衝池裡;

把頁號為4的頁,放到LRU的頭部即可,沒有頁被淘汰;

為了減少資料移動,LRU一般用連結串列實現。

假如,再接下來要訪問的資料在頁號為50的頁中:

「SQL」MySQL之緩衝池

頁號為50的頁,原來不在緩衝池裡;

把頁號為50的頁,放到LRU頭部,同時淘汰尾部頁號為7的頁;

傳統的LRU緩衝池演算法十分直觀,OS,memcache等很多軟體都在用,但是InnoDB對傳統LRU演算法做了一些最佳化,來應對

預讀失效

緩衝池汙染

的問題。

3。2 預讀失效

由於預讀,提前把頁放入了緩衝池,但最終MySQL並沒有從頁中讀取資料,稱為

預讀失效

要最佳化預讀失效,思路是:

讓預讀失敗的頁,停留在緩衝池LRU裡的時間儘可能短;

讓真正被讀取的頁,才挪到緩衝池LRU的頭部;

以此來保證真正被讀取的熱資料留在緩衝池裡的時間儘可能長。

具體方法是:

將LRU分為兩個部分:新生代(new sublist)與老生代(old sublist)

新老生代收尾相連,即:新生代的尾(tail)連線著老生代的頭(head);

新頁(例如被預讀的頁)加入緩衝池時,只加入到老生代頭部:如果資料真正被讀取(預讀成功),才會加入到新生代的頭部;如果資料沒有被讀取,則會比新生代裡的“熱資料頁”更早被淘汰出緩衝池

「SQL」MySQL之緩衝池

舉個例子,整個緩衝池LRU如上圖:

整個LRU長度是10;

前70%是新生代;

後30%是老生代;

新老生代首尾相連;

「SQL」MySQL之緩衝池

假如有一個頁號為50的新頁被預讀加入緩衝池:

50只會從老生代頭部插入,老生代尾部(也是整體尾部)的頁會被淘汰掉;

假設50這一頁不會被真正讀取,即預讀失敗,它將比新生代的資料更早淘汰出緩衝池;

「SQL」MySQL之緩衝池

假如50這一頁立刻被讀取到,例如SQL訪問了頁內的行row資料:

它會被立刻加入到新生代的頭部;

新生代的頁會被擠到老生代,此時並不會有頁面被真正淘汰;

3。3 緩衝池汙染

當某一個SQL語句,要批次掃描大量資料時,可能導致把緩衝池的所有頁都替換出去,導致大量熱資料被換出,MySQL效能急劇下降,這種情況叫

緩衝池汙染

例如,有一個數據量較大的使用者表,當執行:

select * from user where name like “%John%”;

雖然結果集可能只有少量資料,但這類like不能命中索引,必須全表掃描,就需要訪問大量的頁:

把頁加到緩衝池(插入老生代頭部);

從頁裡讀出相關的row(插入新生代頭部);

row裡的name欄位和字串shenjian進行比較,如果符合條件,加入到結果集中;

…直到掃描完所有頁中的所有row…

如此一來,所有的資料頁都會被載入到新生代的頭部,但只會訪問一次,真正的熱資料被大量換出。

怎麼這類掃碼大量資料導致的緩衝池汙染問題呢?MySQL緩衝池加入了一個“

老生代停留時間視窗

”的機制:假設T=老生代停留時間視窗,插入老生代頭部的頁,即使立刻被訪問,並不會立刻放入新生代頭部,只*滿足“被訪問”並且“在老生代停留時間”大於T,才會被放入新生代頭部。

「SQL」MySQL之緩衝池

繼續舉例,假如批次資料掃描,有51,52,53,54,55等五個頁面將要依次被訪問。

「SQL」MySQL之緩衝池

如果沒有“老生代停留時間視窗”的策略,這些批次被訪問的頁面,會換出大量熱資料。

「SQL」MySQL之緩衝池

加入“老生代停留時間視窗”策略後,短時間內被大量載入的頁,並不會立刻插入新生代頭部,而是優先淘汰那些,短期內僅僅訪問了一次的頁。

「SQL」MySQL之緩衝池

而只有在老生代呆的時間足夠久,停留時間大於T,才會被插入新生代頭部。

3。4 相關引數

mysql> show variables like ‘innodb_old_blocks_pct’\G;1。 row: innodb_old_blocks_pctValue: 37

innodb_old_blocks_pct

控制老生代佔整個LRU鏈長度的比例,預設是37,即整個LRU中新生代與老生代長度比例是63:37。如果把這個引數設為100,就退化為普通LRU了。

mysql> show variables like ‘innodb_old_blocks_time’\G;1。 row: innodb_old_blocks_timeValue: 1000

innodb_old_blocks_time

代表老生代停留時間視窗,單位是毫秒,預設是1000,即同時滿足“被訪問”與“在老生代停留時間超過1秒”兩個條件,才會被插入到新生代頭部。

資料庫剛啟動時,LRU列表是空的,緩衝池的所有頁都存放在

Free列表

中。需要新增新的緩衝時,若Free列表中有可用的空閒頁,則將其移到LRU列表;否則,根據LRU演算法,淘汰末尾頁。

LRU列表中的頁被修改後,跟磁碟上的頁就產生了不一致的情況,稱該頁為

髒頁

(dirty page)。資料庫會透過checkpoint機制將髒頁重新整理回磁碟。髒頁由

Flush列表

管理。

可以透過

show engine innodb status

命令檢視緩衝池的的狀態:

mysql> show engine innodb status\G;1。 rowType: InnoDBName:Status:=====================================2019-03-07 22:09:08 0x7000013d8000 INNODB MONITOR OUTPUTPer second averages calculated from the last 3 seconds。。。————————————BUFFER POOL AND MEMORYTotal large memory allocated 137428992Dictionary memory allocated 100382Buffer pool size 8192 //緩衝池頁的總數Free buffers 7945 //Free列表頁的數量Database pages 247 //LRU列表頁的數量Old database pages 0Modified db pages 0 //髒頁數量Pending reads 0Pending writes: LRU 0, flush list 0, single page 0Pages made young 0, not young 00。00 youngs/s, 0。00 non-youngs/sPages read 213, created 34, written 360。00 reads/s, 0。00 creates/s, 0。00 writes/sNo buffer pool page gets since the last printout //Buffer pool hit rate 1000 / 1000。。。Pages read ahead 0。00/s, evicted without access 0。00/s, Random read ahead 0。00/sLRU len: 247, unzip_LRU len: 0 //LRU表共有247頁,unzip_LRU管理的是壓縮頁I/O sum[0]:cur[0], unzip sum[0]:cur[0]。。。

本例中的資料庫是一個空資料庫,所以沒有緩衝池命中率的統計。實際應用中一般會打印出這樣的一句話:

Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000…

正常情況下緩衝池的命中率應該接近100%,如果低於95%,說明LRU表很可能存在被汙染的問題。

「SQL」MySQL之緩衝池