欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

mysql的Buffer?Pool存儲及原理解析

 更新時間:2022年04月01日 15:23:14   作者:小柒7  
buffer pool是mysql一個非常關(guān)鍵的核心組件,實際上主要都是針對內(nèi)存里的Buffer Pool中的數(shù)據(jù)進行的,這篇文章主要介紹了mysql的Buffer?Pool存儲及原理,需要的朋友可以參考下

一、前言

1、buffer pool是什么

咱們在使用mysql的時候,比如很簡單的select * from table;這條語句,具體查詢數(shù)據(jù)其實是在存儲引擎中實現(xiàn)的,數(shù)據(jù)庫中的數(shù)據(jù)實際上最終都是要存放在磁盤文件上的,如果每次查詢都直接從磁盤里面查詢,這樣勢必會很影響性能,所以一定是先把數(shù)據(jù)從磁盤中取出,然后放在內(nèi)存中,下次查詢直接從內(nèi)存中來取。
但是一臺機器中往往不是只有mysql一個進程在運行的,很多個進程都需要使用內(nèi)存,所以mysql中會有一個專門的內(nèi)存區(qū)域來處理這些數(shù)據(jù),這個專門為mysql準備的區(qū)域,就叫buffer pool。
buffer pool是mysql一個非常關(guān)鍵的核心組件。

如下圖所示:

在對數(shù)據(jù)庫執(zhí)行增刪改操作的時候,不可能直接更新磁盤上的數(shù)據(jù)的,因為如果你對磁盤進行隨機讀寫操作,那速度是相當?shù)穆?,隨便一個大磁盤文件的隨機讀寫操作,可能都要幾百毫秒。如果要是那么搞的話,可能你的數(shù)據(jù)庫每秒也就只能處理幾百個請求了! 在對數(shù)據(jù)庫執(zhí)行增刪改操作的時候,實際上主要都是針對內(nèi)存里的Buffer Pool中的數(shù)據(jù)進行的,也就是實際上主要是對**數(shù)據(jù)庫的內(nèi)存(Buffer Pool)**里的數(shù)據(jù)結(jié)構(gòu)進行了增刪改。
如下圖所示:

操作內(nèi)存的主要問題就是在數(shù)據(jù)庫的內(nèi)存里執(zhí)行了一堆增刪改的操作,內(nèi)存數(shù)據(jù)是更新了,但是這個時候如果數(shù)據(jù)庫突然崩潰了,那么內(nèi)存里更新好的數(shù)據(jù)不是都沒了嗎? MySQL就怕這個問題,所以引入了一個redo log機制,你在對內(nèi)存里的數(shù)據(jù)進行增刪改的時候,他同時會把增刪改對應(yīng)的日志寫入redo log中。

如下圖:

萬一數(shù)據(jù)庫突然崩潰了,沒關(guān)系,只要從redo log日志文件里讀取出來你之前做過哪些增刪改操作,瞬間就可以重新把這些增刪改操作在你的內(nèi)存里執(zhí)行一遍,這就可以恢復(fù)出來你之前做過哪些增刪改操作了。
當然對于數(shù)據(jù)更新的過程,它是有一套嚴密的步驟的,還涉及到undo log、binlog、提交事務(wù)、buffer pool臟數(shù)據(jù)刷回磁盤等等。

小結(jié):

Buffer Pool就是數(shù)據(jù)庫的一個內(nèi)存組件,里面緩存了磁盤上的真實數(shù)據(jù),然后我們的系統(tǒng)對數(shù)據(jù)庫執(zhí)行的增刪改操作,其實主要就是對這個內(nèi)存數(shù)據(jù)結(jié)構(gòu)中的緩存數(shù)據(jù)執(zhí)行的。通過這種方式,保證每個更新請求,盡量就是只更新內(nèi)存,然后往磁盤順序?qū)懭罩疚募?br />更新內(nèi)存的性能是極高的,然后順序?qū)懘疟P上的日志文件的性能也是比較高的,因為順序?qū)懘疟P文件,他的性能要遠高于隨機讀寫磁盤文件。

2、buffer pool的工作流程

以查詢語句為例

  • 在查詢的時候會先去buffer pool(內(nèi)存)中看看有沒有對應(yīng)的數(shù)據(jù)頁,如果有的話直接返回
  • 如果buffer pool中沒有對應(yīng)的數(shù)據(jù)頁,則會去磁盤中查找,磁盤中如果找到了對應(yīng)的數(shù)據(jù),則會把該頁的數(shù)據(jù)直接copy一份到buffer pool中返回給客戶端
  • 下次有同樣的查詢進來直接查找buffer pool找到對應(yīng)的數(shù)據(jù)返回即可。

3、buffer pool緩沖池和查詢緩存(query cache)

在正式講解buffer pool 之前,我們先搞清楚buffer pool緩沖池和查詢緩存(query cache)簡稱Qcache的區(qū)別。

如果將Mysql分為Server層和存儲引擎層兩大部分,那么Qcache位于Server層,Buffer Pool位于存儲引擎層。

