一、前言
1、buffer pool是什么
咱們在使用mysql的時候,比如很簡單的select * from table;這條語句,具體查詢數據其實是在存儲引擎中實現的,大家都知道mysql數據其實是放在磁盤里面的,如果每次查詢都直接從磁盤里面查詢,這樣勢必會很影響性能,所以一定是先把數據從磁盤中取出,然后放在內存中,下次查詢直接從內存中來取。但是一台機器中往往不是只有mysql一個進程在運行的,很多個進程都需要使用內存,所以mysql中會有一個專門的區域來處理這些數據,這個專門為mysql准備的區域,就叫buffer pool。
buffer pool是mysql一個非常關鍵的核心組件。數據庫中的數據實際上最終都是要存放在磁盤文件上的,如下圖所示
但是我們在對數據庫執行增刪改操作的時候,不可能直接更新磁盤上的數據的,因為如果你對磁盤進行隨機讀寫操作,那速度是相當的慢,隨便一個大磁盤文件的隨機讀寫操作,可能都要幾百毫秒。如果要是那么搞的話,可能你的數據庫每秒也就只能處理幾百個請求了! 在對數據庫執行增刪改操作的時候,實際上主要都是針對內存里的Buffer Pool中的數據進行的,也就是實際上主要是對數據庫的內存里的數據結構進行了增刪改,如下圖所示
其實每個人都擔心一個事,就是你在數據庫的內存里執行了一堆增刪改的操作,內存數據是更新了,但是這個時候如果數據庫突然崩潰了,那么內存里更新好的數據不是都沒了嗎? MySQL就怕這個問題,所以引入了一個redo log機制,你在對內存里的數據進行增刪改的時候,他同時會把增刪改對應的日志寫入redo log中,如下圖
萬一你的數據庫突然崩潰了,沒關系,只要從redo log日志文件里讀取出來你之前做過哪些增刪改操作,瞬間就可以重新把這些增刪改操作在你的內存里執行一遍,這就可以恢復出來你之前做過哪些增刪改操作了。 當然對於數據更新的過程,他是有一套嚴密的步驟的,還涉及到undo log、binlog、提交事務、buffer pool臟數據刷回磁盤,等等。
Buffer Pool就是數據庫的一個內存組件,里面緩存了磁盤上的真實數據,然后我們的系統對數據庫執行的增刪改操作,其實主要就是對這個內存數據結構中的緩存數據執行的。通過這種方式,保證每個更新請求,盡量就是只更新內存,然后往磁盤順序寫日志文件。
更新內存的性能是極高的,然后順序寫磁盤上的日志文件的性能也是比較高的,因為順序寫磁盤文件,他的性能要遠高於隨機讀寫磁盤文件。
2、buffer pool的工作流程
咱們以查詢語句為例
1:在查詢的時候會先去buffer pool(內存)中看看有沒有對應的數據頁,如果有的話直接返回
2:如果buffer pool中沒有對應的數據頁,則會去磁盤中查找,磁盤中如果找到了對應的數據,則會把該頁的數據直接copy一份到buffer pool中返回給客戶端
3:下次有同樣的查詢進來直接查找buffer pool找到對應的數據返回即可。
大家看到這里相信應該對buffer pool有了個大概的認識,有沒有感覺有點緩存的感覺,當然buffer pool可沒有緩存那么簡單,內部結構還是比較復雜的,不過沒關系,咱們繼續往下看。
3、buffer pool緩沖池和查詢緩存(query cache)
在正式講解buffer pool 之前,我們先搞清楚buffer pool緩沖池和查詢緩存(query cache)簡稱Qcache的區別。
如果將Mysql分為Server層和存儲引擎層兩大部分,那么Qcache位於Server層,Buffer Pool位於存儲引擎層。
如果你的Mysql 查詢緩存功能是打開的,那么當一個sql進入Mysql Server之后,Mysql Server首先會從查詢緩存中查看是否曾經執行過這個SQL,如果曾經執行過的話,曾經執行的查詢結果之前會以key-value的形式
保存在查詢緩存中。key是sql語句,value是查詢結果。我們將這個過程稱為查詢緩存!
如果查詢緩存中沒有你要找的數據的話,MySQL才會執行后續的邏輯,通過存儲引擎將數據檢索出來。並且查詢緩存會被shared cache for sessions,是的,它會被所有的session共享。
MySQL查詢緩存是查詢結果緩存。它將以SEL開頭的查詢與哈希表進行比較,如果匹配,則返回上一次查詢的結果。進行匹配時,查詢必須逐字節匹配,例如 SELECT * FROM t1; 不等於select * from t1;,此外,一些不確定的查詢結果無法被緩存,任何對表的修改都會導致這些表的所有緩存無效(只要有一個sql update了該表,那么表的查詢緩存就會失效)。因此,適用於查詢緩存的最理想的方案是只讀,特別是需要檢查數百萬行后僅返回數行的復雜查詢。如果你的查詢符合這樣一個特點,開啟查詢緩存會提升你的查詢性能。
MySQL查詢緩存的目的是為了提升查詢性能,但它本身也是有性能開銷的。需要在合適的業務場景下(讀寫壓力模型)使用,不合適的業務場景不但不能提升查詢性能,查詢緩存反而會變成MySQL的瓶頸。
查詢緩存的開銷主要有:
- 讀查詢在開始前必須先檢查是否命中緩存;
- 如果這個讀查詢可以被緩存,那么當完成執行后,MySQL若發現查詢緩存中沒有這個查詢,會將其結果存入查詢緩存,這會帶來額外的系統消耗;
- 當向某個表寫入數據的時候,MySQL必須將對應表的所有緩存都設置失效。如果查詢緩存非常大或者碎片很多,這個操作就可能帶來很大的系統消耗。
查詢緩存的缺點:
首先,查詢緩存的效果取決於緩存的命中率,只有命中緩存的查詢效果才能有改善,因此無法預測其性能。只要有一個sql update了該表,那么表的查詢緩存就會失效,所以當你的業務對表CRUD的比例不相上下,那么查詢緩存may be會影響應用的吞吐效率。
其次,查詢緩存的另一個大問題是它受到單個互斥鎖的保護。在具有多個內核的服務器上,大量查詢會導致大量的互斥鎖爭用。
在mysql8.0的版本中,已經將查詢緩存模塊刪除了。
二、buffer pool的內存數據結構
1、數據頁概念
我們先了解一下數據頁這個概念。它是 MySQL 抽象出來的數據單位,磁盤文件中就是存放了很多數據頁,每個數據頁里存放了很多行數據。
默認情況下,數據頁的大小是 16kb。
所以對應的,在 Buffer Pool
中,也是以數據頁為數據單位,存放着很多數據。但是我們通常叫做緩存頁,因為 Buffer Pool
畢竟是一個緩沖池,並且里面的數據都是從磁盤文件中緩存到內存中。
所以,默認情況下緩存頁的大小也是 16kb,因為它和磁盤文件中數據頁是一一對應的。
所以,緩沖池和磁盤之間的數據交換的單位是數據頁,Buffer Pool中存放的是一個一個的數據頁。
假設我們要更新一行數據,此時數據庫會找到這行數據所在的數據頁,然后從磁盤文件里把這行數據所在的數據頁直接給加載到Buffer Pool里去。如下圖。
2、那么怎么識別數據在哪個緩存頁中
每個緩存頁都會對應着一個描述數據塊,里面包含數據頁所屬的表空間、數據頁的編號,緩存頁在 Buffer Pool
中的地址等等。
描述數據塊本身也是一塊數據,它的大小大概是緩存頁大小的5%左右,大概800個字節左右的大小。然后假設你設置的buffer pool大小是128MB,實際上Buffer Pool真正的最終大小會超出一些,可能有個130多MB的樣子,因為他里面還要存放每個緩存頁的描述數據。
在Buffer Pool中,每個緩存頁的描述數據放在最前面,然后各個緩存頁放在后面。所以此時我們看下面的圖,Buffer Pool實際看起來大概長這個樣子 。
3、buffer pool的初始化與配置
MySQL 啟動時,是如何初始化 Buffer Pool
的呢?
1、MySQL 啟動時,會根據參數 innodb_buffer_pool_size
的值來為 Buffer Pool
分配內存區域。
2、然后會按照緩存頁的默認大小 16k 以及對應的描述數據塊的 800個字節 左右大小,在 Buffer Pool
中划分中一個個的緩存頁和一個個的描述數據庫塊。
3、注意,此時的緩存頁和描述數據塊都是空的,畢竟才剛啟動 MySQL 呢。
buffer pool的配置
buffer pool通常由數個內存塊加上一組控制結構體對象組成。內存塊的個數取決於buffer pool instance的個數,不過在5.7版本中開始默認以128M(可配置)的chunk單位分配內存塊,這樣做的目的是為了支持buffer pool的在線動態調整大小。
Buffer Pool默認情況下是128MB,還是有一點偏小了,我們實際生產環境下完全可以對Buffer Pool進行調整。 比如我們的數據庫如果是16核32G的機器,那么你就可以給Buffer Pool分配個2GB的內存。
1、innodb_buffer_pool_size:這個值是設置 InnoDB Buffer Pool 的總大小;
2、innodb_buffer_pool_chunk_size:當增加或減少innodb_buffer_pool_size時,操作以塊(chunk)形式執行。塊大小由innodb_buffer_pool_chunk_size配置選項定義,默認值128M。
這里面有個關系要確定一下,最好按照這個設置 innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances*N(N>=1);
3、innodb_buffer_pool_instances:設置 InnoDB Buffer Pool 實例的個數,每一個實例都有自己獨立的 list 管理Buffer Pool;
當buffer pool比較大的時候(超過1G),innodb會把buffer pool划分成幾個instances,這樣可以提高讀寫操作的並發,減少競爭。讀寫page都使用hash函數分配給一個instances。
當增加或者減少buffer pool大小的時候,實際上是操作的chunk。buffer pool的大小必須是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的整數倍,如果配置的innodb_buffer_pool_size不是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數,buffer pool的大小會自動調整為innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數,自動調整的值不少於指定的值。
如果指定的buffer大小是9G,instances的個數是16,chunk默認的大小是128M,那么buffer會自動調整為10G。
4、innodb_old_blocks_pct:默認 InnoDB Buffer Pool 中點的位置,默認值是37,最大100,也就是我們所謂的3/8的位置,可以自己設置。
3.1、Buffer Pool Size 設置和生效過程
理想情況下,在給服務器的其他進程留下足夠的內存空間的情況下,Buffer Pool Size 應該設置的盡可能大。當 Buffer Pool Size 設置的足夠大時,整個數據庫就相當於存儲在內存當中,當讀取一次數據到 Buffer Pool Size 以后,后續的讀操作就不用再訪問磁盤。
下面我們看一下 Buffer Pool Size 的設置方式:
當數據庫已經啟動的情況下,我們可以通過在線調整的方式修改 Buffer Pool Size 的大小。通過以下語句:
SET GLOBAL innodb_buffer_pool_size=402653184;
當執行這個語句以后,並不會立即生效,而是要等所有的事務全部執行成功以后才會生效;新的連接和事務必須等其他事務完全執行成功以后,Buffer Pool Size 設置生效以后才能夠連接成功,不然會一直處於等待狀態。
期間,Buffer Pool Size 要完成碎片整理,去除緩存 page 等等操作。在執行增加或者減少 Buffer Pool Size 的操作時,操作會作為一個執行塊執行,innodb_buffer_pool_chunk_size 的大小會定義一個執行塊的大小,默認的情況下,這個值是128M。
Buffer Pool Size 的大小最好設置為 innodb_buffer_pool_chunk_size innodb_buffer_pool_instances 的整數倍,而且是大於等於1。
如果你的機器配置的大小不是整數倍的話,Buffer Pool Size 的大小是會自適應修改為 innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances 的整數倍,會略小於你配置的 Buffer Pool Size 的大小。
比如以8G為例:
mysqld --innodb_buffer_pool_size=8G --innodb_buffer_pool_instances=16,然后innodb_buffer_pool_instances=16 的大小剛好設置為16,是一個整數倍的關系。而且innodb_buffer_pool_chunk_size 的大小也是可以在my.cnf里面指定的。
還有一種情況是 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 大於 buffer pool size 的情況下,innodb_buffer_pool_chunk_size 也會自適應為 Buffer Pool size/innodb_buffer_pool_instances,可見MySQL 的管理還是非常的智能的。
如果我們要查 Buffer Pool 的狀態的話:
SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status'
可以幫我們查看到狀態。我們可以看一下增加 Buffer Pool 的時候的一個過程,再看一下減少的時候的日志,其實還是很好理解的,我們可以看成每次增大或者減少 Buffer Pool 的時候就是進行 innodb_buffer_pool_chunk 的增加或者釋放,按照 innodb_buffer_pool_chunk_size 設定值的大小增加或者釋放執行塊。
增加的過程:增加執行塊,指定新地址,將新加入的執行塊加入到 free list(控制執行塊的一個列表,可以這么理解)。
減少的過程:重新整理 Buffer Pool 和空閑頁,將數據從塊中移除,指定新地址。
3.2、Buffer Pool Instances
在64位操作系統的情況下,可以拆分緩沖池成多個部分,這樣可以在高並發的情況下最大可能的減少爭用。下面我們看一下怎么配置 Buffer Pool Instances?
配置多個 Buffer Pool Instances 能在很大程度上能夠提高 MySQL 在高並發的情況下處理事物的性能,優化不同連接讀取緩沖頁的爭用。
我們可以通過設置 innodb_buffer_pool_instances 來設置 Buffer Pool Instances。當 InnoDB Buffer Pool 足夠大的時候,你能夠從內存中讀取時候能有一個較好的性能,但是也有可能碰到多個線程同時請求緩沖池的瓶頸。這個時候設置多個 Buffer Pool Instances 能夠盡量減少連接的爭用。
這能夠保證每次從內存讀取的頁都對應一個 Buffer Pool Instances,而且這種對應關系是一個隨機的關系。並不是熱數據存放在一個 Buffer Pool Instances下,內部也是通過 hash 算法來實現這個隨機數的。每一個 Buffer Pool Instances 都有自己的 free lists,LRU 和其他的一些 Buffer Poll 的數據結構,各個 Buffer Pool Instances 是相對獨立的。
innodb_buffer_pool_instances 的設置必須大於1才算得上是多配置,但是這個功能起作用的前提是innodb_buffer_pool_size 的大小必須大於1G,理想情況下 innodb_buffer_pool_instances 的每一個 instance 都保證在1G以上。
3.3、SHOW ENGINE INNODB STATUS
當你的數據庫啟動之后,你隨時可以通過上述命令,去查看當前innodb里的一些具體情況,執行SHOW ENGINE INNODB STATUS就可以了。此時你可能會看到如下一系列的東西:
Total memory allocated xxxx; Dictionary memory allocated xxx Buffer pool size xxxx Free buffers xxx Database pages xxx Old database pages xxxx Modified db pages xx Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young xxxx, not young xxx xx youngs/s, xx non-youngs/s Pages read xxxx, created xxx, written xxx xx reads/s, xx creates/s, 1xx writes/s Buffer pool hit rate xxx / 1000, young-making rate xxx / 1000 not xx / 1000 Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: xxxx, unzip_LRU len: xxx I/O sum[xxx]:cur[xx], unzip sum[16xx:cur[0]
下面解釋一下這里的東西,主要講解這里跟buffer pool相關的一些東西。
Total memory allocated,這就是說buffer pool最終的總大小是多少
Buffer pool size,這就是說buffer pool一共能容納多少個緩存頁
Free buffers,這就是說free鏈表中一共有多少個空閑的緩存頁是可用的
Database pages和Old database pages,就是說lru鏈表中一共有多少個緩存頁,以及冷數據區域里的緩存頁數量
Modified db pages,這就是flush鏈表中的緩存頁數量
Pending reads和Pending writes,等待從磁盤上加載進緩存頁的數量,還有就是即將從lru鏈表中刷入磁盤的數量、即將從flush鏈表中刷入磁盤的數量
Pages made young和not young,這就是說已經lru冷數據區域里訪問之后轉移到熱數據區域的緩存頁的數 量,以及在lru冷數據區域里1s內被訪問了沒進入熱數據區域的緩存頁的數量
youngs/s和not youngs/s,這就是說每秒從冷數據區域進入熱數據區域的緩存頁的數量,以及每秒在冷數據區域里被訪問了但是不能進入熱數據區域的緩存頁的數量
Pages read xxxx, created xxx, written xxx,xx reads/s, xx creates/s, 1xx writes/s,這里就是說已經讀取、創建和寫入了多少個緩存頁,以及每秒鍾讀取、創建和寫入的緩存頁數量
Buffer pool hit rate xxx / 1000,這就是說每1000次訪問,有多少次是直接命中了buffer pool里的緩存的
young-making rate xxx / 1000 not xx / 1000,每1000次訪問,有多少次訪問讓緩存頁從冷數據區域移動到了熱數據區域,以及沒移動的緩存頁數量
LRU len:這就是lru鏈表里的緩存頁的數量
I/O sum:最近50s讀取磁盤頁的總數
I/O cur:現在正在讀取磁盤頁的數量
三、buffer pool的空間管理
緩沖池也是有大小限制的,那么既然緩沖池有大小限制的,每次都讀入的數據頁怎么來管理呢?這里我們來聊聊緩沖池的空間管理,其實對緩沖池進行管理的關鍵部分是如何安排進池的數據並且按照一定的策略淘汰池中的數據,保證池中的數據不“溢出”,同時還能保證常用數據留在池子中。
1、傳統 LRU 淘汰法
緩沖池是基於傳統的 LRU 方法來進行緩存頁管理的,我們先來看下如果使用 LRU 是如何管理的。
LRU,全稱是 Least Recently Used,中文名字叫作「最近最少使用」。從名字上就很容易理解了。
這里分兩種情況:
1.1、緩存頁已在緩沖池中
這種情況下會將對應的緩存頁放到 LRU 鏈表的頭部,無需從磁盤再進行讀取,也無需淘汰其它緩存頁。
如下圖所示,如果要訪問的數據在 6 號頁中,則將 6 號頁放到鏈表頭部即可,這種情況下沒有緩存頁被淘汰。
1.2、緩存頁不在緩沖池中
緩存頁不在緩沖中,這時候就需要從磁盤中讀入對應的數據頁,將其放置在鏈表頭部,同時淘汰掉末尾的緩存頁
如下圖所示,如果要訪問的數據在 60 號頁中,60 號頁不在緩沖池中,此時加載進來放到鏈表的頭部,同時淘汰掉末尾的 17 號緩存頁。
是不是看上去很簡單,同時也能滿足緩沖池淘汰緩存頁的方法?但是我們來思考幾個問題:
預讀失效
上面我們提到了緩沖池的預讀機制可能會預先加載相鄰的數據頁。假如加載了 20、21 相鄰的兩個數據頁,如果只有頁號為 20 的緩存頁被訪問了,而另一個緩存頁卻沒有被訪問。此時兩個緩存頁都在鏈表的頭部,但是為了加載這兩個緩存頁卻淘汰了末尾的緩存頁,而被淘汰的緩存頁卻是經常被訪問的。這種情況就是預讀失效,被預先加載進緩沖池的頁,並沒有被訪問到,這種情況是不是很不合理。
緩沖池污染
還有一種情況是當執行一條 SQL 語句時,如果掃描了大量數據或是進行了全表掃描,此時緩沖池中就會加載大量的數據頁,從而將緩沖池中已存在的所有頁替換出去,這種情況同樣是不合理的。這就是緩沖池污染,並且還會導致 MySQL 性能急劇下降。
2、冷熱數據分離
這樣看來,傳統的 LRU 方法並不能滿足緩沖池的空間管理。因此,Msyql 基於 LRU 設計了冷熱數據分離的處理方案。
也就是將 LRU 鏈表分為兩部分,一部分為熱數據區域,一部分為冷數據區域。
當數據頁第一次被加載到緩沖池中的時候,先將其放到冷數據區域的鏈表頭部,1s(由 innodb_old_blocks_time 參數控制) 后該緩存頁被訪問了再將其移至熱數據區域的鏈表頭部。
可能你會有疑惑了,為什么要等 1s 后才將其移至熱數據區域呢?你想想,如果數據頁剛被加載到冷數據區就被訪問了,之后再也不訪問它了呢?這不就造成熱數據區的浪費了嗎?要是 1s 后不訪問了,說明之后可能也不會去頻繁訪問它,也就沒有移至熱緩沖區的必要了。當緩存頁不夠的時候,從冷數據區淘汰它們就行了。
另一種情況,當我的數據頁已經在熱緩沖區了,是不是緩存頁只要被訪問了就將其插到鏈表頭部呢?不用我說你肯定也覺得不合理。熱數據區域里的緩存頁是會被經常訪問的,如果每訪問一個緩存頁就插入一次鏈表頭,那整個熱緩沖區里就異常騷動了,你想想那個畫面。
那咋整呢?Mysql 中優化為熱數據區的后 3/4 部分被訪問后才將其移動到鏈表頭部去,對於前 1/4 部分的緩存頁被訪問了不會進行移動。
四、Buffer Pool預讀機制
預讀是mysql提高性能的一個重要的特性。預讀就是 IO 異步讀取多個頁數據讀入 Buffer Pool 的一個過程,並且這些頁被認為是很快就會被讀取到的。InnoDB使用兩種預讀算法來提高I/O性能:線性預讀(linear read-ahead)和隨機預讀(randomread-ahead)
為了區分這兩種預讀的方式,我們可以把線性預讀放到以extent為單位,而隨機預讀放到以extent中的page為單位。線性預讀着眼於將下一個extent提前讀取到buffer pool中,而隨機預讀着眼於將當前extent中的剩余的page提前讀取到buffer pool中。
Linear線性預讀
線性預讀的單位是extend,一個extend中有64個page。線性預讀的一個重要參數是innodb_read_ahead_threshold,是指在連續訪問多少個頁面之后,把下一個extend讀入到buffer pool中,不過預讀是一個異步的操作。當然這個參數不能超過64,因為一個extend最多只有64個頁面。
例如,innodb_read_ahead_threshold = 56,就是指在連續訪問了一個extend的56個頁面之后把下一個extend讀入到buffer pool中。在添加此參數之前,InnoDB僅計算當它在當前范圍的最后一頁中讀取時是否為整個下一個范圍發出異步預取請求。
Random隨機預讀
隨機預讀方式則是表示當同一個extent中的一些page在buffer pool中發現時,Innodb會將該extent中的剩余page一並讀到buffer pool中。由於隨機預讀方式給innodb code帶來了一些不必要的復雜性,同時在性能也存在不穩定性,在5.5中已經將這種預讀方式廢棄,默認是OFF。若要啟用此功能,即將配置變量設置innodb_random_read_ahead為ON。
五、Buffer Pool的三種Page和鏈表
Buffer Pool 是Innodb 內存中的的一塊占比較大的區域,用來緩存表和索引數據。眾所周知,從內存訪問會比從磁盤訪問快很多。為了提高數據的讀取速度,Buffer Pool 會通過三種Page 和鏈表來管理這些經常訪問的數據,保證熱數據不被置換出Buffer Pool。
1、三種Page
1.1Free Page(空閑頁)
表示此Page 未被使用,位於 Free 鏈表。
1.2. Clean Page(干凈頁)
此Page 已被使用,但是頁面未發生修改,位於LRU 鏈表。
1.3. Dirty Page(臟頁)
此Page 已被使用,頁面已經被修改,其數據和磁盤上的數據已經不一致。當臟頁上的數據寫入磁盤后,內存數據和磁盤數據一致,那么該Page 就變成了干凈頁。臟頁 同時存在於LRU 鏈表和Flush 鏈表。
2、三種鏈表
2.1、LRU 鏈表
如上圖所示,是Buffer Pool里面的LRU(least recently used)鏈表。LRU鏈表是被一種叫做最近最少使用的算法管理。
LRU鏈表被分成兩部分,一部分是New Sublist(Young 鏈表),用來存放經常被讀取的頁的地址,另外一部分是Old Sublist(Old 鏈表),用來存放較少被使用的頁面。每部分都有對應的頭部 和尾部。
默認情況下
- Old 鏈表占整個LRU 鏈表的比例是3/8。該比例由innodb_old_blocks_pct控制,默認值是37(3/8*100)。該值取值范圍為5~95,為全局動態變量。
- 當新的頁被讀取到Buffer Pool里面的時候,和傳統的LRU算法插入到LRU鏈表頭部不同,Innodb LRU算法是將新的頁面插入到Yong 鏈表的尾部和Old 鏈表的頭部中間的位置,這個位置叫做Mid Point,如上圖所示。
- 頻繁訪問一個Buffer Pool的頁面,會促使頁面往Young鏈表的頭部移動。如果一個Page在被讀到Buffer Pool后很快就被訪問,那么該Page會往Young List的頭部移動,但是如果一個頁面是通過預讀的方式讀到Buffer Pool,且之后短時間內沒有被訪問,那么很可能在下次訪問之前就被移動到Old List的尾部,而被驅逐了。
- 隨着數據庫的持續運行,新的頁面被不斷的插入到LRU鏈表的Mid Point,Old 鏈表里的頁面會逐漸的被移動Old鏈表的尾部。同時,當經常被訪問的頁面移動到LRU鏈表頭部的時候,那些沒有被訪問的頁面會逐漸的被移動到鏈表的尾部。最終,位於Old 鏈表尾部的頁面將被驅逐。
如果一個數據頁已經處於Young 鏈表,當它再次被訪問的時候,只有當其處於Young 鏈表長度的1/4(大約值)之后,才會被移動到Young 鏈表的頭部。這樣做的目的是減少對LRU 鏈表的修改,因為LRU 鏈表的目標是保證經常被訪問的數據頁不會被驅逐出去。
innodb_old_blocks_time 控制的Old 鏈表頭部頁面的轉移策略。該Page需要在Old 鏈表停留超過innodb_old_blocks_time 時間,之后再次被訪問,才會移動到Young 鏈表。這么操作是避免Young 鏈表被那些只在innodb_old_blocks_time時間間隔內頻繁訪問,之后就不被訪問的頁面塞滿,從而有效的保護Young 鏈表。
在全表掃描或者全索引掃描的時候,Innodb會將大量的頁面寫入LRU 鏈表的Mid Point位置,並且只在短時間內訪問幾次之后就不再訪問了。設置innodb_old_blocks_time的時間窗口可以有效的保護Young List,保證了真正的頻繁訪問的頁面不被驅逐。
innodb_old_blocks_time 單位是毫秒,默認值是1000。調大該值提高了從Old鏈表移動到Young鏈表的難度,會促使更多頁面被移動到Old 鏈表,老化,從而被驅逐。
當掃描的表很大,Buffer Pool都放不下時,可以將innodb_old_blocks_pct設置為較小的值,這樣只讀取一次的數據頁就不會占據大部分的Buffer Pool。例如,設置innodb_old_blocks_pct = 5,會將僅讀取一次的數據頁在Buffer Pool的占用限制為5%。
當經常掃描一些小表時,這些頁面在Buffer Pool移動的開銷較小,我們可以適當的調大innodb_old_blocks_pct,例如設置innodb_old_blocks_pct = 50。
在SHOW ENGINE INNODB STATUS 里面提供了Buffer Pool一些監控指標,有幾個我們需要關注一下:
- youngs/s:該指標表示的是每秒訪問Old 鏈表中頁面,使其移動到Young鏈表的次數。如果MySQL實例都是一些小事務,沒有大表全掃描,且該指標很小,就需要調大innodb_old_blocks_pct 或者減小innodb_old_blocks_time,這樣會使得Old List 的長度更長,Old頁面被移動到Old List 的尾部消耗的時間會更久,那么就提升了下一次訪問到Old List里面的頁面的可能性。如果該指標很大,可以調小innodb_old_blocks_pct,同時調大innodb_old_blocks_time,保護熱數據。
- non-youngs/s:該指標表示的是每秒訪問Old 鏈表中頁面,沒有移動到Young鏈表的次數,因為其不符合innodb_old_blocks_time。如果該指標很大,一般情況下是MySQL存在大量的全表掃描。如果MySQL存在大量全表掃描,且這個指標又不大的時候,需要調大innodb_old_blocks_time,因為這個指標不大意味着全表掃描的頁面被移動到Young 鏈表了,調大innodb_old_blocks_time時間會使得這些短時間頻繁訪問的頁面保留在Old 鏈表里面。
每隔1秒鍾,Page Cleaner線程執行LRU List Flush的操作,來釋放足夠的Free Page。innodb_lru_scan_depth 變量控制每個Buffer Pool實例每次掃描LRU List的長度,來尋找對應的臟頁,執行Flush操作。
2.2、Flush 鏈表
- Flush 鏈表里面保存的都是臟頁,也會存在於LRU 鏈表。
- Flush 鏈表是按照oldest_modification排序,值大的在頭部,值小的在尾部
- 當有頁面訪被修改的時候,使用mini-transaction,對應的page進入Flush 鏈表
- 如果當前頁面已經是臟頁,就不需要再次加入Flush list,否則是第一次修改,需要加入Flush 鏈表
- 當Page Cleaner線程執行flush操作的時候,從尾部開始scan,將一定的臟頁寫入磁盤,推進檢查點,減少recover的時間
SQL 的增刪改查都在 Buffer Pool
中執行,慢慢地,Buffer Pool
中的緩存頁因為不斷被修改而導致和磁盤文件中的數據不一致了,也就是 Buffer Pool
中會有很多個臟頁,臟頁里面很多臟數據。
所以,MySQL 會有一條后台線程,定時地將 Buffer Pool
中的臟頁刷回到磁盤文件中。
但是,后台線程怎么知道哪些緩存頁是臟頁呢,不可能將全部的緩存頁都往磁盤中刷吧,這會導致 MySQL 暫停一段時間。
2、MySQL 是怎么判斷臟頁的
我們引入一個和 free 鏈表類似的 flush 鏈表。他的本質也是通過緩存頁的描述數據塊中的兩個指針,讓修改過的緩存頁的描述數據塊能串成一個雙向鏈表,這兩指針大家可以認為是 flush_pre 指針和 flush_next 指針。
下面我用偽代碼來描述一下:
DescriptionDataBlock{ block_id = block1; // free 鏈表的 free_pre = null; free_next = null; // flush 鏈表的 flush_pre = null; flush_next = block2; }
flush 鏈表也有對應的基礎節點,也是包含鏈表的頭節點和尾節點,還有就是修改過的緩存頁的數量。
FlushListBaseNode{ start = block1; end = block2; count = 2; }
到這里,我們都知道,SQL 的增刪改都會使得緩存頁變為臟頁,此時會修改臟頁對應的描述數據塊的 flush_pre 指針和 flush_next 指針,使得描述數據塊加入到 flush 鏈表中,之后 MySQL 的后台線程就可以將這個臟頁刷回到磁盤中。
2.3、Free 鏈表
- Free 鏈表 存放的是空閑頁面,初始化的時候申請一定數量的頁面,當 MySQL 啟動后,會不斷地有 SQL 請求進來,此時空先的緩存頁就會不斷地被使用。
- 在執行SQL的過程中,每次成功load 頁面到內存后,會判斷Free 鏈表的頁面是否夠用。如果不夠用的話,就flush LRU 鏈表和Flush 鏈表來釋放空閑頁。如果夠用,就從Free 鏈表里面刪除對應的頁面,在LRU 鏈表增加頁面,保持總數不變。
Free 鏈表的使用原理
free 鏈表,它是一個雙向鏈表,鏈表的每個節點就是一個個空閑的緩存頁對應的描述數據塊。
他本身其實就是由 Buffer Pool
里的描述數據塊組成的,你可以認為是每個描述數據塊里都有兩個指針,一個是 free_pre 指針,一個是 free_next 指針,分別指向自己的上一個 free 鏈表的節點,以及下一個 free 鏈表的節點。
通過 Buffer Pool
中的描述數據塊的 free_pre 和 free_next 兩個指針,就可以把所有的描述數據塊串成一個 free 鏈表。
下面我們可以用偽代碼來描述一下 free 鏈表中描述數據塊節點的數據結構:
DescriptionDataBlock{ block_id = block1; free_pre = null; free_next = block2; }
free 鏈表有一個基礎節點,他會引用鏈表的頭節點和尾節點,里面還存儲了鏈表中有多少個描述數據塊的節點,也就是有多少個空閑的緩存頁。
下面我們也用偽代碼來描述一下基礎節點的數據結構:
FreeListBaseNode{ start = block01; end = block03; count = 2; }
到此,free 鏈表就介紹完了。上面我們也介紹了 MySQL 啟動時 Buffer Pool
的初始流程,接下來,我會將結合剛介紹完的 free 鏈表,講解一下 SQL 進來時,磁盤數據頁讀取到 Buffer Pool
的緩存頁的過程。但是,我們先要了解一下一個新概念:數據頁緩存哈希表,它的 key 是表空間+數據頁號,而 value 是對應緩存頁的地址。
描述如圖所示:
磁盤數據頁讀取到 Buffer Pool
的緩存頁的過程
1、首先,SQL 進來時,判斷數據對應的數據頁能否在 數據頁緩存哈希表里 找到對應的緩存頁。
2、如果找到,將直接在 Buffer Pool
中進行增刪改查。
3、如果找不到,則從 free 鏈表中找到一個空閑的緩存頁,然后從磁盤文件中讀取對應的數據頁的數據到緩存頁中,並且將數據頁的信息和緩存頁的地址寫入到對應的描述數據塊中,然后修改相關的描述數據塊的 free_pre 指針和 free_next 指針,將使用了的描述數據塊從 free 鏈表中移除。記得,還要在數據頁緩存哈希表中寫入對應的 key-value 對。最后也是在 Buffer Pool
中進行增刪改查。
3、LRU 鏈表和Flush鏈表的區別
- LRU 鏈表 flush,由用戶線程觸發(MySQL 5.6.2之前);而Flush 鏈表 flush由MySQL數據庫InnoDB存儲引擎后台srv_master線程處理。(在MySQL 5.6.2之后,都被遷移到Page Cleaner線程中)。
- LRU 鏈表 flush,其目的是為了寫出LRU 鏈表尾部的臟頁,釋放足夠的空閑頁,當Buffer Pool滿的時候,用戶可以立即獲得空閑頁面,而不需要長時間等待;Flush 鏈表 flush,其目的是推進Checkpoint LSN,使得InnoDB系統崩潰之后能夠快速的恢復。
- LRU 鏈表 flush,其寫出的臟頁,需要從LRU鏈表中刪除,移動到Free 鏈表。Flush List flush,不需要移動page在LRU鏈表中的位置。
- LRU 鏈表 flush,每次flush的臟頁數量較少,基本固定,只要釋放一定的空閑頁即可;Flush 鏈表 flush,根據當前系統的更新繁忙程度,動態調整一次flush的臟頁數量,量很大。
- 在Flush 鏈表上的頁面一定在LRU 鏈表上,反之則不成立。
4、觸發刷臟頁的條件
- REDO日志快用滿的時候。由於MySQL更新是先寫REDO日志,后面再將數據Flush到磁盤,如果REDO日志對應臟數據還沒有刷新到磁盤就被覆蓋的話,萬一發生Crash,數據就無法恢復了。此時會從Flush 鏈表里面選取臟頁,進行Flush。
- 為了保證MySQL中的空閑頁面的數量,Page Cleaner線程會從LRU 鏈表尾部淘汰一部分頁面作為空閑頁。如果對應的頁面是臟頁的話,就需要先將頁面Flush到磁盤。
- MySQL中臟頁太多的時候。innodb_max_dirty_pages_pct 表示的是Buffer Pool最大的臟頁比例,默認值是75%,當臟頁比例大於這個值時會強制進行刷臟頁,保證系統有足夠可用的Free Page。innodb_max_dirty_pages_pct_lwm參數控制的是臟頁比例的低水位,當達到該參數設定的時候,會進行preflush,避免比例達到innodb_max_dirty_pages_pct 來強制Flush,對MySQL實例產生影響。
- MySQL實例正常關閉的時候,也會觸發MySQL把內存里面的臟頁全部刷新到磁盤。
Innodb 的策略是在運行過程中盡可能的多占用內存,因此未被使用的頁面會很少。當我們讀取的數據不在Buffer Pool里面時,就需要申請一個空閑頁來存放。如果沒有足夠的空閑頁時,就必須從LRU 鏈表的尾部淘汰頁面。如果該頁面是干凈的,可以直接拿來用,如果是臟頁,就需要進行刷臟操作,將內存數據Flush到磁盤。
所以,如果出現以下情況,是很容易影響MySQL實例的性能:
- 一個SQL查詢的數據頁需要淘汰的頁面過多
- 實例是個寫多型的MySQL,checkpoint跟不上日志產生量,會導致更新全部堵塞,TPS跌0。
innodb_io_capacity 參數定義了Innodb 后台任務的IO能力,例如刷臟操作還有Change Buffer的merge操作等。
Innodb 的三種Page和鏈表的設計,保證了我們需要的熱數據常駐在內存,及時淘汰不需要的數據,提升了我們的查詢速度,同時不同的刷臟策略也提高了我們的恢復速度,保證了數據安全。
參考文章:
https://www.cnblogs.com/wxlevel/p/12995324.html
https://www.cnblogs.com/Howinfun/p/12327490.html
https://www.jb51.net/article/204018.htm
https://cloud.tencent.com/developer/article/1396105
https://cloud.tencent.com/developer/article/1581267