InnoDB學習(一)之BufferPool


我們知道InnoDB數據庫的數據是持久化在磁盤上的,而磁盤的IO速度很慢,如果每次數據庫訪問都直接訪問磁盤,顯然嚴重影響數據庫的性能。為了提升數據庫的訪問性能,InnoDB為數據庫的數據增加了內存緩存區(BufferPool),避免每次訪問數據庫都進行磁盤IO。

緩存區BufferPool

緩存區並不是Innodb中特有的概念,操作系統中也有緩存區的概念,當用戶第一次從磁盤讀取文件時,會把文件緩存到內存中,后續再對這個文件進行讀操作就可以直接從內存中讀,從而減少磁盤IO次數。緩存只是內存中的一塊連續空間,InnoDB是如何合理利用緩存區的空間的呢?本文會從以下幾個方面介紹InnoDB的緩存區:

  1. 緩存區概覽:InnoDB緩存區的結構和狀態查詢;
  2. 緩存區實例(BufferPool Instance):緩存區可以划分為多個實例;
  3. BufferChunk:緩存區實例內的數據塊;
  4. 控制塊和數據頁:InnoDB是以什么形式緩存數據庫中的數據的;
  5. 空閑空間管理;緩存區內的空閑空間管理邏輯;
  6. 用戶數據管理:數據庫數據和索引在緩存區緩存的管理;
  7. 自適應哈希索引:優化熱點數據等值查詢的哈希索引;
  8. ChangeBuffer簡介:提高數據庫更新效率的ChangeBuffer;
  9. 鎖信息管理:InnoDB中的行鎖信息也是存放在緩存區中的;

緩存區概覽

InnoDB中的緩存區叫innodb_buffer_pool,當讀取數據時,就會先從緩存中查看是否數據的頁(page)存在,不存在的話去磁盤上檢索,查到后緩存到innodb_buffer_pool中。同理,插入、修改、刪除也是先操作緩存里數據,之后再以一定頻率更新到磁盤上,這個刷盤機制叫做Checkpoint。

如下圖所示,InnoDB中的數據主要有數據頁、索引頁、插入緩存、自適應哈希索引、鎖信息和數據字典信息。我們經常聽到的RedoLog不在緩存區中。

InnoDB緩存區結構

MySQL默認的innodb_buffer_pool的大小是128M,我們可以通過以下命令查看innodb_buffer_pool的參數,執行結果如下圖所示:

show variables like 'innodb_buffer_pool%';

InnoDB緩存區參數示例

在MySQL使用過程中,我們可能需要查看緩存區的狀態,比如已使用空間大小、臟頁大小等狀態,我們可以通過以下命令查看innodb_buffer_pool的狀態,執行結果如下圖所示,圖中的執行結果中,共有8192頁數據。

show global status like '%innodb_buffer_pool%';

InnoDB緩存區狀態示例

緩存區實例

緩存區本身是一塊內存空間,在多線程並發訪問緩存的情況下,為了保證緩存頁數據的正確性,可能會對緩存區單實例鎖互斥訪問,如果緩存區非常大並且多線程並發訪問非常高的情況下,單實例緩存區的可能會影響請求的處理速度。如下圖所示,數據庫緩存區大小為3G,並發訪問QPS為3000,如果緩存區只有一個實例,那么這3000個請求可能需要競爭同一個互斥鎖。

InnoDB緩存區單個實例