如果Mysql 查詢緩存功能是打開的,那么當一個sql進入Mysql Server之后,Mysql Server首先會從查詢緩存中查看是否曾經(jīng)執(zhí)行過這個SQL,如果曾經(jīng)執(zhí)行過的話,曾經(jīng)執(zhí)行的查詢結(jié)果之前會以key-value的形式保存在查詢緩存中。key是sql語句,value是查詢結(jié)果。我們將這個過程稱為查詢緩存。查詢緩存會被所有的session共享。
如果查詢緩存中沒有你要找的數(shù)據(jù)的話,MySQL才會執(zhí)行后續(xù)的邏輯,通過存儲引擎將數(shù)據(jù)檢索出來。
MySQL查詢緩存是查詢結(jié)果緩存。它將以SEL開頭的查詢與哈希表進行比較,如果匹配,則返回上一次查詢的結(jié)果。進行匹配時,查詢必須逐字節(jié)匹配,例如 SELECT * FROM t1; 不等于select * from t1;,此外,一些不確定的查詢結(jié)果無法被緩存,任何對表的修改都會導(dǎo)致這些表的所有緩存無效(只要有一個sql update了該表,那么表的查詢緩存就會失效)。因此,適用于查詢緩存的最理想的方案是只讀,特別是需要檢查數(shù)百萬行后僅返回數(shù)行的復(fù)雜查詢。如果你的查詢符合這樣一個特點,開啟查詢緩存會提升你的查詢性能。
MySQL查詢緩存的目的是為了提升查詢性能,但它本身也是有性能開銷的。需要在合適的業(yè)務(wù)場景下(讀寫壓力模型)使用,不合適的業(yè)務(wù)場景不但不能提升查詢性能,查詢緩存反而會變成MySQL的瓶頸。

查詢緩存的開銷主要有:

  • 讀查詢在開始前必須先檢查是否命中緩存;
  • 如果這個讀查詢可以被緩存,那么當完成執(zhí)行后,MySQL若發(fā)現(xiàn)查詢緩存中沒有這個查詢,會將其結(jié)果存入查詢緩存,這會帶來額外的系統(tǒng)消耗;
  • 當向某個表寫入數(shù)據(jù)的時候,MySQL必須將對應(yīng)表的所有緩存都設(shè)置失效。如果查詢緩存非常大或者碎片很多,這個操作就可能帶來很大的系統(tǒng)消耗。

查詢緩存的缺點:
首先,查詢緩存的效果取決于緩存的命中率,只有命中緩存的查詢效果才能有改善,因此無法預(yù)測其性能。只要有一個sql update了該表,那么表的查詢緩存就會失效,所以當你的業(yè)務(wù)對表CRUD的比例不相上下,那么查詢緩存會影響應(yīng)用的吞吐效率。
其次,查詢緩存的另一個大問題是它受到單個互斥鎖的保護。在具有多個內(nèi)核的服務(wù)器上,大量查詢會導(dǎo)致大量的互斥鎖爭用。

注意:在mysql8.0的版本中,已經(jīng)將查詢緩存模塊刪除了。

二、buffer pool的內(nèi)存數(shù)據(jù)結(jié)構(gòu)

1、數(shù)據(jù)頁概念

我們先了解一下數(shù)據(jù)頁這個概念。它是 MySQL 抽象出來的數(shù)據(jù)單位,磁盤文件中就是存放了很多數(shù)據(jù)頁,每個數(shù)據(jù)頁里存放了很多行數(shù)據(jù)。
默認情況下,數(shù)據(jù)頁的大小是 16kb。
所以對應(yīng)的,在 Buffer Pool 中,也是以數(shù)據(jù)頁為數(shù)據(jù)單位,存放著很多數(shù)據(jù)。但是我們通常叫做緩存頁,因為 Buffer Pool 畢竟是一個緩沖池,并且里面的數(shù)據(jù)都是從磁盤文件中緩存到內(nèi)存中。它和磁盤文件中數(shù)據(jù)頁是一一對應(yīng)的。

假設(shè)我們要更新一行數(shù)據(jù),此時數(shù)據(jù)庫會找到這行數(shù)據(jù)所在的數(shù)據(jù)頁,然后從磁盤文件里把這行數(shù)據(jù)所在的數(shù)據(jù)頁直接給加載到Buffer Pool里去。如下圖。

2、那么怎么識別數(shù)據(jù)在哪個緩存頁中

每個緩存頁都會對應(yīng)著一個描述數(shù)據(jù)塊,里面包含數(shù)據(jù)頁所屬的表空間、數(shù)據(jù)頁的編號,緩存頁在 Buffer Pool 中的地址等等。
描述數(shù)據(jù)塊本身也是一塊數(shù)據(jù),它的大小大概是緩存頁大小的5%左右。假設(shè)你設(shè)置的buffer pool大小是128MB,實際上Buffer Pool真正的最終大小會超出一些,可能有個130多MB的樣子,因為他里面還要存放每個緩存頁的描述數(shù)據(jù)。
在Buffer Pool中,每個緩存頁的描述數(shù)據(jù)放在最前面,然后各個緩存頁放在后面。

所以Buffer Pool實際看起來大概如下:

3、buffer pool的初始化與配置

3.1、初始化

  • MySQL 啟動時,會根據(jù)參數(shù) innodb_buffer_pool_size 的值來為 Buffer Pool 分配內(nèi)存區(qū)域。
  • 然后會按照緩存頁的默認大小 16k 以及對應(yīng)的描述數(shù)據(jù)塊大小,在 Buffer Pool 中劃分中一個個的緩存頁和一個個的描述數(shù)據(jù)庫塊。
  • 注意:此時的緩存頁和描述數(shù)據(jù)塊都是空的,畢竟才剛啟動 MySQL。

3.2、buffer pool的配置

buffer pool通常由數(shù)個內(nèi)存塊加上一組控制結(jié)構(gòu)體對象組成。內(nèi)存塊的個數(shù)取決于buffer pool instance的個數(shù),不過在5.7版本中開始默認以128M(可配置)的chunk單位分配內(nèi)存塊,這樣做的目的是為了支持buffer pool的在線動態(tài)調(diào)整大小。
Buffer Pool默認情況下是128MB,還是有一點偏小了,我們實際生產(chǎn)環(huán)境下完全可以對Buffer Pool進行調(diào)整。 比如我們的數(shù)據(jù)庫如果是16核32G的機器,那么你就可以給Buffer Pool分配個2GB的內(nèi)存。

