天天看點

揭秘InnoDB插入緩沖:提升非唯一輔助索引插入性能的秘密武器

作者:極速星空4DO

想要提高資料庫的插入性能嗎?本文揭示了InnoDB存儲引擎的秘密武器——插入緩沖(Insert Buffer)。通過插入緩沖,InnoDB存儲引擎可以顯著提升非唯一輔助索引的插入性能。本文詳細解析了插入緩沖的原理和内部實作,帶你深入了解這項性能優化技術。同時,我們也會探讨插入緩沖可能帶來的問題,并介紹其更新版——變更緩沖(Change Buffer)。

接下來我們進入正文。

insert buffer 是InnoDB存儲引擎所獨有的功能。通過insert buffer,InnoDB存儲引擎可以大幅度提高資料庫中非唯一輔助索引的插入性能。

資料庫對于自增主鍵值的插入是順序的,是以插入能有較高的性能。

SQL複制代碼create table t (
   id int auto_increment,
   name varchar(30),
   primary key (id))
}
           

id列式自增長的,即當執行插入操作時,id列會自動增長,頁中行記錄按id順序存放,不需要随機讀取其它頁的資料。是以,在這樣的情況下(即聚集索引),插入操作效率很高。

但是實際生産環境中,使用者表中主鍵僅有并且隻能有1個,然而表中可能存在多個輔助索引。如下所示:

SQL複制代碼create table t (
   id int auto_increment,
   name varchar(30),
   primary key (id),
   key (name));
}
           

在進行插入操作時,資料頁的存放還是按主鍵 id 進行順序存放的,但是對于非聚集索引葉子節點的插人不再是順序的了,這時就需要離散地通路非聚集素引頁,由于随機讀取的存在而導緻了插入操作性能下降。當然這并不是 name 字段上索引的錯誤,而是因為 B+樹的特性決定了非聚集索引插人的離散性。

為什麼說非聚集索引插入操作會導緻性能下降?

非聚集索引的離散寫操作涉及到在索引頁中插入新的鍵值對。由于非聚集索引頁的分散存儲,插入操作通常會導緻非聚集索引頁的分裂或調整,以保持B+樹的平衡和有序性(在前文我們學習自增主鍵時,提到過自增主鍵可以防止頁分裂)。這種非順序的寫入會引發随機寫入,而随機寫入的性能通常較差。

插入緩沖原理

為了解決這個問題,InnoDB設計出了插入緩沖技術,對于非聚集類索引的插入和更新操作,不是每一次都直接插入到索引頁中,而是先判斷插入的非聚集索引頁是否在緩沖池中,若在,則直接插入;若不在,則先将插入的記錄放到insert buffer中,然後根據一些算法将insert buffer 緩存的記錄通過背景線程慢慢的合并(merge)回輔助索引頁中。這樣做的好處是:

(1)減少磁盤的離散讀取;

(2)将多次插入合并為一次操作。

例如name字段的插入順序為:

SQL複制代碼('Maria',10), ('David',7), ('Tim', 11), ('Jim', 7), ('Monty', 10), ('Herry', 7), ('Heikki', 7) 
           

後面的數字表示原先插入的輔助索引對應的資料頁碼 page_no,可以看到頁的通路是完全無序的,然而當插入到insert buffer中時,記錄根據應插入輔助索引的葉子節點 page_no 進行排序,故上述記錄在insert buffer中的狀态應為:

SQL複制代碼('David',7), ('Jim', 7), ('Herry', 7), ('Heikki', 7) , ('Maria',10), ('Monty', 10), ('Tim', 11)
           

當要進行合并時,頁page_no為7的記錄有4條,可以一次性将這4條記錄插入到輔助索引中,進而提高資料庫的整體性能。

insert buffer的使用需要滿足以下兩個條件:

(1)索引是輔助索引(secondary index)

(2)索引是非唯一的

若是唯一索引,那麼在插入時需要判斷插入的記錄是否是唯一,這需要讀取輔助索引頁,而 insert buffer 的設計就是避免讀取insert buffer,這會導緻失去insert buffer 的設計意義。

通過如下指令可以檢視插入緩沖的資訊:

SQL複制代碼show engine innodb status;
           

