Mysql頁分裂

寫文章mysql

InnoDB中的頁合併與分裂

InnoDB中的頁合併與分裂

胖懶鴨

胖懶鴨git

Python Web後端 / 努力成爲Redis磚家github

15 人贊同了該文章sql

原文標題:InnoDB Page Merging and Page Splitting
原文連接: https://www.percona.com/blog/2017/04/10/innodb-page-merging-and-page-splitting/
做者:Marco Tusa
譯者:2014BDuck
博客地址: https://blog.2014bduck.com/archives/260
翻譯時間:2019-12-22

若是你找過任何一位MySQL顧問,問他對你的語句和/或數據庫設計的建議,我保證他會跟你講主鍵設計的重要性。特別是在使用InnoDB引擎的情景,他們確定會給你解釋索引合併和頁分裂這些。這兩個方面與性能息息相關,你應該在任何設計索引(不止是主鍵索引)的時候都將他們考慮在內。數據庫

你可能以爲這些聽起來挺莫名其妙,沒準你也沒錯。這不是容易的事,特別是講到關於內部實現的時候。一般你都不會須要處理這些事情,而且你也不想去着手他們。後端

可是有時候這些問題又是必須搞清楚的。若是有這種狀況,那這篇文章正適合你。ruby

我嘗試用這篇文章將一些最不清晰、InnoDB內部的操做解釋清楚:索引頁的建立、頁合併和頁分裂。併發

在InnoDB中,數據即索引(譯註:索引組織數據)。你可能聽過這種說法,但它具體是什麼樣的?app

文件表(File-Table)結構

假設你已經裝好了MySQL最新的5.7版本(譯註:文章發佈於17年4月),而且你建立了一個windmills庫(schema)和wmills表。在文件目錄(一般是/var/lib/mysql/)你會看到如下內容:dom

data/
  windmills/
      wmills.ibd
      wmills.frm

這是由於從MySQL 5.6版本開始innodb_file_per_table參數默認設置爲1。該配置下你的每個表都會單獨做爲一個文件存儲(若是有分區也可能有多個文件)。

目錄下要注意的是這個叫wmills.ibd的文件。這個文件由多個段(segments)組成,每一個段和一個索引相關。

文件的結構是不會隨着數據行的刪除而變化的,但段則會跟着構成它的更小一級單位——區的變化而變化。區僅存在於段內,而且每一個區都是固定的1MB大小(頁體積默認的狀況下)。頁則是區的下一級構成單位,默認體積爲16KB。

按這樣算,一個區能夠容納最多64個頁,一個頁能夠容納2-N個行。行的數量取決於它的大小,由你的表結構定義。InnoDB要求頁至少要有兩個行,所以能夠算出行的大小最多爲8000 bytes。

聽起來就像俄羅斯娃娃(Matryoshka dolls)同樣是麼,沒錯!下面這張圖能幫助你理解:

根,分支與葉子

每一個頁(邏輯上講即葉子節點)是包含了2-N行數據,根據主鍵排列。樹有着特殊的頁區管理不一樣的分支,即內部節點(INodes)。

上圖僅爲示例,後文纔是真實的結構描述。

具體來看一下:

ROOT NODE #3: 4 records, 68 bytes
 NODE POINTER RECORD ≥ (id=2) → #197
 INTERNAL NODE #197: 464 records, 7888 bytes
 NODE POINTER RECORD ≥ (id=2) → #5
 LEAF NODE #5: 57 records, 7524 bytes
 RECORD: (id=2) → (uuid="884e471c-0e82-11e7-8bf6-08002734ed50", millid=139, kwatts_s=1956, date="2017-05-01", location="For beauty's pattern to succeeding men.Yet do thy", active=1, time="2017-03-21 22:05:45", strrecordtype="Wit")

下面是表結構:

CREATE TABLE `wmills` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `uuid` char(36) COLLATE utf8_bin NOT NULL,
  `millid` smallint(6) NOT NULL,
  `kwatts_s` int(11) NOT NULL,
  `date` date NOT NULL,
  `location` varchar(50) COLLATE utf8_bin DEFAULT NULL,
  `active` tinyint(2) NOT NULL DEFAULT '1',
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `strrecordtype` char(3) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_millid` (`millid`)
) ENGINE=InnoDB;

全部的B樹都有着一個入口,也就是根節點,在上圖中#3就是根節點。根節點(頁)包含了如索引ID、INodes數量等信息。INode頁包含了關於頁自己的信息、值的範圍等。最後還有葉子節點,也就是咱們數據實際所在的位置。在示例中,咱們能夠看到葉子節點#5有57行記錄,共7524 bytes。在這行信息後是具體的記錄,能夠看到數據行內容。

這裏想引出的概念是當你使用InnoDB管理表和行,InnoDB會將他們會以分支、頁和記錄的形式組織起來。InnoDB不是按行的來操做的,它可操做的最小粒度是頁,頁加載進內存後纔會經過掃描頁來獲取行/記錄。

如今頁的結構清楚了嗎?好,咱們繼續。

頁的內部原理

頁能夠空或者填充滿(100%),行記錄會按照主鍵順序來排列。例如在使用AUTO_INCREMENT時,你會有順序的ID 一、二、三、4等。

頁還有另外一個重要的屬性:MERGE_THRESHOLD。該參數的默認值是50%頁的大小,它在InnoDB的合併操做中扮演了很重要的角色。

當你插入數據時,若是數據(大小)可以放的進頁中的話,那他們是按順序將頁填滿的。

若當前頁滿,則下一行記錄會被插入下一頁(NEXT)中。

根據B樹的特性,它能夠自頂向下遍歷,但也能夠在各葉子節點水平遍歷。由於每一個葉子節點都有着一個指向包含下一條(順序)記錄的頁的指針。

例如,頁#5有指向頁#6的指針,頁#6有指向前一頁(#5)的指針和後一頁(#7)的指針。

這種機制下能夠作到快速的順序掃描(如範圍掃描)。以前提到過,這就是當你基於自增主鍵進行插入的狀況。但若是你不只插入還進行刪除呢?

頁合併

當你刪了一行記錄時,實際上記錄並無被物理刪除,記錄被標記(flaged)爲刪除而且它的空間變得容許被其餘記錄聲明使用。

當頁中刪除的記錄達到MERGE_THRESHOLD(默認頁體積的50%),InnoDB會開始尋找最靠近的頁(前或後)看看是否能夠將兩個頁合併以優化空間使用。

在示例中,頁#6使用了不到一半的空間,頁#5又有足夠的刪除數量,如今一樣處於50%使用如下。從InnoDB的角度來看,它們可以進行合併。

合併操做使得頁#5保留它以前的數據,而且容納來自頁#6的數據。頁#6變成一個空頁,能夠接納新數據。

若是咱們在UPDATE操做中讓頁中數據體積達到相似的閾值點,InnoDB也會進行同樣的操做。

規則就是:頁合併發生在刪除或更新操做中,關聯到當前頁的相鄰頁。若是頁合併成功,在INFOMATION_SCHEMA.INNODB_METRICS中的index_page_merge_successful將會增長。

頁分裂

前面提到,頁可能填充至100%,在頁填滿了以後,下一頁會繼續接管新的記錄。但若是有下面這種狀況呢?

頁#10沒有足夠空間去容納新(或更新)的記錄。根據「下一頁」的邏輯,記錄應該由頁#11負責。然而:

頁#11也一樣滿了,數據也不可能不按順序地插入。怎麼辦?

還記得以前說的鏈表嗎(譯註:指B+樹的每一層都是雙向鏈表)?頁#10有指向頁#9和頁#11的指針。

InnoDB的作法是(簡化版):

  1. 建立新頁
  2. 判斷當前頁(頁#10)能夠從哪裏進行分裂(記錄行層面)
  3. 移動記錄行
  4. 從新定義頁之間的關係

新的頁#12被建立:

頁#11保持原樣,只有頁之間的關係發生了改變:

  • 頁#10相鄰的前一頁爲頁#9,後一頁爲頁#12
  • 頁#12相鄰的前一頁爲頁#10,後一頁爲頁#11
  • 頁#11相鄰的前一頁爲頁#10,後一頁爲頁#13

(譯註:頁#13可能原本就有,這裏意思爲頁#10與頁#11之間插入了頁#12)

這樣B樹水平方向的一致性仍然知足,由於知足原定的順序排列邏輯。然而從物理存儲上講頁是亂序的,並且大機率會落到不一樣的區。

規律總結:頁分裂會發生在插入或更新,而且形成頁的錯位(dislocation,落入不一樣的區)

InnoDB用INFORMATION_SCHEMA.INNODB_METRICS表來跟蹤頁的分裂數。能夠查看其中的index_page_splitsindex_page_reorg_attempts/successful統計。

一旦建立分裂的頁,惟一(譯註:實則仍有其餘方法,見下文)將原先順序恢復的辦法就是新分裂出來的頁由於低於合併閾值(merge threshold)被刪掉。這時候InnoDB用頁合併將數據合併回來。

另外一種方式就是用OPTIMIZE從新整理表。這多是個很重量級和耗時的過程,但多是惟一將大量分佈在不一樣區的頁理順的方法。

另外一方面,要記住在合併和分裂的過程,InnoDB會在索引樹上加寫鎖(x-latch)。在操做頻繁的系統中這可能會是個隱患。它可能會致使索引的鎖爭用(index latch contention)。若是表中沒有合併和分裂(也就是寫操做)的操做,稱爲「樂觀」更新,只須要使用讀鎖(S)。帶有合併也分裂操做則稱爲「悲觀」更新,使用寫鎖(X)。

個人主鍵

好的主鍵不只對於數據查找很重要,並且也影響寫操做時數據在區上的分佈(也就是與頁分裂和頁合併操做相關)。

在第一個測試中我使用的是是自增主鍵,第二個測試主鍵是基於一個1-200的ID與自增值的,第三個測試也是1-200的ID不過與UUID聯合。

插入操做時,InnoDB須要增長頁,視爲「分裂」操做:

表現因不一樣主鍵而異。

在頭兩種狀況中數據的分佈更爲緊湊,也就是說他們擁有更好的空間利用率。對比半隨機(semi-random)特性的UUID會致使明顯的頁稀疏分佈(頁數量更多,相關分裂操做更多)。

在頁合併的狀況中,嘗試合併的次數因主鍵類型的不一樣而表現得更加不一致。

在插入-更新-刪除操做中,自增主鍵有更少的合併嘗試次數,成功比例比其餘兩種類型低9.45%。UUID型主鍵(圖表的右一側)有更多的合併嘗試,可是合併成功率明顯更高,達22.34%,由於數據稀疏分佈讓不少頁都有部分空閒空間。

在輔助索引與上面主鍵索引類似的狀況下,測試的表現也是相似的。

總結

MySQL/InnoDB不斷地進行這些操做,你可能只能瞭解到不多的信息。但他們可能給你形成傷害,特別是比起用SSD,你還在用傳統的機械存儲(spindle storage)的時候(順便提一下SSD會有另外的問題)。

壞消息就是咱們用什麼參數或者魔法去改變服務端。但好消息是咱們能夠在設計的時候作不少(有幫助)的事。

恰當地使用主鍵和設計輔助索引,而且記住不要濫用(索引)。若是你已經預計到會有不少插入/刪除/更新操做,規劃一個合適的時間窗來管理(整理)表。

有個很重要的點,InnoDB中你不會有斷斷續續的行記錄,可是你會在頁-區的維度上遇到這些問題。忽略表的管理工做會致使須要在IO層面、內存層面和InnoDB緩衝池層面作更多工做。

你必須不時(at regular intervals)重建一些表。能夠採用一些技巧,好比分區和外部的工具(pt-osc)。不要讓表變得過大和過於碎片化(fragmented)。

磁盤空間浪費?須要讀多個表去獲取須要的數據而不是一次搞定?每次搜索致使明顯更多的讀操做?那是你的鍋,不要找藉口!

Happy MySQL to everyone!

感謝

Laurynas Biveinis: 感謝花時間向我解釋一些內部實現。

Jeremy Cole: 感謝他的項目InnoDB_ruby (我常常用上)。

發佈於 2019-12-22

[

Innodb

](https://www.zhihu.com/topic/1...

[

MySQL

](https://www.zhihu.com/topic/1...

相關文章
相關標籤/搜索