主要配置參數(shù)如下:

  • innodb_buffer_pool_size:這個值是設(shè)置 InnoDB Buffer Pool 的總大??;
  • innodb_buffer_pool_chunk_size:當增加或減少innodb_buffer_pool_size時,操作以塊(chunk)形式執(zhí)行,默認是128MB。
  • innodb_buffer_pool_instances:設(shè)置 InnoDB Buffer Pool 實例的個數(shù),每一個實例都有自己獨立的 list 管理Buffer Pool;
  • innodb_old_blocks_pct:默認 InnoDB Buffer Pool 中點的位置,默認值是37,最大100,也就是我們所謂的3/8的位置,可以自己設(shè)置。

這里面有個關(guān)系要確定一下,最好按照這個設(shè)置 innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instancesN(N>=1);
當buffer pool比較大的時候(超過1G),innodb會把buffer pool劃分成幾個instances,這樣可以提高讀寫操作的并發(fā),減少競爭。讀寫page都使用hash函數(shù)分配給一個instances。
當增加或者減少buffer pool大小的時候,實際上是操作的chunk。buffer pool的大小必須是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances的整數(shù)倍,如果不是的話,innodb會自動調(diào)整的。
比如:
如果指定的buffer pool size大小是9G,instances的個數(shù)是16,chunk默認的大小是128M,那么buffer會自動調(diào)整為10G。因為9216MB/16/128MB = 4.5不是整數(shù)倍,會自動調(diào)整為10240MB/16/128MB=5整數(shù)倍

3.3、Buffer Pool Size 設(shè)置和生效過程

理想情況下,在給服務(wù)器的其他進程留下足夠的內(nèi)存空間的情況下,Buffer Pool Size 應(yīng)該設(shè)置的盡可能大。當 Buffer Pool Size 設(shè)置的足夠大時,整個數(shù)據(jù)庫就相當于存儲在內(nèi)存當中,當讀取一次數(shù)據(jù)到 Buffer Pool Size 以后,后續(xù)的讀操作就不用再訪問磁盤。

設(shè)置方式:
當數(shù)據(jù)庫已經(jīng)啟動的情況下,我們可以通過在線調(diào)整的方式修改 Buffer Pool Size 的大小。
通過以下語句:SET GLOBAL innodb_buffer_pool_size=402653184;
當執(zhí)行這個語句以后,并不會立即生效,而是要等所有的事務(wù)全部執(zhí)行成功以后才會生效;新的連接和事務(wù)必須等其他事務(wù)完全執(zhí)行成功以后,Buffer Pool Size 設(shè)置生效以后才能夠連接成功,不然會一直處于等待狀態(tài)。
期間,Buffer Pool Size 要完成碎片整理,去除緩存 page 等等操作。在執(zhí)行增加或者減少 Buffer Pool Size 的操作時,操作會作為一個執(zhí)行塊執(zhí)行,innodb_buffer_pool_chunk_size 的大小會定義一個執(zhí)行塊的大小,默認的情況下,這個值是128M。
Buffer Pool Size 的大小最好設(shè)置為 innodb_buffer_pool_chunk_size/ innodb_buffer_pool_instances 的整數(shù)倍,而且是大于等于1。
如果我們要查 Buffer Pool 的狀態(tài)的話,可以使用一下sql:
SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status';
可以幫我們查看到狀態(tài)。我們可以看一下增加 Buffer Pool 的時候的一個過程,再看一下減少的時候的日志,其實還是很好理解的,我們可以看成每次增大或者減少 Buffer Pool 的時候就是進行 innodb_buffer_pool_chunk 的增加或者釋放,按照 innodb_buffer_pool_chunk_size 設(shè)定值的大小增加或者釋放執(zhí)行塊。

  • 增加的過程: 增加執(zhí)行塊,指定新地址,將新加入的執(zhí)行塊加入到 free list(控制執(zhí)行塊的一個列表)。
  • 減少的過程: 重新整理 Buffer Pool 和空閑頁,將數(shù)據(jù)從塊中移除,指定新地址。

3.4、Buffer Pool Instances

在64位操作系統(tǒng)的情況下,可以拆分緩沖池成多個部分,這樣可以在高并發(fā)的情況下最大可能的減少爭用。
配置多個 Buffer Pool Instances 能在很大程度上能夠提高 MySQL 在高并發(fā)的情況下處理事物的性能,優(yōu)化不同連接讀取緩沖頁的爭用。
我們可以通過設(shè)置 innodb_buffer_pool_instances 來設(shè)置 Buffer Pool Instances。當 InnoDB Buffer Pool 足夠大的時候,你能夠從內(nèi)存中讀取時候能有一個較好的性能,但是也有可能碰到多個線程同時請求緩沖池的瓶頸。這個時候設(shè)置多個 Buffer Pool Instances 能夠盡量減少連接的爭用。
這能夠保證每次從內(nèi)存讀取的頁都對應(yīng)一個 Buffer Pool Instances,而且這種對應(yīng)關(guān)系是一個隨機的關(guān)系。并不是熱數(shù)據(jù)存放在一個 Buffer Pool Instances下,內(nèi)部也是通過 hash 算法來實現(xiàn)這個隨機數(shù)的。每一個 Buffer Pool Instances 都有自己的 free lists,LRU 和其他的一些 Buffer Pool 的數(shù)據(jù)結(jié)構(gòu),各個 Buffer Pool Instances 是相對獨立的。
innodb_buffer_pool_instances 的設(shè)置必須大于1才算得上是多配置,但是這個功能起作用的前提是innodb_buffer_pool_size 的大小必須大于1G,理想情況下 innodb_buffer_pool_instances 的每一個 instance 都保證在1G以上。

3.5、SHOW ENGINE INNODB STATUS

當你的數(shù)據(jù)庫啟動之后,你隨時可以通過上述命令,去查看當前innodb里的一些具體情況,執(zhí)行:
SHOW ENGINE INNODB STATUS
就可以了。此時你可能會看到如下一系列的東西:

下面解釋一下這里的東西,主要講解這里跟buffer pool相關(guān)的一些東西。

  • Total memory allocated,這就是說buffer pool最終的總大小是多少
  • Buffer pool size,這就是說buffer pool一共能容納多少個緩存頁
  • Free buffers,這就是說free鏈表中一共有多少個空閑的緩存頁是可用的
  • Database pages和Old database pages,就是說lru鏈表中一共有多少個緩存頁,以及冷數(shù)據(jù)區(qū)域里的緩存頁數(shù)量
  • Modified db pages,這就是flush鏈表中的緩存頁數(shù)量
  • Pending reads和Pending writes,等待從磁盤上加載進緩存頁的數(shù)量,還有就是即將從lru鏈表中刷入磁盤的數(shù)量、即將從flush鏈表中刷入磁盤的數(shù)量
  • Pages made young和not young,這就是說已經(jīng)lru冷數(shù)據(jù)區(qū)域里訪問之后轉(zhuǎn)移到熱數(shù)據(jù)區(qū)域的緩存頁的數(shù) 量,以及在lru冷數(shù)據(jù)區(qū)域里1s內(nèi)被訪問了沒進入熱數(shù)據(jù)區(qū)域的緩存頁的數(shù)量
  • youngs/s和not youngs/s,這就是說每秒從冷數(shù)據(jù)區(qū)域進入熱數(shù)據(jù)區(qū)域的緩存頁的數(shù)量,以及每秒在冷數(shù)據(jù)區(qū)域里被訪問了但是不能進入熱數(shù)據(jù)區(qū)域的緩存頁的數(shù)量
  • Pages read xxxx, created xxx, written xxx,xx reads/s, xx creates/s, 1xx writes/s,這里就是說已經(jīng)讀取、創(chuàng)建和寫入了多少個緩存頁,以及每秒鐘讀取、創(chuàng)建和寫入的緩存頁數(shù)量
  • Buffer pool hit rate xxx / 1000,這就是說每1000次訪問,有多少次是直接命中了buffer pool里的緩存的
  • young-making rate xxx / 1000 not xx / 1000,每1000次訪問,有多少次訪問讓緩存頁從冷數(shù)據(jù)區(qū)域移動到了熱數(shù)據(jù)區(qū)域,以及沒移動的緩存頁數(shù)量
  • LRU len:這就是lru鏈表里的緩存頁的數(shù)量
  • I/O sum:最近50s讀取磁盤頁的總數(shù)
  • I/O cur:現(xiàn)在正在讀取磁盤頁的數(shù)量

三、buffer pool的空間管理

緩沖池也是有大小限制的,那么既然緩沖池有大小限制的,每次都讀入的數(shù)據(jù)頁怎么來管理呢?這里我們來聊聊緩沖池的空間管理,其實對緩沖池進行管理的關(guān)鍵部分是如何安排進池的數(shù)據(jù)并且按照一定的策略淘汰池中的數(shù)據(jù),保證池中的數(shù)據(jù)不溢出,同時還能保證常用數(shù)據(jù)留在池子中。

1、傳統(tǒng) LRU 淘汰法

緩沖池是基于傳統(tǒng)的 LRU 方法來進行緩存頁管理的,我們先來看下如果使用 LRU 是如何管理的。
LRU,全稱是 Least Recently Used,中文名字叫作「最近最少使用」。從名字上就很容易理解了。
這里分兩種情況:

1.1、緩存頁已在緩沖池中

這種情況下會將對應(yīng)的緩存頁放到 LRU 鏈表的頭部,無需從磁盤再進行讀取,也無需淘汰其它緩存頁。
如下圖所示,如果要訪問的數(shù)據(jù)在 6 號頁中,則將 6 號頁放到鏈表頭部即可,這種情況下沒有緩存頁被淘汰。

1.2、緩存頁不在緩沖池中

緩存頁不在緩沖中,這時候就需要從磁盤中讀入對應(yīng)的數(shù)據(jù)頁,將其放置在鏈表頭部,同時淘汰掉末尾的緩存頁
如下圖所示,如果要訪問的數(shù)據(jù)在 60 號頁中,60 號頁不在緩沖池中,此時加載進來放到鏈表的頭部,同時淘汰掉末尾的 17 號緩存頁。

簡單的LRU算法會帶來幾個問題:

  • **預(yù)讀失效:**上面我們提到了緩沖池的預(yù)讀機制可能會預(yù)先加載相鄰的數(shù)據(jù)頁。假如加載了 20、21 相鄰的兩個數(shù)據(jù)頁,如果只有頁號為 20 的緩存頁被訪問了,而另一個緩存頁卻沒有被訪問。此時兩個緩存頁都在鏈表的頭部,但是為了加載這兩個緩存頁卻淘汰了末尾的緩存頁,而被淘汰的緩存頁卻是經(jīng)常被訪問的。這種情況就是預(yù)讀失效,被預(yù)先加載進緩沖池的頁,并沒有被訪問到,這種情況是不是很不合理。
  • **緩沖池污染 **:還有一種情況是當執(zhí)行一條 SQL 語句時,如果掃描了大量數(shù)據(jù)或是進行了全表掃描,此時緩沖池中就會加載大量的數(shù)據(jù)頁,從而將緩沖池中已存在的所有頁替換出去,這種情況同樣是不合理的。這就是緩沖池污染,并且還會導(dǎo)致 MySQL 性能急劇下降。