插入緩沖的内部實作

insert buffer 的資料結構是一棵B+樹。在 MySQL4.1之前的版本中每張表都有一棵 insert buffer B+樹。而在現在的版本中,全局隻有一棵insert buffer B+樹,負責對所有的表的輔助索引進行 insert buffer。這棵B+樹存放在共享表空間中,預設也就是ibdata1中。是以,試圖通過獨立表空間ibd檔案恢複表中資料時,往往會導緻check table 失敗。這是因為表的輔助索引中的資料可能還在insert buffer中,也就是共享表空間中。是以通過idb檔案進行恢複後,還需要進行repair table 操作來重建表上所有的輔助索引。

insert buffer是一棵B+樹,是以其也由葉子節點和非葉子節點組成。非葉子節點存放的是查詢的search key(鍵值)。其構造包括三個字段:

揭秘InnoDB插入緩沖:提升非唯一輔助索引插入性能的秘密武器

search key一共占9位元組,其中space占4位元組,marker占1位元組、offset占4位元組。space表示待插入記錄所在的表空間id,在InnoDB存儲引擎中,每個表有一個唯一的space id,可以通過space id查詢得知是哪張表,marker是用來相容老版本的insert buffer,offset表示插入緩沖中的頁在磁盤資料檔案中的偏移量。

當一個輔助索引需要插入到頁(space,offset)時,如果這個頁不在緩沖池中,那麼InnoDB存儲引擎首先根據上述規則構造一個search key,接下來查詢insert buffer這棵B+樹,然後再将這條記錄插入到insert buffer B+樹的葉子節點中。

對于插入到insert buffer B+樹葉子節點的記錄,需要根據如下規則進行構造:

揭秘InnoDB插入緩沖:提升非唯一輔助索引插入性能的秘密武器

space、marker、page_ no 字段和之前非葉節點中的含義相同,一共占用9位元組。第 4個字段 metadata 占用4位元組,其存儲的内容如下圖所示。

揭秘InnoDB插入緩沖:提升非唯一輔助索引插入性能的秘密武器

IBUF_REC_OFFSET_COUNT 是儲存兩個位元組的整數,用來排序每個記錄進入 Insert Buffer 的順序。因為從 InnoDB1.0.x 開始支援 Change Buffer,是以這個值同樣記錄進入 Insert Buffer 的順序。通過這個順序回放(replay)才能得到記錄的正确值。

Secondary index record 記錄的是插入的資料,相較于原插入資料,insert buffer B+樹葉子節點額外多了 13個位元組的開銷。為何要如此設定呢?

因為啟用 insert buffer索引後,輔助索引頁(space、page_no)中的記錄可能被插入到insert buffer B+樹中,是以為了保證每次merge insert buffer頁必須成功,還需要有一個特殊的頁來标記每個輔助索引頁(space、page_no)的可用空間。這個頁的類型為insert buffer bitmap。

每個 Insert Buffer Bitmap頁用來追蹤16384個輔助索引頁,也就是256個區 (Extent )。每個 Insert Buffer Bitmap 頁都在16384個頁的第二個頁中。關于 Insert Buffer Bitmap 頁的作用會在下一小節中詳細介紹。

相關知識點:

每個區64頁,一頁16KB,一個區所占大小為: 16 * 64 = 1024KB = 1M 256個區,一個區64頁,則一個BiMap可以追蹤的頁為:256 * 64 = 16384

每個輔助索引頁在 Insert Buffer Bitmap 頁中占用4位(bit),由三個部分組成。

揭秘InnoDB插入緩沖:提升非唯一輔助索引插入性能的秘密武器

Merge insert buffer

merge insert buffer的操作可能發生在以下幾種情況:

(1)輔助索引頁被讀取到緩沖池時;

(2)insert buffer bitmap頁追蹤到該輔助索引頁已無可用空間時;

(3)master thread。

第一種情況好了解,如果該輔助索引頁是否有記錄存放于Insert Buffer B+樹中,即存在髒頁,輔助索引頁被讀取到緩沖池時,那麼就需要進行資料同步。可以通過檢查 Insert BufferBitMap頁中的 IBF_BITMAP_BUFFERED,最終将髒頁資料更新到輔助索引頁中。某些情況下,對該頁多次的記錄操作可以通過一次操作合并到了原有的輔助索引頁中,是以性能會有大幅提高。

