前言
通過前邊的學習我們知道,對于使用InnoDB
作為存儲引擎的表來說,不管是用于存儲用戶數據的索引(包括聚簇索引和二級索引),還是各種系統(tǒng)數據,都是以頁的形式存放在表空間中的,而所謂的表空間只不過是InnoDB
對文件系統(tǒng)上一個或幾個實際文件的抽象,也就是說我們的數據說到底還是存儲在磁盤上的。但是各位也都知道,磁盤的速度慢的跟烏龜一樣,怎么能配得上“快如風,疾如電”的CPU呢?所以InnoDB存儲引擎在處理客戶端的請求時,當需要訪問某個頁的數據時,就會把完整的頁的數據全部加載到內存中,也就是說即使我們只需要訪問一個頁的一條記錄,那也需要先把整個頁的數據加載到內存中
。將整個頁加載到內存中后就可以進行讀寫訪問了,在進行完讀寫訪問之后并不著急把該頁對應的內存空間釋放掉,而是將其緩存起來,這樣將來有請求再次訪問該頁面時,就可以省去磁盤IO的開銷了。
一、InnoDB架構
如圖:
我們可以看出,InnoDB
分為了內存結構
和磁盤結構
兩大部分,Buffer Pool
是內存結構中最為重要且核心的組件,今天就來一起了解一下Buffer Pool
的工作原理。我們可以看到,內存結構中不僅有Buffer Pool
,還有Adaptive Hash Index
、Log Buffer
、Change Buffer
等等組件,后面會單獨開辟的文章單獨進行講解
官檔地址:https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
點擊此處跳轉
二、Buffer Pool
2.1 什么是緩沖池
官檔介紹: 緩沖池是InnoDB在訪問表和索引數據時緩存的主內存區(qū)域。緩沖池允許直接從內存訪問頻繁使用的數據,這加快了處理速度
從字面意思理解就是: MySQL InnoDB緩沖池,既然是緩沖池,那么它的作用就是緩存表數據與索引數據,把磁盤上的數據加載到緩沖池,避免每次訪問都進行磁盤IO,起到加速訪問的作用。
專業(yè)人士介紹: Buffer Pool是MySQL中最重要的內存組件,介于外部系統(tǒng)和存儲引擎之間的一個緩存區(qū),里面可以緩存磁盤上經常操作的真實數據,在執(zhí)行增刪改查操作時,先操作緩沖池中的數據(若緩沖池沒有數據,則從磁盤加載并緩存),然后再以一定頻率刷新到磁盤,從而減少磁盤 IO,加快處理速度。在緩沖池中不僅緩存了索引頁和數據頁,還包含了 undo 頁、插入緩存(insert page)、自適應哈希索引以及 InnoDB 的鎖信息等。
2.2 緩沖池大小的設置
緩沖池的配置通過變量innodb_buffer_pool_size
來設置,通常它的大小占用內存60%-80%,MySQL默認是134217728字節(jié),也就是128M。
mysql> show variables like '%innodb_buffer_pool_size%';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.01 sec)
我們可以通過set persist
命令設置緩沖池的大小
[root@mysql2 ~]# free -h
total used free shared buff/cache available
Mem: 15G 1.1G 12G 13M 1.4G 14G
Swap: 15G 0B 15G
15X0.7X1024X1024X1024=11274289152
mysql> set persist innodb_buffer_pool_size=11274289152;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%innodb_buffer_pool_size%';
+-------------------------+-------------+
| Variable_name | Value |
+-------------------------+-------------+
| innodb_buffer_pool_size | 11274289152 |
+-------------------------+-------------+
1 row in set (0.01 sec)
那我們如何判斷緩沖池的大小是否合理,可以通過:
-
show engine innodb status
如果Free buffers值為0,表示緩存池設置過小 -
show status like '%buffer_pool_wait%'
如果value值大于0,表示緩存池設置過小
mysql> show engine innodb status \G;
**********忽略部分**********
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Total large memory allocated表示Buffer Pool向操作系統(tǒng)申請的連續(xù)內存空間大小,包括全部控制塊、緩存頁、以及碎片的字節(jié)大小
Dictionary memory allocated 1290731
Dictionary memory allocated表示數據字典信息分配的內存空間的字節(jié)大小,注意這個內存空間和Buffer Pool沒啥關系,不包括在Total memory allocated中
Buffer pool size 688067
Buffer pool size 表示該Buffer Pool可以容納多少緩存頁,注意,單位是頁!
Free buffers 680866
Free buffers表示當前Buffer Pool還有多少空閑緩存頁,也就是free鏈表中還有多少個節(jié)點
Database pages 7194
Database pages表示LRU鏈表中的頁的數量,包含young和old兩個區(qū)域的節(jié)點數量
Old database pages 2650
Old database pages表示LRU鏈表old區(qū)域的節(jié)點數量
Modified db pages 0
Modified db pages表示臟頁數量,也就是flush鏈表中節(jié)點的數量。
Pending reads 0
Pending reads表示正在等待從磁盤上加載到Buffer Pool中的頁面數量,需要注意的s當準備從磁盤中加載某個頁面時,會先為這個頁面在Buffer Pool中分配一個緩存頁以及它對應的控制塊,然后把這個控制塊添加到LRU的old區(qū)域的頭部,但是這個時候真正的磁盤頁并沒有被加載進來,Pending reads的值會跟著加1。
Pending writes: LRU 0, flush list 0, single page 0
Pending writes表示即將從LRU、flush鏈表和單個頁面刷新到磁盤中的頁面數量
Pages made young 23621, not young 178247
Pages made young表示LRU鏈表中曾經從old區(qū)域移動到y(tǒng)oung區(qū)域頭部的節(jié)點數量
not young表示在將innodb_old_blocks_time設置的值大于0時,首次訪問或者后續(xù)訪問某個處在old區(qū)域的節(jié)點時由于不符合時間間隔的限制而不能將其移動到y(tǒng)oung區(qū)域頭部時,Page made not young的值會加1。
0.00 youngs/s, 0.00 non-youngs/s
youngs/s表示每秒從old區(qū)域被移動到y(tǒng)oung區(qū)域頭部的節(jié)點數量
non-youngs/s表示每秒由于不滿足時間限制而不能從old區(qū)域移動到y(tǒng)oung區(qū)域頭部的節(jié)點數量
Pages read 7056, created 29120, written 45996
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
表示讀取,創(chuàng)建,寫入了多少頁。后邊跟著讀取、創(chuàng)建、寫入的速率
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Buffer pool hit rate表示在過去某段時間,平均訪問1000次頁面,有多少次該頁面已經被緩存到Buffer Pool了
young-making rate表示在過去某段時間,平均訪問1000次頁面,有多少次訪問使頁面移動到y(tǒng)oung區(qū)域的頭部了
not (young-making rate)表示在過去某段時間,平均訪問1000次頁面,有多少次訪問沒有使頁面移動到y(tǒng)oung區(qū)域的頭部
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
Pages read ahead表示每秒讀入的pages
evicted without access表示每秒讀出的pages
Random read ahead表示隨機讀人的pages
LRU len: 7194, unzip_LRU len: 0
LRU len表示LRU鏈表中節(jié)點的數量
I/O sum[5]:cur[0], unzip sum[0]:cur[0]
I/O sum表示最近50s讀取磁盤頁的總數
I/O cur表示現在正在讀取的磁盤頁數量
I/O unzip sum表示最近50s解壓的頁面數量
I/O unzip cur表示正在解壓的頁面數量
mysql> show status like '%buffer_pool_wait%';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| Innodb_buffer_pool_wait_free | 0 |
+------------------------------+-------+
1 row in set (0.00 sec)
或者通過分析InnoDB緩沖池的性能來驗證。
可以使用以下公式計算InnoDB緩沖池性能:
Performance = innodb_buffer_pool_reads / innodb_buffer_pool_read_requests * 100
innodb_buffer_pool_reads:
表示InnoDB緩沖池無法滿足的請求數。需要從磁盤中讀取。innodb_buffer_pool_read_requests:
表示從內存中讀取邏輯的請求數。
例如,在我的服務器上,檢查當前InnoDB緩沖池的性能
mysql> show status like 'innodb_buffer_pool_read%';
+---------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_ahead_rnd | 0 |
| Innodb_buffer_pool_read_ahead | 839 |
| Innodb_buffer_pool_read_ahead_evicted | 0 |
| Innodb_buffer_pool_read_requests | 62567 |
| Innodb_buffer_pool_reads | 3043 |
+---------------------------------------+-------+
5 rows in set (0.01 sec)
Innodb_buffer_pool_reads/Innodb_buffer_pool_read_requests*100 即 3043 /62567 *100=4.86
意味著InnoDB可以滿足緩沖池本身的大部分請求。從磁盤完成讀取的百分比非常小。因此無需增加innodb_buffer_pool_size值。
InnoDB buffer pool 命中率:
InnoDB buffer pool 命中率 = innodb_buffer_pool_read_requests / (innodb_buffer_pool_read_requests + innodb_buffer_pool_reads ) * 100
此值低于99%,則可以考慮增加innodb_buffer_pool_size。
InnoDB緩沖池狀態(tài)變量有哪些?
可以運行以下命令進行查看:show status like '%innodb_buffer_pool_pages%'
mysql> show status like '%innodb_buffer_pool_pages%';
+----------------------------------+--------+
| Variable_name | Value |
+----------------------------------+--------+
| Innodb_buffer_pool_pages_data | 4025 |
| Innodb_buffer_pool_pages_dirty | 0 |
| Innodb_buffer_pool_pages_flushed | 215 |
| Innodb_buffer_pool_pages_free | 684034 |
| Innodb_buffer_pool_pages_misc | 69 |
| Innodb_buffer_pool_pages_total | 688128 |
+----------------------------------+--------+
6 rows in set (0.01 sec)
說明:
-
Innodb_buffer_pool_pages_data
InnoDB緩沖池中包含數據的頁數。 該數字包括臟頁面和干凈頁面。 使用壓縮表時,報告的Innodb_buffer_pool_pages_data值可能大于Innodb_buffer_pool_pages_total。 -
Innodb_buffer_pool_pages_dirty
顯示在內存中修改但尚未寫入數據文件的InnoDB緩沖池數據頁的數量(臟頁刷新)。 -
Innodb_buffer_pool_pages_flushed
表示從InnoDB緩沖池中刷新臟頁的請求數。 -
Innodb_buffer_pool_pages_fre
e顯示InnoDB緩沖池中的空閑頁面 -
Innodb_buffer_pool_pages_misc
InnoDB緩沖池中的頁面數量很多,因為它們已被分配用于管理開銷,例如行鎖或自適應哈希索引。此值也可以計算為Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free - Innodb_buffer_pool_pages_data。 -
Innodb_buffer_pool_pages_total
InnoDB緩沖池的總大小,以page為單位。 -
innodb_buffer_pool_reads
表示InnoDB緩沖池無法滿足的請求數。需要從磁盤中讀取。 -
innodb_buffer_pool_read_requests
它表示從內存中邏輯讀取的請求數。 -
innodb_buffer_pool_wait_free
通常,對InnoDB緩沖池的寫入發(fā)生在后臺。 當InnoDB需要讀取或創(chuàng)建頁面并且沒有可用的干凈頁面時,InnoDB首先刷新一些臟頁并等待該操作完成。 此計數器計算這些等待的實例。 如果已正確設置innodb_buffer_pool_size,則此值應該很小。如果大于0,則表示InnoDb緩沖池太小。 -
innodb_buffer_pool_write_request
表示對緩沖池執(zhí)行的寫入次數。
2.3 緩沖池的管理
2.3.1 Buffer Pool的初始化
在 MySQL 啟動的時候,InnoDB 會為 Buffer Pool 申請一片連續(xù)的內存空間,然后按照默認的16KB的大小劃分出一個個的頁, Buffer Pool 中的頁就叫做緩存頁。此時這些緩存頁都是空閑的,之后隨著執(zhí)行增刪改查操作時,才會有磁盤上的數據頁被緩存到 Buffer Pool 中。
為了更好的管理這些在 Buffer Pool 中的緩存頁,InnoDB 為每一個緩存頁的最前面都創(chuàng)建了一個內存大小一樣的控制塊,里面包括緩存頁的表空間、頁號、緩存頁地址、鏈表節(jié)點等。
每一個控制塊都對應一個緩存頁,在分配控制塊和緩存頁后,剩余的空間不夠一對控制塊和緩存頁的大小,就被稱為
碎片空間
2.3.2 如何管理空閑頁
我們知道Buffer Pool 是一片連續(xù)的內存空間,當 MySQL 運行一段時間后,這片連續(xù)的內存空間中的緩存頁既有空閑的,也有被使用的,為了能夠快速找到空閑的緩存頁,可以使用鏈表結構。MySQL將空閑緩存頁的控制塊作為鏈表的節(jié)點,這個鏈表稱為 Free 鏈表(空閑鏈表)
,如圖
- Free 鏈表上除了有控制塊,還有一個頭節(jié)點,該頭節(jié)點包含鏈表的
頭節(jié)點地址
,尾節(jié)點地址
,以及當前鏈表中節(jié)點的數量
等信息,頭節(jié)點是一塊單獨申請的內存空間(約占40字節(jié)),并不在Buffer Pool的連續(xù)內存空間里 - Free 鏈表節(jié)點是一個一個的控制塊,而每個控制塊包含著對應緩存頁的地址,所以相當于 Free 鏈表節(jié)點都對應一個空閑的緩存頁
- 每個控制塊塊里都有兩個指針分別是:(pre)指向上一個節(jié)點,(next)指向下一個節(jié)點;而且還有一個(clt)數據頁地址
buffer pool在尋找空閑數據頁的時候直接用free鏈表可以直接找到。只要有一頁數據空閑出來之后,直接把該數據頁的地址追加到free鏈表即可。反之每當需要從磁盤中加載一個頁到 Buffer Pool 中時,就從 Free鏈表中取一個空閑的緩存頁,并且把該緩存頁對應的控制塊的信息填上,然后把該緩存頁對應的控制塊從 Free 鏈表中移除
2.3.3 如何管理臟頁
設計 Buffer Pool 除了能提高讀性能,還能提高寫性能,也就是更新數據的時候,不需要每次都要寫入磁盤,而是將 Buffer Pool 對應的緩存頁標記為臟頁,然后再由后臺線程將臟頁寫入到磁盤。
那為了能快速知道哪些緩存頁是臟的,于是就設計出 Flush 鏈表
,它跟 Free 鏈表類似的,鏈表的節(jié)點也是控制塊,區(qū)別在于 Flush 鏈表的元素都是臟頁。
有了 Flush 鏈表后,后臺線程就可以遍歷 Flush 鏈表,將臟頁寫入到磁盤。
2.3.4 如何提高緩存命中率?
Buffer Pool 的大小是有限的,對于一些頻繁訪問的數據我們希望可以一直留在 Buffer Pool 中,而一些很少訪問的數據希望可以在某些時機可以淘汰掉,從而保證 Buffer Pool 不會因為滿了而導致無法再緩存新的數據,同時還能保證常用數據留在 Buffer Pool 中。要實現這個,最容易想到的就是 LRU(Least recently used)算法
。
該算法的思路是,鏈表頭部的節(jié)點是最近使用的,而鏈表末尾的節(jié)點是最久沒被使用的。那么,當空間不夠了,就淘汰最久沒被使用的節(jié)點,從而騰出空間。
簡單的 LRU 算法的實現思路是這樣的:
- 當訪問的頁在 Buffer Pool 里,就直接把該頁對應的 LRU 鏈表節(jié)點移動到鏈表的頭部。
- 當訪問的頁不在 Buffer Pool里,除了要把頁放入到 LRU 鏈表的頭部,還要淘汰 LRU 鏈表末尾的節(jié)點。
LRU 的實現過程如下:
假如我們要訪問3號頁數據,因為3號頁在Buffer Pool 中,所以會把3號頁移動到頭部即可
假如我們要訪問6號頁數據,但是6號頁不在Buffer Pool 中,所以會淘汰了5號頁,然后在頭部加入6號頁數據
到這里我們可以知道,Buffer Pool 里有三種頁和鏈表來管理數據
-
Free Page
(空閑頁),表示此頁未被使用,位于 Free 鏈表; -
Clean Page
(干凈頁),表示此頁已被使用,但是頁面未發(fā)生修改,位于LRU 鏈表。 -
Dirty Page
(臟頁),表示此頁已被使用且已經被修改,其數據和磁盤上的數據已經不一致。當臟頁上的數據寫入磁盤后,內存數據和磁盤數據一致,那么該頁就變成了干凈頁。臟頁同時存在于 LRU 鏈表和 Flush 鏈表。
但是,MYSQL并沒有使用簡單的LRU算法,因為它無法解決下面問題:
預讀失效
Buffer Pool 污染
2.3.5 預讀失效
先來說說 MySQL 的預讀機制
程序是有空間局部性的,靠近當前被訪問數據的數據,在未來很大概率會被訪問到。所以,MySQL
在加載數據頁時,會提前把它相鄰的數據頁一并加載進來,目的是為了減少磁盤 IO
但是可能這些被提前加載進來的數據頁,并沒有被訪問,相當于這個預讀是白做了,這個就是預讀失效
。
如果使用簡單的 LRU 算法,就會把預讀頁放到 LRU 鏈表頭部,而當 Buffer Pool空間不夠的時候,還需要把末尾的頁淘汰掉。如果這些預讀頁如果一直不會被訪問到,就會出現一個很奇怪的問題,不會被訪問的預讀頁卻占用了 LRU 鏈表前排的位置,而末尾淘汰的頁,可能是頻繁訪問的頁,這樣就大大降低了緩存命中率。
怎么避免預讀失效帶來影響?
要避免預讀失效帶來影響,最好就是讓預讀的頁停留在 Buffer Pool 里的時間要盡可能的短,讓真正被訪問的頁才移動到 LRU 鏈表的頭部,從而保證真正被讀取的熱數據留在 Buffer Pool 里的時間盡可能長。
MySQL 是這樣做的,它改進了 LRU 算法,將 LRU 劃分了 2 個區(qū)域:old 區(qū)域
和 young 區(qū)域
。
young 區(qū)域在 LRU 鏈表的前半部分,old 區(qū)域則是在后半部分,如圖
old 區(qū)域占整個 LRU 鏈表長度的比例可以通 innodb_old_blocks_pc
變量來設置,默認是 37
mysql> show variables like '%innodb_old_blocks_pc%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
1 row in set (0.01 sec)
代表整個 LRU 鏈表中 young 區(qū)域與 old 區(qū)域比例是 63:37,劃分這兩個區(qū)域后,預讀的頁就只需要加入到 old 區(qū)域的頭部,當頁被真正訪問的時候,才將頁插入 young 區(qū)域的頭部。如果預讀的頁一直沒有被訪問,就會從 old 區(qū)域移除,這樣就不會影響 young 區(qū)域中的熱點數據。這個變量是可以根據我們實際情況修改
mysql> set persist innodb_old_blocks_pct = 40;
Query OK, 0 rows affected (0.00 sec)
舉個例子
- 假設有一個長度為 10 的 LRU 鏈表,其中 young 區(qū)域占比 70 %,old 區(qū)域占比 30%。如下
- 假如我們有兩個11和12號頁被預讀了,這個頁號會被插入到old區(qū)域頭部,而old區(qū)域9和10號頁給淘汰,如果9和10號頁一直沒有被訪問到,那么就不會占用young區(qū)域的位置,而且會給young區(qū)域的數據更早被淘汰
- 如果11號頁被預讀后,立刻被訪問了,那么就會將它插入到 young 區(qū)域的頭部,young 區(qū)域末尾的頁(7號),會被擠到 old 區(qū)域,作為 old 區(qū)域的頭部,這個過程并不會有頁被淘汰。
2.3.6 Buffer Pool污染
當某一個 SQL 語句掃描了大量的數據時,在 Buffer Pool 空間比較有限的情況下,可能會將 Buffer Pool 里的所有頁都替換出去,導致大量熱數據被淘汰了,等這些熱數據又被再次訪問的時候,由于緩存未命中,就會產生大量的磁盤 IO,MySQL 性能就會急劇下降,這個過程被稱為 Buffer Pool 污染
。
注意, Buffer Pool 污染并不只是查詢語句查詢出了大量的數據才出現的問題,即使查詢出來的結果集很小,也會造成 Buffer Pool 污染。比如,在一個數據量非常大的表,執(zhí)行了這條語句:select * from t_user where name like '%a%'
可能這個查詢出來的結果就幾條記錄,但是由于這條語句會發(fā)生索引失效,所以這個查詢過程是全表掃描的,接著會發(fā)生如下的過程:
- 從磁盤讀到的頁加入到 LRU 鏈表的 old 區(qū)域頭部;
- 當從頁里讀取行記錄時,也就是頁被訪問的時候,就要將該頁放到 young 區(qū)域頭部;
- 接下來拿行記錄的 name 字段和字符串 ian 進行模糊匹配,如果符合條件,就加入到結果集里;
- 如此往復,直到掃描完表中的所有記錄。
怎么解決出現 Buffer Pool 污染而導致緩存命中率下降的問題?
像前面這種全表掃描的查詢,很多緩沖頁其實只會被訪問一次,但是它卻只因為被訪問了一次而進入到 young 區(qū)域,從而導致熱點數據被替換了。
LRU 鏈表中 young 區(qū)域就是熱點數據,只要我們提高進入到 young 區(qū)域的門檻,就能有效地保證 young 區(qū)域里的熱點數據不會被替換掉。
MySQL 是這樣做的,進入到 young 區(qū)域條件增加了一個停留在 old 區(qū)域的時間判斷。
具體是這樣做的,在對某個處在 old 區(qū)域的緩存頁進行第一次訪問時,就在它對應的控制塊中記錄下來這個訪問時間:
- 如果后續(xù)的訪問時間與第一次訪問的時間在某個時間間隔內,那么該緩存頁就不會被從 old 區(qū)域移動到 young 區(qū)域的頭部;
- 如果后續(xù)的訪問時間與第一次訪問的時間不在某個時間間隔內,那么該緩存頁移動到 young 區(qū)域的頭部;
這個間隔時間是由 innodb_old_blocks_time
控制的,默認1000毫秒,也就是1秒
mysql> show variables like '%innodb_old_blocks_time%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 |
+------------------------+-------+
1 row in set (0.01 sec)
也就說,只有同時滿足被訪問
與在 old 區(qū)域停留時間超過 1 秒
兩個條件,才會被插入到 young 區(qū)域頭部,這樣就解決了 Buffer Pool 污染的問題 ,這個變量是可以根據我們實際情況進行修改
mysql> set persist innodb_old_blocks_time = 2000;
Query OK, 0 rows affected (0.01 sec)
2.3.7 臟頁什么時候會被刷入磁盤?
引入了 Buffer Pool 后,當修改數據時,首先是修改 Buffer Pool 中數據所在的頁,然后將其頁設置為臟頁,但是磁盤中還是原數據。
因此,臟頁需要被刷入磁盤,保證緩存和磁盤數據一致,但是若每次修改數據都刷入磁盤,則性能會很差,因此一般都會在一定時機進行批量刷盤。
可能大家擔心,如果在臟頁還沒有來得及刷入到磁盤時,MySQL 宕機了,不就丟失數據了嗎?
這個不用擔心,InnoDB 的更新操作采用的是 Write Ahead Log 策略,即先寫日志,再寫入磁盤,通過 redo log 日志讓 MySQL 擁有了崩潰恢復能力。
下面幾種情況會觸發(fā)臟頁的刷新:
- 當 redo log 日志滿了的情況下,會主動觸發(fā)臟頁刷新到磁盤;
- Buffer Pool 空間不足時,需要將一部分數據頁淘汰掉,如果淘汰的是臟頁,需要先將臟頁同步到磁盤;
- MySQL 認為空閑時,后臺線程回定期將適量的臟頁刷入到磁盤;
- MySQL 正常關閉之前,會把所有的臟頁刷入到磁盤;
在我們開啟了慢 SQL 監(jiān)控后,如果你發(fā)現偶爾會出現一些用時稍長的 SQL,這可能是因為臟頁在刷新到磁盤時可能會給數據庫帶來性能開銷,導致數據庫操作抖動。如果間斷出現這種現象,就需要調大 Buffer Pool 空間或 redo log 日志的大小。
后臺有專門的線程每隔一段時間負責把臟頁刷新到磁盤,這樣可以不影響用戶線程處理正常的請求。主要有兩種刷新路徑:
-
從LRU鏈表的冷數據中刷新一部分頁面到磁盤。后臺線程會定時從LRU鏈表尾部開始掃描一些頁面,掃描的頁面數量可以通過系統(tǒng)變量innodb_lru_scan_depth來指定,如果從里邊兒發(fā)現臟頁,會把它們刷新到磁盤。這種刷新頁面的方式被稱之為
BUF_FLUSH_LRU
。 -
從flush鏈表中刷新一部分頁面到磁盤。后臺線程也會定時從flush鏈表中刷新一部分頁面到磁盤,刷新的速率取決于當時系統(tǒng)是不是很繁忙。這種刷新頁面的方式被稱之為
BUF_FLUSH_LIST
。
有時候后臺線程刷新臟頁的進度比較慢,導致用戶線程在準備加載一個磁盤頁到Buffer Pool時沒有可用的緩存頁,這時就會嘗試看看LRU鏈表尾部有沒有可以直接釋放掉的未修改頁面,如果沒有的話會不得不將LRU鏈表尾部的一個臟頁同步刷新到磁盤(和磁盤交互是很慢的,這會降低處理用戶請求的速度)。這種刷新單個頁面到磁盤中的刷新方式被稱之為BUF_FLUSH_SINGLE_PAGE
。
當然,有時候系統(tǒng)特別繁忙時,也可能出現用戶線程批量的從flush鏈表中刷新臟頁的情況,很顯然在處理用戶請求過程中去刷新臟頁是一種嚴重降低處理速度的行為(畢竟磁盤的速度滿的要死),這屬于一種迫不得已的情況,不過這得放在后邊嘮叨redo?志的checkpoint時說了。
2.4 緩沖池的高并發(fā)
如果 InnoDB 存儲引擎只有一個 Buffer Pool,當高并發(fā)時,多個請求進來,那么為了保證數據的一致性(緩存頁、free 鏈表、flush 鏈表、lru 鏈表等多種操作),必須得給緩沖池加鎖了,每一時刻只能有一個請求獲得鎖去操作 Buffer Pool,其他請求只能排隊等待鎖釋放,那么此時 MySQL 的性能是有多么的低。
我們是可以通過修改變量innodb_buffer_pool_instances
給 MySQL 設置多個 Buffer Pool 來提升 MySQL 的并發(fā)能力。
innodb_buffer_pool_instances
是一個持久化只讀系統(tǒng)變量,需要授予persist_ro_variables_admin
(啟用持久化只讀系統(tǒng)變量)和system_variables_admin
(啟用修改或保留全局系統(tǒng)變量)的權限,
mysql> set persist_only innodb_buffer_pool_instances=4;
Query OK, 0 rows affected (0.01 sec)
修改完成后,我們需要重啟mysql
mysql> show variables like '%innodb_buffer_pool_instances%';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 4 |
+------------------------------+-------+
1 row in set (0.02 sec)
每個 Buffer Pool 負責管理著自己的控制塊和緩存頁,有自己獨立一套 free 鏈表、flush 鏈表和 LRU鏈表
假設給 Buffer Pool 調整到 16 G,就是說變量innodb_buffer_pool_size 改為 17179869184
,此時,MySQL 會為 Buffer Pool 申請一塊大小為16G 的連續(xù)內存,然后分成 4塊,接著將每一個 Buffer Pool 的數據都復制到對應的內存塊里,最后再清空之前的內存區(qū)域。那這是相當耗費時間的操作
為了解決上面的問題,Buffer Pool 引入一個機制:chunk 機制
- 每個 Buffer Pool 其實是由多個 chunk 組成的。每個 chunk 的大小由參數 innodb_buffer_pool_chunk_size 控制,默認值是 128M。
mysql> show variables like '%innodb_buffer_pool_chunk_size%';
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 134217728 |
+-------------------------------+-----------+
1 row in set (0.01 sec)
innodb_buffer_pool_chunk_size
這個變量如同innodb_buffer_pool_instances
一樣,是一個持久化只讀系統(tǒng)變量,修改完成后需要重啟MySQL
mysql> set persist_only innodb_buffer_pool_chunk_size = 132417728;
Query OK, 0 rows affected (0.00 sec)
- 每個 chunk 就是一系列的描述數據塊和對應的緩存頁。
- 每個 Buffer Pool 里的所有 chunk 共享一套 free、flush、lru 鏈表。
得益于 chunk 機制,通過增加 Buffer Pool 的chunk個數就能避免了上面說到的問題。當擴大 Buffer Pool 內存時,不再需要全部數據進行復制和粘貼,而是在原本的基礎上進行增減內存。
下面舉個例子,介紹一下 chunk 機制下,Buffer Pool 是如何動態(tài)調整大小的:
- 調整前
Buffer Pool
的總大小為8G
,調整后的Buffer Pool
大小為 16 G。 - 由于
Buffer Pool
的實例數是不可以變的,所以是每個Buffer Pool
增加 2G 的大小,此時只要給每個Buffer Pool
申請 (2048M/128M)個chunk就行了,但是要注意的是,新增的每個 chunk 都是連續(xù)的128M內存。
緩沖池大小必須始終等于或者是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的倍數。如果將緩沖池大小更改為不等于或等于innodb_buffer_pool_chunk_size **innodb_buffer_pool_instances的倍數的值,
則緩沖池大小將自動調整為等于或者是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的倍數的值
2.5 緩沖池數據預熱(了解)
我們關閉數據庫的時候,想將緩沖池中的數據保存到ib_Buffer_pool中,可以調整如下變量innodb_buffer_pool_dump_pct:
指定每個緩沖池最近使用的頁面讀取和轉儲的百分比。 范圍是1到100。默認值是25。例如,如果有4個緩沖池,每個緩沖池有100個page,并且innodb_buffer_pool_dump_pct設置為25,則dump每個緩沖池中最近使用的25個page。innodb_buffer_pool_dump_at_shutdown:
默認啟用。指定在MySQL服務器關閉時是否記錄在InnoDB緩沖池中緩存的頁面,以便在下次重新啟動時縮短預熱過程。innodb_buffer_pool_load_at_startup:
默認啟用。指定在MySQL服務器啟動時,InnoDB緩沖池通過加載之前保存的相同頁面自動預熱。 通常與innodb_buffer_pool_dump_at_shutdown結合使用
2.6 緩沖池的案例
2.6.1 大量的全表掃描
如果在業(yè)務中做了大量的全表掃描,那么你就可以將innodb_old_blocks_pct設置減小,增大innodb_old_blocks_time的時間,不讓這些無用的查詢數據進入old區(qū)域,盡量不讓緩存再young 區(qū)域的有用的數據被立即刷掉。(這也是治標的方法,大量全表掃描就要優(yōu)化sql和表索引結構了)
mysql> set persist innodb_old_blocks_time=4000;
Query OK, 0 rows affected (0.00 sec)
mysql> set persist innodb_old_blocks_pct=20;
Query OK, 0 rows affected (0.01 sec)
2.6.2 沒有大量的全表掃描
如果在業(yè)務中沒有做大量的全表掃描,那么你就可以將innodb_old_blocks_pct增大,減小innodb_old_blocks_time的時間,讓有用的查詢緩存數據盡量緩存在innodb_buffer_pool_size中,減小磁盤io,提高性能。
mysql> set persist innodb_old_blocks_time=1000;
Query OK, 0 rows affected (0.00 sec)
mysql> set persist innodb_old_blocks_pct=37;
Query OK, 0 rows affected (0.01 sec)
總結
今天理論的知識很多,下面簡單做一下總結:
- 緩沖池(Buffer Pool)是一種常見的降低磁盤訪問的機制;
- 緩沖池通常以頁(Page)為單位緩存數據,OS的Page大小一般為4KB,MySQL的Page大小一般為16KB;
- Page可以分為Free Page(空閑頁)、Clean Page(干凈頁)、Dirty Page(臟頁);
- 緩沖池中含有3個鏈表:LRU鏈表(LRU List)、Free鏈表(Free List)、Flush鏈表(Flush List),以及LRU鏈表和Flush鏈表的區(qū)別;
- 緩沖池常見的管理算法是LRU,Memcache、OS、MySQL的InnoDB存儲引擎都使用了這種最近、最少使用原則算法(Least Rrecently Used);
- MySQL的InnoDB存儲引擎對普通的LRU進行了優(yōu)化:
- 將緩沖池分為New Sublist(新生代/Young)和Old Sublist(老生代/Old),入緩沖池的Page,優(yōu)先從Midpoint進入Old Sublist,Page被訪問,才進入New Sublist,以解決預讀(Read-Ahead)失效的問題。
- Page被訪問,且在Old Sublist停留時間超過配置innodb_old_blocks_time閥值時,才進入New Sublist,以解決批量數據訪問,大量數據淘汰的問題。
至此今天的學習就到此結束了,愿您成為堅不可摧的自己~~~
?
You can’t connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future.You have to trust in something - your gut, destiny, life, karma, whatever. This approach has never let me down, and it has made all the difference in my life
如果我的內容對你有幫助,請 點贊
、評論
、收藏
,創(chuàng)作不易,大家的支持就是我堅持下去的動力文章來源:http://www.zghlxwxcb.cn/news/detail-428767.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-428767.html
到了這里,關于一文帶你了解MySQL數據庫InnoDB_Buffer_Pool的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!