2、冷熱數(shù)據(jù)分離

傳統(tǒng)的 LRU 方法并不能滿足緩沖池的空間管理。因此,Msyql 基于 LRU 設(shè)計了冷熱數(shù)據(jù)分離的處理方案。
也就是將 LRU 鏈表分為兩部分,一部分為熱數(shù)據(jù)區(qū)域,一部分為冷數(shù)據(jù)區(qū)域。

當數(shù)據(jù)頁第一次被加載到緩沖池中的時候,先將其放到冷數(shù)據(jù)區(qū)域的鏈表頭部,1s(由 innodb_old_blocks_time 參數(shù)控制) 后該緩存頁被訪問了再將其移至熱數(shù)據(jù)區(qū)域的鏈表頭部。

為什么要等 1s 后才將其移至熱數(shù)據(jù)區(qū)域呢?

如果數(shù)據(jù)頁剛被加載到冷數(shù)據(jù)區(qū)就被訪問了,之后再也不訪問它了呢?這不就造成熱數(shù)據(jù)區(qū)的浪費了嗎?要是 1s 后不訪問了,說明之后可能也不會去頻繁訪問它,也就沒有移至熱緩沖區(qū)的必要了。當緩存頁不夠的時候,從冷數(shù)據(jù)區(qū)淘汰它們就行了。

另一種情況,當我的數(shù)據(jù)頁已經(jīng)在熱緩沖區(qū)了,是不是緩存頁只要被訪問了就將其插到鏈表頭部呢?不用我說你肯定也覺得不合理。熱數(shù)據(jù)區(qū)域里的緩存頁是會被經(jīng)常訪問的,如果每訪問一個緩存頁就插入一次鏈表頭,那整個熱緩沖區(qū)里就異常騷動了,所以,Mysql 中優(yōu)化為熱數(shù)據(jù)區(qū)的后 3/4 部分被訪問后才將其移動到鏈表頭部去,對于前 1/4 部分的緩存頁被訪問了不會進行移動。

四、Buffer Pool預(yù)讀機制

預(yù)讀是mysql提高性能的一個重要的特性。預(yù)讀就是 IO 異步讀取多個頁數(shù)據(jù)讀入 Buffer Pool 的一個過程,并且這些頁被認為是很快就會被讀取到的。InnoDB使用兩種預(yù)讀算法來提高I/O性能:線性預(yù)讀(linear read-ahead)隨機預(yù)讀(randomread-ahead)
為了區(qū)分這兩種預(yù)讀的方式,我們可以把線性預(yù)讀放到以extent為單位,而隨機預(yù)讀放到以extent中的page為單位。線性預(yù)讀著眼于將下一個extent提前讀取到buffer pool中,而隨機預(yù)讀著眼于將當前extent中的剩余的page提前讀取到buffer pool中。

Linear線性預(yù)讀

線性預(yù)讀的單位是extend,一個extend中有64個page。線性預(yù)讀的一個重要參數(shù)innodb_read_ahead_threshold,是指在連續(xù)訪問多少個頁面之后,把下一個extend讀入到buffer pool中,不過預(yù)讀是一個異步的操作。當然這個參數(shù)不能超過64,因為一個extend最多只有64個頁面。
例如,innodb_read_ahead_threshold = 56,就是指在連續(xù)訪問了一個extend的56個頁面之后把下一個extend讀入到buffer pool中。在添加此參數(shù)之前,InnoDB僅計算當它在當前范圍的最后一頁中讀取時是否為整個下一個范圍發(fā)出異步預(yù)取請求。

Random隨機預(yù)讀

隨機預(yù)讀方式則是表示當同一個extent中的一些page在buffer pool中發(fā)現(xiàn)時,Innodb會將該extent中的剩余page一并讀到buffer pool中。由于隨機預(yù)讀方式給innodb code帶來了一些不必要的復(fù)雜性,同時在性能也存在不穩(wěn)定性,在5.5中已經(jīng)將這種預(yù)讀方式廢棄,默認是OFF。若要啟用此功能,即將配置變量設(shè)置innodb_random_read_ahead為ON。

五、Buffer Pool的三種Page和鏈表

Buffer Pool 是Innodb 內(nèi)存中的的一塊占比較大的區(qū)域,用來緩存表和索引數(shù)據(jù)。眾所周知,從內(nèi)存訪問會比從磁盤訪問快很多。為了提高數(shù)據(jù)的讀取速度,Buffer Pool 會通過三種Page 和鏈表來管理這些經(jīng)常訪問的數(shù)據(jù),保證熱數(shù)據(jù)不被置換出Buffer Pool。

1、三種Page

  • Free Page(空閑頁): 表示此Page 未被使用,位于 Free 鏈表。
  • Clean Page(干凈頁): 此Page 已被使用,但是頁面未發(fā)生修改,位于LRU 鏈表。
  • Dirty Page(臟頁): 此Page 已被使用,頁面已經(jīng)被修改,其數(shù)據(jù)和磁盤上的數(shù)據(jù)已經(jīng)不一致。當臟頁上的數(shù)據(jù)寫入磁盤后,內(nèi)存數(shù)據(jù)和磁盤數(shù)據(jù)一致,那么該Page 就變成了干凈頁。臟頁同時存在于LRU 鏈表和Flush 鏈表。

2、三種鏈表

2.1、LRU 鏈表

如上圖所示,是Buffer Pool里面的LRU(least recently used)鏈表。LRU鏈表是被一種叫做最近最少使用的算法管理。
LRU鏈表被分成兩部分:

  • New Sublist(Young 鏈表),用來存放經(jīng)常被讀取的頁的地址。
  • Old Sublist(Old 鏈表),用來存放較少被使用的頁面。每部分都有對應(yīng)的頭部和尾部。

默認情況下

  • Old 鏈表占整個LRU 鏈表的比例是3/8。該比例由innodb_old_blocks_pct控制,默認值是37(3/8*100),該值取值范圍為5~95,為全局動態(tài)變量。
  • 當新的頁被讀取到Buffer Pool里面的時候,和傳統(tǒng)的LRU算法插入到LRU鏈表頭部不同,Innodb LRU算法是將新的頁面插入到Y(jié)ong 鏈表的尾部和Old 鏈表的頭部中間的位置,這個位置叫做Mid Point。如上圖所示。
  • 頻繁訪問一個Buffer Pool的頁面( innodb_old_blocks_time間隔時間配置),會促使頁面往Young鏈表的頭部移動。如果一個Page在被讀到Buffer Pool后很快就被訪問,那么該Page會往Young List的頭部移動,但是如果一個頁面是通過預(yù)讀的方式讀到Buffer Pool,且之后短時間內(nèi)沒有被訪問,那么很可能在下次訪問之前就被移動到Old List的尾部,而被驅(qū)逐了。
  • 隨著數(shù)據(jù)庫的持續(xù)運行,新的頁面被不斷的插入到LRU鏈表的Mid Point,Old 鏈表里的頁面會逐漸的被移動Old鏈表的尾部。同時,當經(jīng)常被訪問的頁面移動到LRU鏈表頭部的時候,那些沒有被訪問的頁面會逐漸的被移動到鏈表的尾部。最終,位于Old 鏈表尾部的頁面將被驅(qū)逐。

如果一個數(shù)據(jù)頁已經(jīng)處于Young 鏈表,當它再次被訪問的時候,只有當其處于Young 鏈表長度的1/4(大約值)之后,才會被移動到Y(jié)oung 鏈表的頭部。這樣做的目的是減少對LRU 鏈表的修改,因為LRU 鏈表的目標是保證經(jīng)常被訪問的數(shù)據(jù)頁不會被驅(qū)逐出去。