第二種情況,insert buffer bitmap 追蹤到該輔助索引頁已無可用空間時(啟用insert buffer後,輔助索引頁中的記錄可能被插入到insert buffer B+樹中,為了保證每次的merge insert buffer頁成功,通過insert buffer bitmap類型的特殊頁來記錄輔助索引頁的可用空間),并至少得有1/32 的可用空間。若往輔助索引頁插入一條記錄後,發現空間少于 1/32 ,則會強制讀取輔助索引頁,将Insert Buffer B+樹中,該頁的記錄插入到輔助索引頁中。

為什麼輔助索引頁的可用空間要至少大于 1/32 的空間,這個門檻值是如何确定的?

個人了解如下:

按照一般情況來說,一頁為16K,16 * 1024 / 32 = 512 B。從磁盤的實體結構來看存取資訊的最小機關是扇區,一個扇區大小為 512 B。

在MySQL的内部實作中,資料被組織為資料頁,每個資料頁通常由多個連續的扇區組成。當MySQL需要讀取或寫入資料時,它會以扇區為機關進行磁盤IO操作。讀取或寫入的最小單元是一個或多個扇區。

這個門檻值的選擇是基于性能和空間利用的考慮。當輔助索引頁的空間少于1/32時,可能意味着輔助索引頁的空間已經相對較小,無法容納更多的記錄。如果不及時進行合并操作,後續插入操作可能會帶來頁分裂等問題。

第三種情況,此 master thread 線程中每秒或者每10秒進行一次merge insert buffer操作,不同之處在于每次merge的數量不一樣。

疑問

1、插入緩沖的節點中offset表示插入緩沖中的頁在磁盤資料檔案中的偏移量,在執行插入操作時,是如何得知這一資訊的?是否需要進行I/O操作?

2、插入緩沖的葉子節點中IBUF_REC_OFFSET_COUNT 字段表示每個記錄進入 Insert Buffer 的順序,既然葉子節點有序,為什麼還需要該字段呢?

插入緩沖帶來的問題

插入緩沖主要帶來如下兩個壞處:

1、可能導緻資料庫當機後執行個體恢複時間變長。如果應用程式執行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出現當機,這時就有大量記憶體中的插入緩沖區資料沒有合并至索引頁中,導緻執行個體恢複時間會很長。

2、在寫密集的情況下,插入緩沖會占用過多的緩沖池記憶體(innodb_buffer_pool),預設情況下最大可以占用1/2,這在實際應用中會帶來一定的問題。

插入緩沖的更新:change buffer

InnoDB從1.0.x版本開始引入了change buffer,可以将其視為insert buffer的更新。從這個版本開始,InnoDB存儲引擎可以對DML操作——insert、delete、update都進行緩沖,它們分别是:insert buffer、delete buffer、purge buffer。

和insert buffer一樣,change buffer适用的對象依然是非唯一的輔助索引。

對一條記錄進行update操作可以分為兩個過程:

(1)将記錄标記為删除;

(2)真正将記錄删除。

是以delete buffer對應update操作的第一個過程,即将記錄标記為删除。purge buffer對應update操作的第二個過程,即将記錄真正的删除。同時InnoDB存儲引擎提供了參數innodb_change_buffering,用來開啟各種buffer選項。該參數的可選值為:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨論過的三種情況。changes表示啟用inserts和deletes,all表示啟用所有,none表示都不啟用。

從 InnoDB 1.2.x版本開始,可以通過參數 innodb_change_buffer_max_size 來控制 Change Buffer 最大使用記憶體的數量:

SQL複制代碼mysql> show variables like 'innodb_change_buffer_max_size';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
+-------------------------------+-------+
1 row in set (0.18 sec)
           

innodb_change_buffer_max_size 值預設為25,表示最多使用1/4 的緩沖池記憶體空間。而需要注意的是,該參數的最大有效值為50。

關于 change buffer 的狀态資訊,仍然通過下面指令檢視:

SQL複制代碼show engine innodb status;