1. 程式人生 > >從MySQL Bug#67718淺談B+樹索引的分裂優化(轉)

從MySQL Bug#67718淺談B+樹索引的分裂優化(轉)

原文連結:http://hedengcheng.com/?p=525

 

問題背景

今天,看到Twitter的DBA團隊釋出了其最新的MySQL分支:Changes in Twitter MySQL 5.5.28.t9,此分支最重要的一個改進,就是修復了MySQL 的Bug #67718:InnoDB drastically under-fills pages in certain conditions。關於此Bug的詳細描述,以及如何重現此問題,可以閱讀以上的Bug連結,以下簡單描述下此Bug對應的問題:

 

InnoDB的索引分裂策略,在特定的情況下,索引頁面的分裂存在問題,導致每個分裂出來的頁面,僅僅儲存一條記錄,頁面的空間利用率極低。

 

此Bug引起了我的興趣,因此準備跟大家簡單聊聊B+樹索引的結構、B+樹的分裂、B+樹分裂操作的優化、Bug #67718的成因,以及個人對如何修復此Bug的一些建議等。

 

 

B+樹索引結構

傳統關係型資料庫(Oracle/MySQL/PostgreSQL…),其主要的索引結構,使用的都是B+樹。更有甚者,InnoDB引擎的表資料,整個都是以B+樹的組織形式存放的。下圖,是一個經典的B+樹組織結構圖(2層B+樹,每個頁面的扇出為4):

 

注意:

  • 此B+樹,以InnoDB實現的B+樹結構為準;
  • 此B+樹,有5條使用者記錄,分別是1,2,3,4,5;
  • B+樹上層頁面中的記錄,儲存的是下層頁面中的最小值(Low Key);
  • B+樹的所有資料,均儲存在B+樹的葉節點;
  • B+樹葉節點的所有頁面,通過雙向連結串列連結起來;

 

B+樹的分裂

在上圖B+樹的基礎上,繼續插入記錄6,7,B+樹結構會產生以下的一系列變化:

插入記錄6,新的B+樹結構如下:

 

插入記錄7,由於葉頁面中只能存放4條記錄,插入記錄7,導致葉頁面分裂,產生一個新的葉頁面。

 

傳統B+樹頁面分裂操作分析:

  • 按照原頁面中50%的資料量進行分裂,針對當前這個分裂操作,3,4記錄保留在原有頁面,5,6記錄,移動到新的頁面。最後將新紀錄7插入到新的頁面中;
  • 50%分裂策略的優勢:
    • 分裂之後,兩個頁面的空間利用率是一樣的;如果新的插入是隨機在兩個頁面中挑選進行,那麼下一次分裂的操作就會更晚觸發;
  • 50%分裂策略的劣勢:
    • 空間利用率不高:按照傳統50%的頁面分裂策略,索引頁面的空間利用率在50%左右;
    • 分裂頻率較大:針對如上所示的遞增插入(遞減插入),每新插入兩條記錄,就會導致最右的葉頁面再次發生分裂;

 

疑問:

傳統50%分裂的策略,有不足之處,如何優化?接著往下看。

 

B+樹分裂操作的優化

由於傳統50%分裂的策略,有不足之處,因此,目前所有的關係型資料庫,包括Oracle/InnoDB/PostgreSQL,以及本人以前參與研發的Oscar資料庫,目前正在研發的NTSE、TNT儲存引擎,都針對B+樹索引的遞增/遞減插入進行了優化。經過優化,以上的B+樹索引,在記錄6插入完畢,記錄7插入引起分裂之後,新的B+樹結構如下圖所示:

 