移動到y(tǒng)oung鏈表的時間配置:
innodb_old_blocks_time 控制的Old 鏈表頭部頁面的轉(zhuǎn)移策略。該Page需要在Old 鏈表停留超過innodb_old_blocks_time 時間,之后再次被訪問,才會移動到Y(jié)oung 鏈表。這么操作是避免Young 鏈表被那些只在innodb_old_blocks_time時間間隔內(nèi)頻繁訪問,之后就不被訪問的頁面塞滿,從而有效的保護Young 鏈表。
在全表掃描或者全索引掃描的時候,Innodb會將大量的頁面寫入LRU 鏈表的Mid Point位置,并且只在短時間內(nèi)訪問幾次之后就不再訪問了。設(shè)置innodb_old_blocks_time的時間窗口可以有效的保護Young List,保證了真正的頻繁訪問的頁面不被驅(qū)逐。
innodb_old_blocks_time 單位是毫秒,默認值是1000。調(diào)大該值提高了從Old鏈表移動到Y(jié)oung鏈表的難度,會促使更多頁面被移動到Old 鏈表,老化,從而被驅(qū)逐。
當掃描的表很大,Buffer Pool都放不下時,可以將innodb_old_blocks_pct設(shè)置為較小的值,這樣只讀取一次的數(shù)據(jù)頁就不會占據(jù)大部分的Buffer Pool。例如,設(shè)置innodb_old_blocks_pct = 5,會將僅讀取一次的數(shù)據(jù)頁在Buffer Pool的占用限制為5%。
當經(jīng)常掃描一些小表時,這些頁面在Buffer Pool移動的開銷較小,我們可以適當?shù)恼{(diào)大innodb_old_blocks_pct,例如設(shè)置innodb_old_blocks_pct = 50。

SHOW ENGINE INNODB STATUS 里面提供了Buffer Pool一些監(jiān)控指標,有幾個我們需要關(guān)注一下:

  • youngs/s:該指標表示的是每秒訪問Old 鏈表中頁面,使其移動到Y(jié)oung鏈表的次數(shù)。如果MySQL實例都是一些小事務(wù),沒有大表全掃描,且該指標很小,就需要調(diào)大innodb_old_blocks_pct 或者減小innodb_old_blocks_time,這樣會使得Old List 的長度更長,Old頁面被移動到Old List 的尾部消耗的時間會更久,那么就提升了下一次訪問到Old List里面的頁面的可能性。如果該指標很大,可以調(diào)小innodb_old_blocks_pct,同時調(diào)innodb_old_blocks_time保護熱數(shù)據(jù)。
  • non-youngs/s:該指標表示的是每秒訪問Old 鏈表中頁面,沒有移動到Y(jié)oung鏈表的次數(shù),因為其不符合innodb_old_blocks_time。如果該指標很大,一般情況下是MySQL存在大量的全表掃描。如果MySQL存在大量全表掃描,且這個指標又不大的時候,需要調(diào)大innodb_old_blocks_time,因為這個指標不大意味著全表掃描的頁面被移動到Y(jié)oung 鏈表了,調(diào)大innodb_old_blocks_time時間會使得這些短時間頻繁訪問的頁面保留在Old 鏈表里面。

臟頁清除線程:

  • 每隔1秒鐘,Page Cleaner線程執(zhí)行LRU List Flush的操作,來釋放足夠的Free Page。
  • innodb_lru_scan_depth 變量控制每個Buffer Pool實例每次掃描LRU List的長度,來尋找對應(yīng)的臟頁,執(zhí)行Flush操作。

2.2、Flush 鏈表

  • Flush 鏈表里面保存的都是臟頁,也會存在于LRU 鏈表。
  • Flush 鏈表是按照oldest_modification排序,值大的在頭部,值小的在尾部
  • 當有頁面訪被修改的時候,使用mini-transaction,對應(yīng)的page進入Flush 鏈表
  • 如果當前頁面已經(jīng)是臟頁,就不需要再次加入Flush list,否則是第一次修改,需要加入Flush 鏈表
  • 當Page Cleaner線程執(zhí)行flush操作的時候,從尾部開始scan,將一定的臟頁寫入磁盤,推進檢查點,減少recover的時間

SQL 的增刪改查都在 Buffer Pool 中執(zhí)行,慢慢地,Buffer Pool 中的緩存頁因為不斷被修改而導(dǎo)致和磁盤文件中的數(shù)據(jù)不一致了,也就是 Buffer Pool 中會有很多個臟頁,臟頁里面很多臟數(shù)據(jù)。
所以,MySQL 會有一條后臺線程,定時地將 Buffer Pool 中的臟頁刷回到磁盤文件中。但是,后臺線程怎么知道哪些緩存頁是臟頁呢,不可能將全部的緩存頁都往磁盤中刷吧,這會導(dǎo)致 MySQL 暫停一段時間。

MySQL 是怎么判斷臟頁的

我們引入一個和 free 鏈表類似的 flush 鏈表。他的本質(zhì)也是通過緩存頁的描述數(shù)據(jù)塊中的兩個指針,讓修改過的緩存頁的描述數(shù)據(jù)塊能串成一個雙向鏈表,這兩指針大家可以認為是 flush_pre 指針和 flush_next 指針。

下面我用偽代碼來描述一下:

DescriptionDataBlock{
block_id = block1; // free 鏈表的
free_pre = null;
free_next = null; // flush 鏈表的
flush_pre = null;
flush_next = block2;
}

flush 鏈表也有對應(yīng)的基礎(chǔ)節(jié)點,也是包含鏈表的頭節(jié)點和尾節(jié)點,還有就是修改過的緩存頁的數(shù)量。

FlushListBaseNode{
start = block1;
end = block2;
count = 2;
}

到這里,我們都知道,SQL 的增刪改都會使得緩存頁變?yōu)榕K頁,此時會修改臟頁對應(yīng)的描述數(shù)據(jù)塊的 flush_pre 指針和 flush_next 指針,使得描述數(shù)據(jù)塊加入到 flush 鏈表中,之后 MySQL 的后臺線程就可以將這個臟頁刷回到磁盤中。

2.3、Free 鏈表

  • Free 鏈表存放的是空閑頁面,初始化的時候申請一定數(shù)量的頁面,當 MySQL 啟動后,會不斷地有 SQL 請求進來,此時空先的緩存頁就會不斷地被使用。
  • 在執(zhí)行SQL的過程中,每次成功load 頁面到內(nèi)存后,會判斷Free 鏈表的頁面是否夠用。如果不夠用的話,就flush LRU 鏈表和Flush 鏈表來釋放空閑頁。如果夠用,就從Free 鏈表里面刪除對應(yīng)的頁面,在LRU 鏈表增加頁面,保持總數(shù)不變。

使用原理

free 鏈表,它是一個雙向鏈表,鏈表的每個節(jié)點就是一個個空閑的緩存頁對應(yīng)的描述數(shù)據(jù)塊。他本身其實就是由 Buffer Pool 里的描述數(shù)據(jù)塊組成的,你可以認為是每個描述數(shù)據(jù)塊里都有兩個指針,一個是 free_pre 指針,一個是 free_next 指針,分別指向自己的上一個 free 鏈表的節(jié)點,以及下一個 free 鏈表的節(jié)點。
通過 Buffer Pool 中的描述數(shù)據(jù)塊的 free_pre 和 free_next 兩個指針,就可以把所有的描述數(shù)據(jù)塊串成一個 free 鏈表。
下面我們可以用偽代碼來描述一下 free 鏈表中描述數(shù)據(jù)塊節(jié)點的數(shù)據(jù)結(jié)構(gòu):

DescriptionDataBlock{
block_id = block1;
free_pre = null;
free_next = block2;
}

free 鏈表有一個基礎(chǔ)節(jié)點,他會引用鏈表的頭節(jié)點和尾節(jié)點,里面還存儲了鏈表中有多少個描述數(shù)據(jù)塊的節(jié)點,也就是有多少個空閑的緩存頁。

下面我們也用偽代碼來描述一下基礎(chǔ)節(jié)點的數(shù)據(jù)結(jié)構(gòu):

FreeListBaseNode{
start = block01;
end = block03;
count = 2;
}

到此,free 鏈表就介紹完了。上面我們也介紹了 MySQL 啟動時 Buffer Pool 的初始流程,接下來,我會將結(jié)合剛介紹完的 free 鏈表,講解一下 SQL 進來時,磁盤數(shù)據(jù)頁讀取到 Buffer Pool 的緩存頁的過程。
但是,我們先要了解一下一個新概念:數(shù)據(jù)頁緩存哈希表,它的 key 是表空間+數(shù)據(jù)頁號,而 value 是對應(yīng)緩存頁的地址。