MySQL 5.5引入了緩存區實例作為減小內部鎖爭用來提高MySQL吞吐量的手段,用戶可以通過設置innodb_buffer_pool_instances參數來指定InnoDB緩存區實例的數目,默認緩存區實例的數目為1。緩存區實例的大小均為`innodb_buffer_pool_size/innodb_buffer_pool_instances。如下圖所示,數據庫緩存區大小為3G,並發訪問QPS為3000,如果緩存區有3個實例,理想情況下最多每1000個請求會競爭同一個互斥鎖。

InnoDB緩存區多個實例

如果緩存區總空間大小小於1G,innodb_buffer_pool_instances會被重置為1,因為小空間的多個緩存區實例反而會影響查詢性能。

緩存區實例有以下特點:

  1. 緩存區實例有自己的鎖/信號量/物理塊/邏輯鏈表,緩存區實例之間沒有鎖競爭關系;
  2. 所有緩存區實例的空間在數據庫啟動時分配,數據庫關閉后釋放;
  3. 緩存頁按照哈希函數隨機分布到不同的緩存實例中;

緩存區實例的BufferChunk

我們知道緩存區可以包含多個緩存區實例,每個緩存區實例包含一塊連續的內存空間,InnoDB把這塊空間划分為多個BufferChunk,BufferChunk是InnoDB中的底層的物理塊,BufferChunck中包含數據頁和控制塊兩部分。

InnoDB緩存區參數示例

BufferChunk是最低層的物理塊,在啟動階段從操作系統申請,直到數據庫關閉才釋放。通過遍歷chunks可以訪問幾乎所有的數據頁,有兩種狀態的數據頁除外:

  1. 沒有被解壓的壓縮頁(BUF_BLOCK_ZIP_PAGE);
  2. 修改過且解壓頁已經被驅逐的壓縮頁(BUF_BLOCK_ZIP_DIRTY);

BufferChunck中包含數據頁和控制塊兩部分,二者存放的數據如下:

  1. 控制塊:頁面管理信息/互斥鎖/頁面的狀態等數據塊控制信息;
  2. 數據頁:數據庫數據/鎖數據/自適應哈希數據,數據頁的大小默認為16K;

BufferChunck數據塊的大小是可配置的,MySQL配置中默認BufferChunck數據塊大小如下所示,用戶可以在MySQL實例啟動之前通過修改配置文件或啟動參數中指定,達到自定義BufferChunck數據塊的大小的目的。

$> mysqld --innodb-buffer-pool-chunk-size=134217728
[mysqld]
innodb_buffer_pool_chunk_size = 134217728

用戶自定義innodb_buffer_pool_chunk_size參數的大小應當小於單個緩存區實例的空間大小。如果innodb_buffer_pool_chunk_size值乘以innodb_buffer_pool_instances大於初始化緩沖池總大小時, innodb_buffer_pool_chunk_size則截斷為innodb_buffer_pool_size/innodb_buffer_pool_instances。

控制塊和數據頁

通過上文,我們知道InnoDB中的底層物理塊是BufferChunk,BufferChunk中包含了控制塊和數據頁,本節會介紹數據頁和控制塊分別包含哪些數據。

控制塊

InnoDB中的每個數據頁都有一個相對應的控制塊,用於存儲數據頁的管理信息,但是這些信息不需要記錄到磁盤,而是根據讀入數據塊在內存中的狀態動態生成的。查找或者修改數據頁時,總是會通過控制塊進行數據塊操作,控制塊主要包含以下數據:

  1. 頁面管理的普通信息/互斥鎖/頁面的狀態等;
  2. 空閑鏈表/LRU鏈表/FLU鏈表等鏈表的管理;
  3. 按照一定的哈希函數快速定位數據頁位置;

InnoDB緩存區控制塊

數據頁

InnoDB中,數據管理的最小單位為頁,默認是16KB,頁中除了存儲用戶數據,還可以存儲控制信息的數據。InnoDB IO子系統的讀寫最小單位也是頁。如果對表進行了壓縮,則對應的數據頁稱為壓縮頁,如果需要從壓縮頁中讀取數據,則壓縮頁需要先解壓,形成解壓頁,解壓頁為16KB。壓縮頁的大小是在建表的時候指定,目前支持16K,8K,4K,2K,1K。即使壓縮頁大小設為16K,在blob/varchar/text的類型中也有一定好處。假設指定的壓縮頁大小為4K,如果有個數據頁無法被壓縮到4K以下,則需要做B-tree分裂操作,這是一個比較耗時的操作。

數據頁可以用於存放以下類型的數據,下文中我們會對這些類型的數據結構進行詳細介紹:

  • 用戶數據,聚簇索引和非聚簇索引對應的節點數據;
  • 行鎖信息,InnoDB鎖過多異常時,可以通過增加BufferPool大小解決;
  • 自適應哈希,用於緩存熱點數據;
  • ChangeBuffer緩存;

空閑空間管理

當我們最初啟動服務器的時候,需要完成對的初始化過程,就是分配的內存空間,把它划分成若干對控制塊和緩存頁。但是此時並沒有真實的磁盤頁被緩存到中(因為還沒有用到),之后隨着程序的運行,會不斷的有磁盤上的頁被緩存到中,那么問題來了,從磁盤上讀取一個頁到中的時候該放到哪個緩存頁的位置呢?或者說怎么區分中哪些緩存頁是空閑的,哪些已經被使用了呢?我們最好在某個地方記錄一下哪些頁是可用的,我們可以把所有空閑的頁包裝成一個節點組成一個雙向鏈表,這個鏈表也可以被稱作(或者說空閑鏈表)。

如果InnoDB剛剛啟動,緩存區的所有緩存頁都是空閑的,每一個緩存頁都會被加入到空閑鏈表中,此時空閑列表的結構如下所示(此處省略數據頁,空閑鏈表的指針指向數據塊的控制塊)。

InnoDB緩存區空閑空間

在需要加載緩存頁到BufferPool的情況下,如果空閑鏈表不為空,我們可以從空閑鏈表中獲取一頁空閑數據頁,將緩存放入空閑的數據頁。以LRU(后文詳細介紹)為例,InnoDB啟動后,LRU加載第一個緩存頁之后,BufferPool中的數據情況如下所示。

InnoDB緩存區空閑空間使用

用戶數據管理

用戶數據管理是BufferPool中最重要的數據,包含表數據與索引數據等數據,用戶數據會按照數據的狀態進行管理,主要包含以下數據管理,下文會一一介紹這幾種鏈表:

  1. 最近最少使用鏈表(Least Recently Used, LRU):InnoDB中最重要的鏈表,包含所有讀取進來的數據頁;
  2. 臟頁鏈表(Flush LRU List):管理LRU中的臟頁,后台線程定時寫入磁盤;
  3. 解壓頁鏈表(Unzip LRU List):管理LRU中的解壓頁數據,解壓頁數據是從壓縮頁通過解壓而來的;
  4. 壓縮頁鏈表(Zip List):顧名思義,對頁數據壓縮后組成的鏈表;

最近最少使用鏈表LRU

最近最少使用鏈表LRU用於緩存表數據與索引數據,由於內存大小通常遠遠小於磁盤大小,內存中無法緩存全部的數據庫數據,所以緩存通常需要一定的淘汰策略,淘汰緩存中不經常使用的數據頁。InnoDB的BufferPool采用了改進版的LRU的淘汰策略。

如下圖所示,LRU鏈表的結構和空閑鏈表的結構類似,是一個雙向鏈表,鏈表中的節點包含指向數據頁控制塊的指針,可以通過控制塊訪問數據頁中的數據。

InnoDB緩存區LRU鏈表

當需要將新數據頁添加到緩沖池時,最近最少使用的數據頁會可能會從LRU鏈表中淘汰,並將新數據頁添加到LRU鏈表的中間。此插入點將列LRU鏈表划分為兩個子鏈表:

  1. 頭部的5/8區域,最近訪問多的熱數據列表;
  2. 尾部的3/8區域,最近訪問少的冷數據列表;

InnoDB緩存區LRU鏈表

LRU算法會將經常使用的數據頁保留在熱數據列表中,冷數據列表中包含了不經常訪問的數據頁,這些數據頁是LRU列表滿了之后最先被淘汰的數據。默認情況下,算法的流程如下:

  1. LRU鏈表的的后3/8區域用於存儲冷數據;
  2. LRU鏈表的中點是熱數據尾部與冷數據頭部相交的邊界;
  3. 被訪問的冷數據會從冷數據鏈表移動到熱數據鏈表;
  4. 熱數據鏈表中的數據如果長時間不訪問,會逐漸移入冷數據鏈表;
  5. 冷數據長時間不被訪問,並且LRU鏈表滿了,那么末尾的冷數據會淘汰出LRU鏈表;
  6. 預讀的數據只會插入LRU鏈表,不會被移動到熱數據鏈表;

LRU算法還有一個問題,當某一個SQL語句,要批量掃描大量數據時,由於這些頁都會被訪問,可能導致把緩沖池的所有頁都替換出去,導致大量熱數據被換出,MySQL性能急劇下降,這種情況叫緩沖池污染。MySQL緩沖池加入了一個冷數據停留時間窗口的機制:

  1. 假設T=冷數據停留時間窗口;
  2. 插入冷數據頭部的數據頁,即使立刻被訪問,也不會立刻放入新生代頭部;
  3. 只有滿足被訪問並且在冷數據區域停留時間大於T,才會被放入新生代頭部;

加入冷數據停留時間窗口策略后,短時間內被大量加載的頁,並不會立刻插入新生代頭部,而是優先淘汰那些短期內僅僅訪問了一次的頁。

MySQL中LRU鏈表相關的參數:

  • innodb_old_blocks_pct:冷數據占整個LRU鏈長度的比例,默認是3/8,即整個LRU中熱數據與冷數據長度比例是5:3。
  • innodb_old_blocks_time冷數據停留時間窗口機制中冷數據停留時長;

臟數據鏈表FLU

當需要更新一個數據頁時,如果數據頁在內存中就直接更新更新內存中的數據,但是由於寫回磁盤的代價比較高,所以InnoDB並不會立刻把修改后的數據寫回磁盤,此時,就出現了緩存區數據頁和磁盤數據頁中的數據不一致的情況,這種情況下緩存區數據頁被稱為臟頁,管理所有臟頁的鏈表叫臟數據鏈表,以下為臟數據鏈表的示例圖:

InnoDB緩存區LRU鏈表

臟數據鏈表是LRU鏈表的子集,LRU鏈表包含了所有的臟頁數據。臟頁中的數據最終是要寫回磁盤的,將內存數據頁刷到磁盤的操作稱為刷臟,以下是幾種會觸發InnoDB刷臟的情況

  • InnoDB的RedoLog寫滿了,這時候系統會停止所有更新操作,把Checkpoint往前推進,RedoLog留出空間可以繼續寫;
  • 當系統內存不足,需要把一個臟頁要從LRU鏈表中淘汰時,要先把臟頁寫回磁盤;
  • MySQL在空閑時,會自動把一部分臟頁寫回磁盤;
  • MySQL正常關閉時,會把所有臟頁都寫回磁盤;

InnoDB中可以通過一些參數設置刷臟行為:

  • innodb_io_capacity:MySQL數據文件所在磁盤的IO能力,innodb_io_capacity參數會影響MySQL刷臟頁的速度。磁盤的IOPS可以通過FIO工具來測試,測試命令如下所示:

    fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
    

    如果不能正確地設置innodb_io_capacity參數,可能能導致數據庫性能問題。舉個例子說明:如果MySQL主機磁盤用的是SSD,但是innodb_io_capacity的值設置的是比較低,只有300。這種情況下,InnoDB認為這個系統的IO能力只有300,所以刷臟頁刷得特別慢,甚至比臟頁生成的速度還慢,這樣就造成了臟頁累積,影響了查詢和更新性能。

  • innodb_flush_neighbors:在准備刷一個臟頁的時候,如果這個數據頁旁邊的數據頁剛好是臟頁,就會把這個“鄰居”也帶着一起刷掉;而且這個把“鄰居”拖下水的邏輯還可以繼續蔓延,也就是對於每個鄰居數據頁,如果跟它相鄰的數據頁也還是臟頁的話,也會被放到一起刷。innodb_flush_neighbors參數就是用來控制這個行為的,值為1的時候會有上述的“連坐”機制,值為0時表示不找鄰居,自己刷自己的。對於SSD這類IOPS比較高的設備,IOPS往往不是瓶頸,innodb_flush_neighbors應該設置為0。在MySQL8.0中,innodb_flush_neighbors參數的默認值已經是0了。

  • innodb_max_dirty_pages_pct:臟頁比例超過innodb_max_dirty_pages_pct之后,InnoDB會全力刷臟頁,如果沒超過這個比例,那么刷臟頁速度=max(當前臟頁比例/innodb_max_dirty_pages_pct*innodb_io_capacity, RedoLog的緩存大小計算刷臟頁速度);

壓縮頁鏈表(Zip List)

Mysql允許用戶對表進行壓縮以節省磁盤空間,這些壓縮頁的數據在進入內存之后,要進行解壓之后才能使用。

我們可以通過以下SQL語句建立一張InnoDB數據表:

create table user_info
(
    id   int primary key,
    age  int not null,
    name varchar(16),
    sex  bool
)engine=InnoDB;

對於建立好的InnoDB數據表,我們可以通過以下SQL語句對表進行壓縮,壓縮后表占用的磁盤空間會減小:

alter table user_info row_format=compressed;

InnoDB中的表壓縮是針對表數據頁的壓縮,不僅可以壓縮表數據,還可以壓縮表索引。壓縮頁的大小可以是1k/2k/4k/8k。

壓縮頁鏈表存儲的就是這些壓縮后的頁,壓縮頁在加載進內存之后,並不會立即解壓,而是在需要使用的時候再進行解壓。

壓縮頁有不同的大小1k/2k/4k/8k,InnoDB使用了伙伴管理算法來管理壓縮頁。有5個ZipFree鏈表分別管理1k/2k/4k/8k/16K的內存碎片,8K的鏈表里存儲的都是8K的碎片,如果新讀入一個8K的頁面,首先從這個鏈表中查找,如果有則直接返回,如果沒有則從16K的鏈表中分裂出兩個8K的塊,一個被使用,另外一個放入8K鏈表中。

解壓頁鏈表(Unzip LRU List)

壓縮頁鏈表中的數據都是被壓縮的,不能直接CRUD,使用前需要解壓,解壓后的數據都存儲在解壓頁鏈表中,解壓頁鏈表中的數據寫回磁盤時需要壓縮。

自適應哈希索引

我們知道B+樹默認的索引數據結構是B+樹,B+樹對范圍查詢或者LIKE語法的支持比較好。

如果數據庫中有大量的等值查詢,使用哈希索引能顯著提升查詢效率。Innodb存儲引擎會監控對表上二級索引的查找,如果發現某二級索引被頻繁訪問,二級索引成為熱數據,會對該熱點數據建立內存哈希索引,這個索引被稱為自適應哈希索引。

自適應哈希索引默認是開啟狀態,可以通過設置innodb_adaptive_hash_index變量或在啟動MySQL時添加--skip-innodb-adaptive-hash-index變量啟用自適應哈希索引。

InnoDB中可以查看到哈希索引的使用情況,命令及輸出如下所示:

mysql> show engine innodb status\G
……
Hash table size 34673, node heap has 0 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

ChangeBuffer

在修改數據庫數據時,如果對應的數據頁剛剛好在緩存區,可以之間修改緩存區的數據頁,並把數據頁標記為臟頁。

如果修改數據數據時,對應的數據頁如果不在緩存區,就需要把數據頁從磁盤加載到緩存區,然后進行修改。對於寫多讀少的場景,會產生大量的磁盤IO,影響數據庫的性能。

Change Buffer對數據更新過程有加速作用。 如果數據頁沒有在內存中,會將更新操作緩存到Change Buffer 中,這樣就不需要從磁盤讀入這個數據頁,減少了IO操作,提高了性能。 先將更新操作,記錄在Change Buffer 中,之后再進行 merge,真正進行數據更新。InnoDB Change Buffer比較復雜,我會在后續單獨章節中進行介紹。

InnoDB Change Buffer

行鎖信息管理

InnoDB支持行鎖,可以對數據庫中的數據進行加鎖操作,這些鎖信息也存放在BufferPool中,具體存儲格式此處不做詳細解釋。

既然鎖信息都存放在BufferPool中,那么鎖的數目肯定受緩存區大小的影響,如果InnoDB中鎖占據的空間超過了BufferPool總大小的70%,在新添加鎖時會報以下錯誤:

[FATAL] InnoDB: Over 95 percent of the buffer pool is occupied by lock heaps or the adaptive hash index! Check that your transactions do not set too many row locks. Your buffer pool size is 8 MB. Maybe you should make the buffer pool bigger? We intentionally generate a seg fault to print a stack trace on Linux!For more information, see Help and Support Center at http://www.mysql.com.

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

參考文檔

  1. MySQL 8.0 Reference Manual/The InnoDB Storage Engine/InnoDB Architecture
  2. Chunk Change: InnoDB Buffer Pool Resizing
  3. 玩轉MySQL之十InnoDB Buffer Pool詳解
  4. InnoDB的Buffer Pool簡介
  5. Mysql的Innodb存儲引擎緩沖池個人理解
  6. InnoDB關鍵特性之自適應hash索引
  7. InnoDB頁壓縮技術

本文最先發布至微信公眾號,版權所有,禁止轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM