1特咆、緩沖池的定義
應(yīng)用系統(tǒng)分層架構(gòu),為了加速數(shù)據(jù)訪問(wèn)录粱,會(huì)把最常訪問(wèn)的數(shù)據(jù)放在緩存(cache)里腻格,避免每次都去訪問(wèn)數(shù)據(jù)庫(kù)画拾。操作系統(tǒng)會(huì)有緩沖池(buffer pool)機(jī)制,避免每次訪問(wèn)磁盤(pán)菜职,以加速數(shù)據(jù)的訪問(wèn)碾阁。MySQL作為一個(gè)存儲(chǔ)系統(tǒng),同樣具有緩沖池機(jī)制些楣,以避免每次查詢數(shù)據(jù)都進(jìn)行磁盤(pán)IO。
緩沖池簡(jiǎn)單來(lái)說(shuō)就是一塊內(nèi)存區(qū)域宪睹,通過(guò)內(nèi)存的速度來(lái)彌補(bǔ)磁盤(pán)速度較慢對(duì)數(shù)據(jù)庫(kù)性能的影響愁茁。
在數(shù)據(jù)庫(kù)當(dāng)中讀取頁(yè)的操作,首先將從磁盤(pán)讀到的頁(yè)存放在緩存池中亭病。下一次再讀相同的頁(yè)時(shí)鹅很,首先判斷該頁(yè)是不是在緩沖池中。若在罪帖,直接讀取促煮。否則,讀取磁盤(pán)上的頁(yè)整袁。
對(duì)于數(shù)據(jù)庫(kù)中頁(yè)的修改操作菠齿,則首先修改緩存池中的頁(yè),然后再以一定的頻率刷新到磁盤(pán)上坐昙。需要注意的是绳匀,緩沖池刷新回磁盤(pán)并不是每次頁(yè)發(fā)生更新時(shí)觸發(fā),而是通過(guò)一種稱為Checkpoint的機(jī)制刷新回磁盤(pán)炸客。
緩沖池中緩存的數(shù)據(jù)頁(yè)類型有:索引頁(yè)疾棵、數(shù)據(jù)頁(yè)、undo頁(yè)痹仙、插入緩沖(insert buffer)是尔、自適應(yīng)哈希索引(adaptive hash index)、InnoDB存儲(chǔ)的鎖信息(lock info)开仰、數(shù)據(jù)字典信息(data dictionary)等拟枚。不能簡(jiǎn)單地認(rèn)為,緩沖池只是緩存索引頁(yè)和數(shù)據(jù)頁(yè)众弓,它們只是占緩沖池很大的一部分而已梨州。
下圖很好地顯示了InnoDB存儲(chǔ)引擎中內(nèi)存的結(jié)構(gòu)情況。
緩沖池中頁(yè)的大小默認(rèn)為16KB田轧。
緩沖池大小可以通過(guò)innodb_buffer_pool_size
參數(shù)來(lái)設(shè)置
mysql> show variables like 'innodb_buffer_pool_size'\G;
*************************** 1. row ***************************
Variable_name: innodb_buffer_pool_size
Value: 134217728
為了減少數(shù)據(jù)庫(kù)內(nèi)部資源競(jìng)爭(zhēng)暴匠,增加數(shù)據(jù)庫(kù)并發(fā)能力,可以使用多個(gè)緩沖實(shí)例傻粘,每個(gè)頁(yè)根據(jù)哈希值平均分配道不同緩沖池實(shí)例中每窖,設(shè)置參數(shù)為innodb_buffer_poll_instances
帮掉,默認(rèn)為1。
mysql> show variables like 'innodb_buffer_pool_instances'\G;
*************************** 1. row ***************************
Variable_name: innodb_buffer_pool_instances
Value: 1
2、預(yù)讀
2.1 基本概念
InnoDB在I/O的優(yōu)化上有個(gè)比較重要的特性為預(yù)讀(Read-Ahead),它會(huì)異步地在緩沖池中提前讀取多個(gè)預(yù)計(jì)很快就會(huì)用到的數(shù)據(jù)頁(yè)娶靡。
數(shù)據(jù)庫(kù)請(qǐng)求數(shù)據(jù)的時(shí)候淫痰,會(huì)將讀請(qǐng)求交給文件系統(tǒng),放入請(qǐng)求隊(duì)列中他挎;相關(guān)進(jìn)程從請(qǐng)求隊(duì)列中將讀請(qǐng)求取出,根據(jù)需求到相關(guān)數(shù)據(jù)區(qū)(內(nèi)存、磁盤(pán))讀取數(shù)據(jù)昧甘;取出的數(shù)據(jù),放入響應(yīng)隊(duì)列中战得,最后數(shù)據(jù)庫(kù)就會(huì)從響應(yīng)隊(duì)列中將數(shù)據(jù)取走充边,完成一次數(shù)據(jù)讀操作過(guò)程。
接著進(jìn)程繼續(xù)處理請(qǐng)求隊(duì)列常侦,判斷后面幾個(gè)數(shù)據(jù)讀請(qǐng)求的數(shù)據(jù)是否相鄰浇冰,再根據(jù)自身系統(tǒng)IO帶寬處理量,進(jìn)行預(yù)讀聋亡,進(jìn)行讀請(qǐng)求的合并處理肘习,一次性讀取多塊數(shù)據(jù)放入響應(yīng)隊(duì)列中,再被數(shù)據(jù)庫(kù)取走坡倔。
2.2 兩種算法
InnoDB使用兩種預(yù)讀算法來(lái)提高I/O性能:線性預(yù)讀(linear read-ahead)和隨機(jī)預(yù)讀(randomread-ahead)
為了區(qū)分這兩種預(yù)讀的方式井厌,我們可以把線性預(yù)讀放到以extent為單位,而隨機(jī)預(yù)讀放到以extent中的page為單位致讥。線性預(yù)讀著眼于將下一個(gè)extent提前讀取到buffer pool中仅仆,而隨機(jī)預(yù)讀著眼于將當(dāng)前extent中的剩余的page提前讀取到buffer pool中。
2.2.1 線性預(yù)讀
線性預(yù)讀方式有一個(gè)很重要的變量控制是否將下一個(gè)extent預(yù)讀到buffer pool中垢袱,通過(guò)使用配置參數(shù)innodb_read_ahead_threshold
控制觸發(fā)innodb執(zhí)行預(yù)讀操作的時(shí)間墓拜。
如果一個(gè)extent中的被順序讀取的page超過(guò)或者等于該參數(shù)變量時(shí),Innodb將會(huì)異步的將下一個(gè)extent讀取到buffer pool中请契,innodb_read_ahead_threshold
可以設(shè)置為0-64的任何值(因?yàn)橐粋€(gè)extent中也就只有64頁(yè))咳榜,默認(rèn)值為56,值越高爽锥,訪問(wèn)模式檢查越嚴(yán)格涌韩。
mysql> show variables like 'innodb_read_ahead_threshold';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| innodb_read_ahead_threshold | 56 |
+-----------------------------+-------+
例如,如果將值設(shè)置為48氯夷,則InnoDB只有在順序訪問(wèn)當(dāng)前extent中的48個(gè)pages時(shí)才觸發(fā)線性預(yù)讀請(qǐng)求臣樱,將下一個(gè)extent讀到內(nèi)存中。如果值為8,InnoDB觸發(fā)異步預(yù)讀雇毫,即使程序段中只有8頁(yè)被順序訪問(wèn)玄捕。
在沒(méi)有該變量之前,當(dāng)訪問(wèn)到extent的最后一個(gè)page的時(shí)候棚放,innodb會(huì)決定是否將下一個(gè)extent放入到buffer pool中枚粘。
3.2.2 隨機(jī)預(yù)讀
隨機(jī)預(yù)讀方式則是表示當(dāng)同一個(gè)extent中的一些page在buffer pool中發(fā)現(xiàn)時(shí),Innodb會(huì)將該extent中的剩余page一并讀到buffer pool中飘蚯。
mysql> show variables like 'innodb_random_read_ahead';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_random_read_ahead | OFF |
+--------------------------+-------+
由于隨機(jī)預(yù)讀方式給innodb code帶來(lái)了一些不必要的復(fù)雜性馍迄,同時(shí)在性能也存在不穩(wěn)定性,在5.5中已經(jīng)將這種預(yù)讀方式廢棄局骤,默認(rèn)是OFF攀圈。
3、緩沖刷新策略
3.1 LRU算法
通常來(lái)說(shuō)庄涡,緩沖池是通過(guò)LRU(Latest Recent Used,最近最少使用)算法來(lái)進(jìn)行管理的搬设。即最多使用頁(yè)在LRU列表前端穴店,而最少使用頁(yè)在LRU列表后端。當(dāng)緩沖池不能存放新讀取到的頁(yè)時(shí)拿穴,將首先釋放LRU列表中末端的頁(yè)泣洞。
這里又分兩種情況:
- 頁(yè)已經(jīng)在緩沖池里,那就只做“移至”LRU頭部的動(dòng)作默色,而沒(méi)有頁(yè)被淘汰球凰;
- 頁(yè)不在緩沖池里腿宰,除了做“放入”LRU頭部的動(dòng)作,還要做“淘汰”LRU尾部頁(yè)的動(dòng)作吃度;
如上圖,假如管理緩沖池的LRU長(zhǎng)度為10椿每,緩沖了頁(yè)號(hào)為1伊者,3间护,5…,40汁尺,7的頁(yè)法精。
假如,接下來(lái)要訪問(wèn)的數(shù)據(jù)在頁(yè)號(hào)為4的頁(yè)中:
- 頁(yè)號(hào)為4的頁(yè)亿虽,本來(lái)就在緩沖池里;
- 把頁(yè)號(hào)為4的頁(yè)洛勉,放到LRU的頭部即可收毫,沒(méi)有頁(yè)被淘汰;
為了減少數(shù)據(jù)移動(dòng)此再,LRU一般用鏈表實(shí)現(xiàn)。
假如摘符,再接下來(lái)要訪問(wèn)的數(shù)據(jù)在頁(yè)號(hào)為50的頁(yè)中:
- 頁(yè)號(hào)為50的頁(yè)逛裤,原來(lái)不在緩沖池里猴抹;
- 把頁(yè)號(hào)為50的頁(yè),放到LRU頭部蝙砌,同時(shí)淘汰尾部頁(yè)號(hào)為7的頁(yè)跋理;
傳統(tǒng)的LRU緩沖池算法十分直觀前普,OS祠饺,memcache等很多軟件都在用汁政,但是InnoDB對(duì)傳統(tǒng)LRU算法做了一些優(yōu)化,來(lái)應(yīng)對(duì)預(yù)讀失效與緩沖池污染的問(wèn)題勺鸦。
3.2 預(yù)讀失效
由于預(yù)讀目木,提前把頁(yè)放入了緩沖池,但最終MySQL并沒(méi)有從頁(yè)中讀取數(shù)據(jù)军拟,稱為預(yù)讀失效懈息。
要優(yōu)化預(yù)讀失效,思路是:
- 讓預(yù)讀失敗的頁(yè)怒见,停留在緩沖池LRU里的時(shí)間盡可能短姑宽;
- 讓真正被讀取的頁(yè)炮车,才挪到緩沖池LRU的頭部;
以此來(lái)保證真正被讀取的熱數(shù)據(jù)留在緩沖池里的時(shí)間盡可能長(zhǎng)纪隙。
具體方法是:
- 將LRU分為兩個(gè)部分:新生代(new sublist)與老生代(old sublist)
- 新老生代收尾相連难审,即:新生代的尾(tail)連接著老生代的頭(head)亿絮;
- 新頁(yè)(例如被預(yù)讀的頁(yè))加入緩沖池時(shí)派昧,只加入到老生代頭部:如果數(shù)據(jù)真正被讀取(預(yù)讀成功)秆吵,才會(huì)加入到新生代的頭部五慈;如果數(shù)據(jù)沒(méi)有被讀取,則會(huì)比新生代里的“熱數(shù)據(jù)頁(yè)”更早被淘汰出緩沖池
舉個(gè)例子毙芜,整個(gè)緩沖池LRU如上圖:
- 整個(gè)LRU長(zhǎng)度是10腋粥;
- 前70%是新生代;
- 后30%是老生代闹瞧;
- 新老生代首尾相連展辞;
假如有一個(gè)頁(yè)號(hào)為50的新頁(yè)被預(yù)讀加入緩沖池:
- 50只會(huì)從老生代頭部插入纵竖,老生代尾部(也是整體尾部)的頁(yè)會(huì)被淘汰掉;
- 假設(shè)50這一頁(yè)不會(huì)被真正讀取已脓,即預(yù)讀失敗通殃,它將比新生代的數(shù)據(jù)更早淘汰出緩沖池;
假如50這一頁(yè)立刻被讀取到,例如SQL訪問(wèn)了頁(yè)內(nèi)的行row數(shù)據(jù):
- 它會(huì)被立刻加入到新生代的頭部曲聂;
- 新生代的頁(yè)會(huì)被擠到老生代朋腋,此時(shí)并不會(huì)有頁(yè)面被真正淘汰;
3.3 緩沖池污染
當(dāng)某一個(gè)SQL語(yǔ)句贞奋,要批量掃描大量數(shù)據(jù)時(shí)穷绵,可能導(dǎo)致把緩沖池的所有頁(yè)都替換出去,導(dǎo)致大量熱數(shù)據(jù)被換出勾缭,MySQL性能急劇下降目养,這種情況叫緩沖池污染混稽。
例如审胚,有一個(gè)數(shù)據(jù)量較大的用戶表膳叨,當(dāng)執(zhí)行:
select * from user where name like "%John%";
雖然結(jié)果集可能只有少量數(shù)據(jù)痘系,但這類like不能命中索引,必須全表掃描龄坪,就需要訪問(wèn)大量的頁(yè):
- 把頁(yè)加到緩沖池(插入老生代頭部)复唤;
- 從頁(yè)里讀出相關(guān)的row(插入新生代頭部)佛纫;
- row里的name字段和字符串shenjian進(jìn)行比較,如果符合條件好爬,加入到結(jié)果集中甥啄;
- …直到掃描完所有頁(yè)中的所有row…
如此一來(lái)蜈漓,所有的數(shù)據(jù)頁(yè)都會(huì)被加載到新生代的頭部,但只會(huì)訪問(wèn)一次充尉,真正的熱數(shù)據(jù)被大量換出衣形。
怎么這類掃碼大量數(shù)據(jù)導(dǎo)致的緩沖池污染問(wèn)題呢谆吴?MySQL緩沖池加入了一個(gè)“老生代停留時(shí)間窗口”的機(jī)制:假設(shè)T=老生代停留時(shí)間窗口苛预,插入老生代頭部的頁(yè),即使立刻被訪問(wèn)腻菇,并不會(huì)立刻放入新生代頭部,只*滿足“被訪問(wèn)”并且“在老生代停留時(shí)間”大于T糖耸,才會(huì)被放入新生代頭部嘉竟。
繼續(xù)舉例洋侨,假如批量數(shù)據(jù)掃描希坚,有51,52勾给,53锅知,54,55等五個(gè)頁(yè)面將要依次被訪問(wèn)桩警。
如果沒(méi)有“老生代停留時(shí)間窗口”的策略捶枢,這些批量被訪問(wèn)的頁(yè)面飞崖,會(huì)換出大量熱數(shù)據(jù)固歪。
加入“老生代停留時(shí)間窗口”策略后牢裳,短時(shí)間內(nèi)被大量加載的頁(yè),并不會(huì)立刻插入新生代頭部忘朝,而是優(yōu)先淘汰那些判帮,短期內(nèi)僅僅訪問(wèn)了一次的頁(yè)。
而只有在老生代呆的時(shí)間足夠久约巷,停留時(shí)間大于T旱捧,才會(huì)被插入新生代頭部。
3.4 相關(guān)參數(shù)
mysql> show variables like 'innodb_old_blocks_pct'\G;
*************************** 1. row ***************************
Variable_name: innodb_old_blocks_pct
Value: 37
innodb_old_blocks_pct
控制老生代占整個(gè)LRU鏈長(zhǎng)度的比例氓癌,默認(rèn)是37贪婉,即整個(gè)LRU中新生代與老生代長(zhǎng)度比例是63:37卢肃。如果把這個(gè)參數(shù)設(shè)為100,就退化為普通LRU了尤蒿。
mysql> show variables like 'innodb_old_blocks_time'\G;
*************************** 1. row ***************************
Variable_name: innodb_old_blocks_time
Value: 1000
innodb_old_blocks_time
代表老生代停留時(shí)間窗口腰池,單位是毫秒忙芒,默認(rèn)是1000,即同時(shí)滿足“被訪問(wèn)”與“在老生代停留時(shí)間超過(guò)1秒”兩個(gè)條件奏属,才會(huì)被插入到新生代頭部囱皿。
數(shù)據(jù)庫(kù)剛啟動(dòng)時(shí)跑杭,LRU列表是空的咆耿,緩沖池的所有頁(yè)都存放在Free列表中萨螺。需要添加新的緩沖時(shí)愧驱,若Free列表中有可用的空閑頁(yè)椭盏,則將其移到LRU列表掏颊;否則,根據(jù)LRU算法盆偿,淘汰末尾頁(yè)事扭。
LRU列表中的頁(yè)被修改后乐横,跟磁盤(pán)上的頁(yè)就產(chǎn)生了不一致的情況,稱該頁(yè)為臟頁(yè)(dirty page)罐农。數(shù)據(jù)庫(kù)會(huì)通過(guò)checkpoint機(jī)制將臟頁(yè)刷新回磁盤(pán)啃匿。臟頁(yè)由Flush列表管理蛆楞。
可以通過(guò)show engine innodb status
命令查看緩沖池的的狀態(tài):
mysql> show engine innodb status\G;
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2019-03-07 22:09:08 0x7000013d8000 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 3 seconds
...
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 100382
Buffer pool size 8192 //緩沖池頁(yè)的總數(shù)
Free buffers 7945 //Free列表頁(yè)的數(shù)量
Database pages 247 //LRU列表頁(yè)的數(shù)量
Old database pages 0
Modified db pages 0 //臟頁(yè)數(shù)量
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 213, created 34, written 36
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No 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/s
LRU len: 247, unzip_LRU len: 0 //LRU表共有247頁(yè)豹爹,unzip_LRU管理的是壓縮頁(yè)
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
...
本例中的數(shù)據(jù)庫(kù)是一個(gè)空數(shù)據(jù)庫(kù)臂聋,所以沒(méi)有緩沖池命中率的統(tǒng)計(jì)。實(shí)際應(yīng)用中一般會(huì)打印出這樣的一句話:
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000…
正常情況下緩沖池的命中率應(yīng)該接近100%艾君,如果低于95%肄方,說(shuō)明LRU表很可能存在被污染的問(wèn)題权她。