描述如圖所示:

磁盤數(shù)據(jù)頁讀取到 Buffer Pool 的緩存頁的過程

  • 首先,SQL 進來時,判斷數(shù)據(jù)對應(yīng)的數(shù)據(jù)頁能否在數(shù)據(jù)頁緩存哈希表里找到對應(yīng)的緩存頁。如果找到,將直接在 Buffer Pool 中進行增刪改查。
  • 如果找不到,則從 free 鏈表中找到一個空閑的緩存頁,然后從磁盤文件中讀取對應(yīng)的數(shù)據(jù)頁的數(shù)據(jù)到緩存頁中,并且將數(shù)據(jù)頁的信息和緩存頁的地址寫入到對應(yīng)的描述數(shù)據(jù)塊中,然后修改相關(guān)的描述數(shù)據(jù)塊的 free_pre 指針和 free_next 指針,將使用了的描述數(shù)據(jù)塊從 free 鏈表中移除。
  • 記得,還要在數(shù)據(jù)頁緩存哈希表中寫入對應(yīng)的 key-value 對。最后也是在 Buffer Pool 中進行增刪改查。

3、LRU 鏈表和Flush鏈表的區(qū)別

LRU鏈表:

  • LRU 鏈表 ,由用戶線程觸發(fā)(MySQL 5.6.2之前);
  • LRU 鏈表,其目的是為了寫出LRU 鏈表尾部的臟頁,釋放足夠的空閑頁,當Buffer Pool滿的時候,用戶可以立即獲得空閑頁面,而不需要長時間等待;
  • LRU 鏈表,其寫出的臟頁,需要從LRU鏈表中刪除,移動到Free 鏈表。
  • Flush 鏈表由MySQL數(shù)據(jù)庫InnoDB存儲引擎后臺srv_master線程處理。(在MySQL 5.6.2之后,都被遷移到Page Cleaner線程中)。
  • LRU 鏈表 flush,每次flush的臟頁數(shù)量較少,基本固定,只要釋放一定的空閑頁即可;

Flush鏈表:

  • Flush 鏈表,其目的是推進Checkpoint LSN,使得InnoDB系統(tǒng)崩潰之后能夠快速的恢復(fù)。
  • Flush List flush,不需要移動page在LRU鏈表中的位置。
  • Flush 鏈表 flush,根據(jù)當前系統(tǒng)的更新繁忙程度,動態(tài)調(diào)整一次flush的臟頁數(shù)量,量很大。
  • 在Flush 鏈表上的頁面一定在LRU 鏈表上,反之則不成立。

4、觸發(fā)刷臟頁的條件

  • REDO日志快用滿的時候。由于MySQL更新是先寫REDO日志,后面再將數(shù)據(jù)Flush到磁盤,如果REDO日志對應(yīng)臟數(shù)據(jù)還沒有刷新到磁盤就被覆蓋的話,萬一發(fā)生Crash,數(shù)據(jù)就無法恢復(fù)了。此時會從Flush 鏈表里面選取臟頁,進行Flush。
  • 為了保證MySQL中的空閑頁面的數(shù)量,Page Cleaner線程會從LRU 鏈表尾部淘汰一部分頁面作為空閑頁。如果對應(yīng)的頁面是臟頁的話,就需要先將頁面Flush到磁盤。
  • MySQL中臟頁太多的時候。innodb_max_dirty_pages_pct 表示的是Buffer Pool最大的臟頁比例,默認值是75%,當臟頁比例大于這個值時會強制進行刷臟頁,保證系統(tǒng)有足夠可用的Free Page。innodb_max_dirty_pages_pct_lwm參數(shù)控制的是臟頁比例的低水位,當達到該參數(shù)設(shè)定的時候,會進行preflush,避免比例達到innodb_max_dirty_pages_pct 來強制Flush,對MySQL實例產(chǎn)生影響。
  • MySQL實例正常關(guān)閉的時候,也會觸發(fā)MySQL把內(nèi)存里面的臟頁全部刷新到磁盤。

Innodb 的策略是在運行過程中盡可能的多占用內(nèi)存,因此未被使用的頁面會很少。當我們讀取的數(shù)據(jù)不在Buffer Pool里面時,就需要申請一個空閑頁來存放。如果沒有足夠的空閑頁時,就必須從LRU 鏈表的尾部淘汰頁面。如果該頁面是干凈的,可以直接拿來用,如果是臟頁,就需要進行刷臟操作,將內(nèi)存數(shù)據(jù)Flush到磁盤。

所以,如果出現(xiàn)以下情況,是很容易影響MySQL實例的性能:

  • 一個SQL查詢的數(shù)據(jù)頁需要淘汰的頁面過多
  • 實例是個寫多型的MySQL,checkpoint跟不上日志產(chǎn)生量,會導(dǎo)致更新全部堵塞,TPS跌0。

innodb_io_capacity 參數(shù)定義了Innodb 后臺任務(wù)的IO能力,例如刷臟操作還有Change Buffer的merge操作等。

Innodb 的三種Page和鏈表的設(shè)計,保證了我們需要的熱數(shù)據(jù)常駐在內(nèi)存,及時淘汰不需要的數(shù)據(jù),提升了我們的查詢速度,同時不同的刷臟策略也提高了我們的恢復(fù)速度,保證了數(shù)據(jù)安全。

到此這篇關(guān)于mysql的Buffer Pool存儲及原理的文章就介紹到這了,更多相關(guān)mysql的Buffer Pool原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論