對比上下兩個插入記錄7之後,B+樹索引的結構圖,可以發現二者有很多的不同之處:

  • 新的分裂策略,在插入7時,不移動原有頁面的任何記錄,只是將新插入的記錄7寫到新頁面之中;
  • 原有頁面的利用率,仍舊是100%;
  • 優化分裂策略的優勢:
    • 索引分裂的代價小:不需要移動記錄;
    • 索引分裂的概率降低:如果接下來的插入,仍舊是遞增插入,那麼需要插入4條記錄,才能再次引起頁面的分裂。相對於50%分裂策略,分裂的概率降低了一半;
    • 索引頁面的空間利用率提高:新的分裂策略,能夠保證分裂前的頁面,仍舊保持100%的利用率,提高了索引的空間利用率;
  • 優化分裂策略的劣勢:
    • 如果新的插入,不再滿足遞增插入的條件,而是插入到原有頁面,那麼就會導致原有頁面再次分裂,增加了分裂的概率。

 

因此,此優化分裂策略,僅僅是針對遞增遞減插入有效,針對隨機插入,就失去了優化的意義,反而帶來了更高的分裂概率。

 

在InnoDB的實現中,為每個索引頁面維護了一個上次插入的位置,以及上次的插入是遞增/遞減的標識。根據這些資訊,InnoDB能夠判斷出新插入到頁面中的記錄,是否仍舊滿足遞增/遞減的約束,若滿足約束,則採用優化後的分裂策略;若不滿足約束,則退回到50%的分裂策略。

 

但是,InnoDB的實現,有不足之處,會導致下面提到的一個Bug。

 

Bug#67718的成因

在Bug#67718中提到,在特定的插入情況下,InnoDB的索引頁面利用率極低,這是由於InnoDB不正確的使用優化分裂策略導致的。

考慮以下的一個B+樹,已有的使用者資料是1,2,3,4,5,6,100,並且在插入記錄100之後,引起索引頁面分裂,記錄100在分裂後被插入到新的頁面:

 

由於插入100能夠滿足遞增的判斷條件,因此採用了優化分裂策略,分裂不移動資料,新紀錄100插入到新頁面之中,原有頁面的最後插入位置仍舊是6號記錄不變,原有頁面仍舊保持遞增的插入標識不變。

此時,考慮連續插入9,8,7這幾條記錄,會得到什麼樣的B+樹?此時,全域性遞增插入變為全域性遞減插入。

插入記錄9後的B+樹結構:

由於InnoDB的B+樹,上層節點儲存的是下層頁面中的最小值(Low Key),因此記錄9仍舊會插入到【3,4,5,6】頁面,此時頁面已滿,需要分裂。而且判斷出記錄9仍舊滿足頁面中的遞增判斷條件(Last_Insert_Pos = 6,9插入到6之後,並且原來是遞增插入的)。因此,採用優化的分裂策略,產生新的頁面插入記錄9,原有頁面記錄保持不變。

 

插入記錄8後的B+樹結構:

 

插入記錄7,也一樣。採用優化的分裂策略,記錄7獨佔一個頁面。

 

分析:

  • Bug#67718的主要副作用
    • 是頁面的利用率極低,每個索引葉頁面,只能存放一條記錄;
  • Bug#67718的主要原因
    • InnoDB錯誤的採用了優化的索引分裂策略。InnoDB判斷是否滿足遞增/遞減的插入模式,採用的是頁面級的判斷,哪怕全域性的模式發生了變化,只要頁面內記錄的模式未變,仍舊會選擇優化後的索引分裂策略;  

修復Bug#67718的建議

在本人做Oscar資料庫的索引分裂優化時,當時也同樣碰到了此問題。當時的解決方案是:每次分裂,若插入的記錄是頁面中的最後一條記錄,則至少將此記錄前一條記錄分裂到新頁面之中。採用此策略,針對100,9,8這一個系列的插入,會產生以下的系列B+樹:

插入100,9,8後的B+樹:

 

插入100時,移動原有頁面最後一條記錄到新的頁面(將6移動到新頁面),此時新頁面中的記錄為【6,100】。接下來插入9,8,都會插入到新的頁面之中,不會產生分裂操作,空間利用率提高,減少了索引頁面分裂,解決了Bug#67718的問題。

 

當然,肯定還有更優的策略,歡迎感興趣的朋友們一起討論!