寫文章mysql
胖懶鴨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
假設你已經裝好了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的作法是(簡化版):
新的頁#12被建立:
頁#11保持原樣,只有頁之間的關係發生了改變:
(譯註:頁#13可能原本就有,這裏意思爲頁#10與頁#11之間插入了頁#12)
這樣B樹水平方向的一致性仍然知足,由於知足原定的順序排列邏輯。然而從物理存儲上講頁是亂序的,並且大機率會落到不一樣的區。
規律總結:頁分裂會發生在插入或更新,而且形成頁的錯位(dislocation,落入不一樣的區)
InnoDB用INFORMATION_SCHEMA.INNODB_METRICS
表來跟蹤頁的分裂數。能夠查看其中的index_page_splits
和index_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...