簡(jiǎn)介
MySQL InnoDB 緩沖池,里面緩存著大量數(shù)據(jù)(數(shù)據(jù)頁),使 CPU 讀取或?qū)懭霐?shù)據(jù)時(shí)坡锡,MySQL 不會(huì)直接去修改磁盤的數(shù)據(jù),因?yàn)檫@樣做太慢了窒所,MySQL 會(huì)先改內(nèi)存鹉勒,然后記錄 redo log,等有空了再刷磁盤吵取,如果內(nèi)存里沒有數(shù)據(jù)禽额,就去磁盤 load,從而解決了因?yàn)榇疟P性能慢導(dǎo)致的數(shù)據(jù)庫(kù)性能差的問題皮官。而這些數(shù)據(jù)存放的地方脯倒,就是 Buffer Pool。
功能
buffer pool 最主要的功能便是加速讀和加速寫捺氢。
加速讀:當(dāng)需要訪問一個(gè)數(shù)據(jù)頁的時(shí)候藻丢,如果這個(gè)頁已經(jīng)在緩存池中,那么就不再需要訪問磁盤摄乒,直接從緩沖池中就能獲取這個(gè)頁面的內(nèi)容悠反。
加速寫:當(dāng)需要修改一個(gè)數(shù)據(jù)頁的時(shí)候板甘,先將這個(gè)頁在緩沖池中進(jìn)行修改撇吞,記下相關(guān)的 redo log,這個(gè)頁的修改就算已經(jīng)完成了老客。至于這個(gè)被修改的頁什么時(shí)候真正刷新到磁盤拭荤,這個(gè)是 buffer pool 后臺(tái)刷新線程來完成的茵臭。
內(nèi)存淘汰
因?yàn)闄C(jī)器的內(nèi)存大小是有限的。當(dāng)數(shù)據(jù)庫(kù)的數(shù)據(jù)量比較大的時(shí)候舅世,緩存池并不能緩存所有的數(shù)據(jù)頁笼恰,所以也就可能會(huì)出現(xiàn),當(dāng)需要訪問的某個(gè)頁時(shí)歇终,該頁卻不在緩存池中的情況,這個(gè)時(shí)候就需要從磁盤中將這個(gè)頁讀出來逼龟,加載到緩存池评凝,然后再去訪問。這樣就涉及到隨機(jī)的物理 IO腺律,也就增加了操作頁所消耗的時(shí)間奕短。
這樣的情況是一個(gè) bad case宜肉,是需要盡量避免的——因此需要想辦法來提高緩存的命中率。
innodb buffer pool 采用經(jīng)典的 LRU 算法來進(jìn)行頁面淘汰翎碑,以提高緩存命中率谬返。
與傳統(tǒng)的 LRU 算法相比,buffer pool 中的 LRU 列表其中間位置被打了一個(gè) old 標(biāo)識(shí)日杈,可以簡(jiǎn)單的理解為將 LRU 列表分為兩個(gè)部分遣铝,這個(gè)標(biāo)記到 LRU 列表頭部的數(shù)據(jù)頁稱為 young 數(shù)據(jù)頁池,這個(gè)標(biāo)志到 LRU 列表尾部的數(shù)據(jù)頁稱之為 old 數(shù)據(jù)頁池莉擒。
當(dāng)一個(gè)頁從磁盤上加載到緩存池的時(shí)候酿炸,會(huì)將它放在 old 標(biāo)識(shí)之后的第一個(gè)位置,也就是說放在了 old 池子中(“中點(diǎn)插入策略”)涨冀。這個(gè)機(jī)制保證了在做大表的一次性全表掃描時(shí)填硕,即使有大量新進(jìn)來的數(shù)據(jù)頁,也會(huì)被存放在 old 池中鹿鳖,當(dāng) old 池的大小不夠緩存新進(jìn)來頁面的時(shí)候扁眯,也只是在 old 池子中進(jìn)行循環(huán)沖洗,這樣就不會(huì)沖洗 young 池子中的熱點(diǎn)頁翅帜,從而保護(hù)了熱點(diǎn)頁姻檀。這就是 buffer pool LRU 算法的簡(jiǎn)單機(jī)制。
Buffer Pool Instance 和 Buffer Pool幾個(gè)鏈表
Buffer Pool Instance
Buffer Pool 實(shí)例藕甩,大小等于
innodb_buffer_pool_size / innodb_buffer_pool_instances
innodb_buffer_pool_instances 的大小可配置施敢。
每個(gè) Buffer Pool Instance 都有自己的鎖,信號(hào)量狭莱,物理塊僵娃,各個(gè) instance 之間沒有競(jìng)爭(zhēng)關(guān)系,可以并發(fā)讀取與寫入腋妙。如果 innodb_buffer_pool_size 大小小于 1G默怨,將只有一個(gè) instance。
Buffer Pool Chunk
Buffer Pool Instance 由若干個(gè) chunk 組成骤素,一個(gè) chunk 就是一片連續(xù)的空間匙睹,每個(gè) chunk 的大小默認(rèn)為 128MB,最小為 1MB济竹,且這個(gè)值在 8.0 中是可以動(dòng)態(tài)調(diào)整生效的痕檬。Buffer Chunk 是最底層的物理塊,在啟動(dòng)階段由操作系統(tǒng)申請(qǐng)送浊,直到數(shù)據(jù)庫(kù)關(guān)閉才釋放梦谜。Buffer chunk 主要存儲(chǔ)數(shù)據(jù)頁和數(shù)據(jù)頁控制體。如下圖:
設(shè)計(jì)數(shù)據(jù)頁控制體的主要目的是為了方便管理數(shù)據(jù)頁,控制體中有指針指向數(shù)據(jù)頁唁桩。InnoDB 為每一個(gè)數(shù)據(jù)頁都創(chuàng)建了一些所謂的控制信息闭树,數(shù)據(jù)頁控制體和數(shù)據(jù)頁是一一對(duì)應(yīng)的。這些控制信息包括該頁所屬的表空間編號(hào)荒澡、頁號(hào)报辱、頁在 Buffer Pool 中的地址等。每個(gè)數(shù)據(jù)頁對(duì)應(yīng)的控制信息占用的內(nèi)存大小是相同的单山。
Free List
當(dāng)最初啟動(dòng) MySQL 服務(wù)器的時(shí)候碍现,需要完成對(duì) Buffer Pool 的初始化(分配 Buffer Pool 的內(nèi)存空間),把它劃分成若干對(duì)控制塊和緩存頁饥侵。此時(shí)并沒有真實(shí)的磁盤頁被緩存到 Buffer Pool 中(因?yàn)檫€沒有用到)鸵赫,之后隨著程序的運(yùn)行,會(huì)不斷的有磁盤上的頁被緩存到 Buffer Pool 中躏升。
因?yàn)閯倓偼瓿沙跏蓟?Buffer Pool 中所有的數(shù)據(jù)頁都是空閑的辩棒,所以每一個(gè)數(shù)據(jù)頁都會(huì)被加入到 Free List 中,假設(shè)該 Buffer Pool 中可容納的數(shù)據(jù)頁數(shù)量為 n膨疏,那增加了 Free List 的效果圖就是這樣的:
如果需要從數(shù)據(jù)庫(kù)中分配新的數(shù)據(jù)頁一睁,直接從上獲取即可。InnoDB 需要保證 Free List 有足夠的節(jié)點(diǎn)佃却,提供給用戶使用者吁,否則需要從 FLU List 或者 LRU List 淘汰一定的節(jié)點(diǎn)。
Lru List
既然 buffer pool 的目的是加速寫和加速讀饲帅,因此必須想辦法提高內(nèi)存數(shù)據(jù)頁的緩存命中率复凳。InnoDB 基于經(jīng)典的 LRU 算法管理 buffer pool 中的數(shù)據(jù)頁。一般情況下 list 頭部存放的是熱數(shù)據(jù)灶泵,就是所謂的 young page(最近經(jīng)常訪問的數(shù)據(jù))育八,list 尾部存放的就是 old page(最近不被訪問的數(shù)據(jù)),
入緩沖池的頁赦邻,優(yōu)先進(jìn)入老生代髓棋,頁被訪問,才進(jìn)入新生代惶洲,以解決預(yù)讀失效的問題按声。
Lru 有以下算法:
- 3/8 的 list 信息是作為 old list,這些信息是被驅(qū)逐的對(duì)象恬吕;
- list 的中點(diǎn)就是我們所謂的 old list 頭部和 young list 尾部的連接點(diǎn)签则,相當(dāng)于一個(gè)界限;
- 新數(shù)據(jù)首先會(huì)插入到 old list 的頭部铐料;
- 如果是 old list 的數(shù)據(jù)被訪問到了怀愧,且在老生代停留時(shí)間超過配置閾值的(默認(rèn)是1000ms)侨颈,這個(gè)頁信息才會(huì)被移動(dòng)到 young list 的頭部變成 young page,以解決批量數(shù)據(jù)訪問芯义,大量熱數(shù)據(jù)淘汰的問題;
- 在 InnoDB buffer pool 里面妻柒,不管是 young list 還是 old list 的數(shù)據(jù)扛拨,如果不會(huì)被訪問到,最后都會(huì)被移動(dòng)到 list 的尾部被淘汰举塔。
為了進(jìn)一步提高讀寫性能绑警,避免掃描 Lru List,實(shí)際上每個(gè) Buffer Pool Instance 都有一個(gè) page hash央渣,通過它计盒,使用 space_id 和 page_no 就能快速找到已經(jīng)被讀入內(nèi)存的數(shù)據(jù)頁,而不用線性遍歷 LRU List 去查找芽丹。關(guān)于 page hash 的數(shù)據(jù)結(jié)構(gòu)見總結(jié)模塊中 InnoDB Buffer Pool 的架構(gòu)圖北启。
Flush List
在了解 Flush List 之前,首先需要了解臟頁的概念拔第。
臟頁:內(nèi)存數(shù)據(jù)頁和磁盤數(shù)據(jù)頁內(nèi)容不一致的時(shí)候咕村,這個(gè)數(shù)據(jù)頁被稱為“臟頁”。內(nèi)存數(shù)據(jù)寫入磁盤后蚊俺,內(nèi)存和磁盤的數(shù)據(jù)頁內(nèi)容就一致了懈涛,稱為“干凈頁”。不論臟頁還是干凈頁泳猬,都存在內(nèi)存里批钠。
臟頁最終肯定需要被刷回磁盤而變成干凈頁,但如果每次產(chǎn)生臟頁后就立即同步到磁盤勢(shì)必將嚴(yán)重影響程序的性能(畢竟磁盤慢的像烏龜一樣)得封。所以每次修改緩存頁后埋心,我們并不著急立即把修改同步到磁盤上,而是在未來的某個(gè)時(shí)間點(diǎn)進(jìn)行同步呛每,由后臺(tái)刷新線程依次刷新到磁盤踩窖,實(shí)現(xiàn)修改落地到磁盤。
由于不是立即同步刷新臟頁晨横,所以我們不得不再創(chuàng)建一個(gè)鏈表洋腮,將臟頁保存起來。凡是在 LRU List 中被修改過的頁都需要加入這個(gè)鏈表中手形,這個(gè)鏈表中的所有節(jié)點(diǎn)都是臟頁啥供,所以也叫 Flush List,一般被簡(jiǎn)寫為 FLU List库糠。
這里的臟頁修改指的此頁被加載進(jìn) Buffer Pool 后第一次被修改伙狐,只有第一次被修改時(shí)才需要加入 FLU List(代碼中是根據(jù) page 頭部的 oldest_modification == 0 來判斷是否是第一次修改)涮毫,如果這個(gè)頁被再次修改就不會(huì)再放到 FLU List 了,因?yàn)橐呀?jīng)存在贷屎。需要注意的是罢防,臟頁數(shù)據(jù)實(shí)際還在 LRU List 中,而 FLU List 中的臟頁記錄只是通過指針指向 LRU List 中的臟頁(即在 FLU List 上的頁一定在 LRU List 上唉侄,反之不成立)咒吐。
一個(gè)數(shù)據(jù)頁可能會(huì)在不同的時(shí)刻被修改多次,在數(shù)據(jù)頁上記錄了最早一次也就是第一次修改的 LSN属划,即 oldest_modification恬叹。不同數(shù)據(jù)頁有不同的 oldest_modification,F(xiàn)LU List 中的節(jié)點(diǎn)按照 oldest_modification 排序同眯,鏈表尾是最小的绽昼,也就是最早被修改的數(shù)據(jù)頁。當(dāng)需要從 FLU List 中淘汰頁的時(shí)候须蜗,從鏈表尾部開始淘汰硅确。加入 FLU List,需要使用 flush_list_mutex 保護(hù)唠粥,所以能保證 FLU List 中節(jié)點(diǎn)的順序疏魏。
雖然臟頁既存在于 LRU List 中,也存在與 FLU List 中晤愧,但 LRU List 用來管理緩沖池中頁的可用性大莫,F(xiàn)LU List 用來管理將臟頁刷新回磁盤,二者互不影響官份。
Buffer Pool預(yù)熱
在 MySQL 重啟后只厘,由于 Buffer Pool 里面沒有什么數(shù)據(jù),因此這個(gè)時(shí)候業(yè)務(wù)上對(duì)數(shù)據(jù)庫(kù)的操作舅巷,MySQL 就只能從磁盤中讀取數(shù)據(jù)到內(nèi)存羔味,而這個(gè)過程可能需要很久才能恢復(fù)到 MySQL 重啟前內(nèi)存中保留的業(yè)務(wù)頻繁使用的熱數(shù)據(jù)。Buffer Pool 從無到重新緩存業(yè)務(wù)頻繁使用熱數(shù)據(jù)的過程稱之為預(yù)熱钠右。在預(yù)熱這個(gè)過程中赋元,MySQL 數(shù)據(jù)庫(kù)的性能不會(huì)特別好,并且 Buffer Pool 越大飒房,預(yù)熱過程越長(zhǎng)搁凸。
為了減短這個(gè)預(yù)熱過程,在 MySQL 關(guān)閉前狠毯,把 Buffer Pool 中的頁信息保存到磁盤护糖,等到 MySQL 啟動(dòng)時(shí),再根據(jù)之前保存的信息把磁盤中的數(shù)據(jù)加載到 Buffer Pool 即可嚼松。
總結(jié)
這三個(gè)重要鏈表(Free List, LRU List, FLU List)的關(guān)系可以用下圖表示:
Free List 跟 LRU List 的關(guān)系是相互流通的嫡良,頁在這兩個(gè)鏈表間來回置換锰扶。而 FLUSH List 中記錄了臟頁數(shù)據(jù),即通過指針指向了 LRU List寝受,所以圖中 FLU List 被 LRU List 包裹坷牛。
三個(gè)鏈表的元素都是控制塊指針,實(shí)際指向內(nèi)存page很澄。
數(shù)據(jù)頁訪問機(jī)制
下面梳理一下數(shù)據(jù)頁的訪問流程漓帅。
當(dāng)訪問的頁在緩存池中命中,則直接從緩沖池中訪問該頁痴怨。如果沒有命中,則需要將這個(gè) page 從磁盤上加載到緩存池器予,因此需要在 Free List 中找一個(gè)空閑的內(nèi)存頁來緩存這個(gè)從磁盤讀入的 page浪藻。
但存在空閑內(nèi)存頁被使用完的情況,不保證一定有空閑的內(nèi)存頁乾翔。假如 Free List 為空爱葵,則需要想辦法盡快產(chǎn)生空閑的內(nèi)存頁。
首先去 LRU List 中找可以替換的內(nèi)存頁(干凈頁)反浓,查找方向是從鏈表的尾部開始找萌丈,只要找到可以替換的頁,就將其從 LRU List 中移除雷则,加入空閑列表辆雾,然后再去空閑列表中找空閑的內(nèi)存頁。第一次查找最多只掃描 100 個(gè)頁月劈,循環(huán)進(jìn)行到第二次時(shí)度迂,查找深度就是整個(gè) LRU List。
如果在 LRU List 中沒有找到可以替換的頁猜揪,則進(jìn)行單頁刷新(從 FLU List 中炔涯埂),將臟頁刷新到磁盤之后而姐,再將其加入到空閑列表腊凶。這便是 InnoDB 中的 LRU 頁面淘汰機(jī)制。為什么只做單頁刷新呢拴念?因?yàn)樗哪康氖菫榱吮M快獲取空閑內(nèi)存頁钧萍,進(jìn)行臟頁刷新是不得已而為之,所以只會(huì)進(jìn)行一個(gè)頁的刷新丈莺。
Free List 是一個(gè)公共的鏈表划煮,所有的用戶線程都可以使用,存在爭(zhēng)用的情況缔俄。因此自己產(chǎn)生的空閑內(nèi)存頁有可能會(huì)剛好被其它線程所使用弛秋,用戶線程可能會(huì)重復(fù)執(zhí)行上面的查找流程器躏,直到找到空閑的內(nèi)存頁為止。
在執(zhí)行一條 SQL 語句的時(shí)候蟹略,如果恰好需要進(jìn)行單頁刷臟登失,這條 SQL 語句的執(zhí)行便會(huì)比預(yù)期更加耗時(shí),有時(shí)候看起來就像是數(shù)據(jù)庫(kù)“抖了一下”挖炬,這也是一個(gè) bad case揽浙,需要盡量避免。
通過數(shù)據(jù)頁訪問機(jī)制意敛,可以知道當(dāng)無空閑頁時(shí)產(chǎn)生空閑頁就成為了一個(gè)必須要做的事情馅巷。如果需要通過刷新臟頁來產(chǎn)生空閑頁,查找空閑頁的時(shí)間就會(huì)延長(zhǎng)草姻。因此钓猬,innodb buffer pool 中存在大量可以替換的頁,或者 Free List 中一直存在著空閑內(nèi)存頁撩独,對(duì)快速獲取空閑內(nèi)存頁就起到了決定性的作用敞曹。
重要參數(shù)配置
參數(shù):innodb_buffer_pool_size
介紹:配置緩沖池的大小,在內(nèi)存允許的情況下综膀,DBA往往會(huì)建議調(diào)大這個(gè)參數(shù)澳迫,越多數(shù)據(jù)和索引放到內(nèi)存里,數(shù)據(jù)庫(kù)的性能會(huì)越好剧劝。
參數(shù):innodb_old_blocks_pct
介紹:老生代占整個(gè)LRU鏈長(zhǎng)度的比例橄登,默認(rèn)是37,即整個(gè)LRU中新生代與老生代長(zhǎng)度比例是63:37担平。
畫外音:如果把這個(gè)參數(shù)設(shè)為100示绊,就退化為普通LRU了。
參數(shù):innodb_old_blocks_time
介紹:老生代停留時(shí)間窗口暂论,單位是毫秒面褐,默認(rèn)是1000,即同時(shí)滿足“被訪問”與“在老生代停留時(shí)間超過1秒”兩個(gè)條件取胎,才會(huì)被插入到新生代頭部展哭。
針對(duì)數(shù)據(jù)寫的優(yōu)化 - change buffer寫緩沖
當(dāng)需要更新一個(gè)數(shù)據(jù)頁時(shí),如果數(shù)據(jù)頁在內(nèi)存中就直接更新闻蛀。
而如果這個(gè)數(shù)據(jù)頁還沒有在內(nèi)存中的話匪傍,在不影響數(shù)據(jù)一致性的前提下,InnoDB 會(huì)將這些更新操作緩存在 change buffer 中觉痛,這樣就不需要從磁盤中讀入這個(gè)數(shù)據(jù)頁了役衡,減少一次IO。在下次查詢需要訪問這個(gè)數(shù)據(jù)頁的時(shí)候薪棒,將數(shù)據(jù)頁讀入內(nèi)存手蝎,然后執(zhí)行 change buffer 中與這個(gè)頁有關(guān)的操作榕莺,例如將數(shù)據(jù)合并(merge)恢復(fù)到緩沖池中。
寫緩沖的目的是降低寫操作的磁盤IO棵介,提升數(shù)據(jù)庫(kù)性能钉鸯。
加入寫緩沖優(yōu)化后,修改不在內(nèi)存中的數(shù)據(jù)頁流程優(yōu)化為:
- 在寫緩沖中記錄這個(gè)操作邮辽,一次內(nèi)存操作唠雕;
- 寫入redo log,一次磁盤順序?qū)懖僮鳌?br> 其性能與這個(gè)索引頁在緩沖池中吨述,相近岩睁。
為什么寫緩沖優(yōu)化,僅適用于非唯一普通索引頁
對(duì)于唯一索引來說揣云,所有的更新操作都要先判斷這個(gè)操作是否違反唯一性約束笙僚,InnoDB必須進(jìn)行唯一性檢查。也就是說灵再,索引頁即使不在緩沖池,磁盤上的頁讀取無法避免亿笤,否則怎么校驗(yàn)是否唯一翎迁,此時(shí)就應(yīng)該直接把相應(yīng)的頁放入緩沖池再進(jìn)行修改,而不應(yīng)該再整寫緩沖净薛。 當(dāng)數(shù)據(jù)不在內(nèi)存頁汪榔,就至少會(huì)產(chǎn)生一次IO。
除了數(shù)據(jù)頁被訪問肃拜,還有哪些場(chǎng)景會(huì)觸發(fā)刷寫緩沖中的數(shù)據(jù)呢.
還有這么幾種情況痴腌,會(huì)刷寫緩沖中的數(shù)據(jù):
- 有一個(gè)后臺(tái)線程,會(huì)認(rèn)為數(shù)據(jù)庫(kù)空閑時(shí)燃领;
- 數(shù)據(jù)庫(kù)緩沖池不夠用時(shí)士聪;
- 數(shù)據(jù)庫(kù)正常關(guān)閉時(shí);
- redo log寫滿時(shí)猛蔽;
幾乎不會(huì)出現(xiàn)redo log寫滿剥悟,此時(shí)整個(gè)數(shù)據(jù)庫(kù)處于無法寫入的不可用狀態(tài)。
什么業(yè)務(wù)場(chǎng)景曼库,適合開啟InnoDB的寫緩沖機(jī)制区岗?
先說什么時(shí)候不適合,如上文分析毁枯,當(dāng):
- 數(shù)據(jù)庫(kù)都是唯一索引慈缔;
- 或者,寫入一個(gè)數(shù)據(jù)后种玛,會(huì)立刻讀取它藐鹤;
這兩類場(chǎng)景瓤檐,在寫操作進(jìn)行時(shí)(進(jìn)行后),本來就要進(jìn)行進(jìn)行頁讀取教藻,本來相應(yīng)頁面就要入緩沖池距帅,此時(shí)寫緩存反倒成了負(fù)擔(dān),增加了復(fù)雜度括堤。
什么時(shí)候適合使用寫緩沖碌秸,如果:
- 數(shù)據(jù)庫(kù)大部分是非唯一索引;
- 業(yè)務(wù)是寫多讀少悄窃,或者不是寫后立刻讀燃サ纭;
可以使用寫緩沖轧抗,將原本每次寫入都需要進(jìn)行磁盤IO的SQL恩敌,優(yōu)化定期批量寫磁盤。
例如横媚,賬單流水業(yè)務(wù)纠炮。
重要參數(shù)設(shè)置
參數(shù):innodb_change_buffer_max_size
介紹:配置寫緩沖的大小,占整個(gè)緩沖池的比例灯蝴,默認(rèn)值是25%恢口,最大值是50%。
寫多讀少的業(yè)務(wù)穷躁,才需要調(diào)大這個(gè)值耕肩,讀多寫少的業(yè)務(wù),25%其實(shí)也多了问潭。
參數(shù):innodb_change_buffering
介紹:配置哪些寫操作啟用寫緩沖猿诸,可以設(shè)置成all/none/inserts/deletes等。