MySQL · 引擎特性 · InnoDB 檔案系統之檔案物理結構
綜述
從上層的角度來看,InnoDB層的檔案,除了redo日誌外,基本上具有相當統一的結構,都是固定block大小,普遍使用的btree結構來管理資料。只是針對不同的block的應用場景會分配不同的頁型別。通常預設情況下,每個block的大小為 UNIV_PAGE_SIZE,在不做任何配置時值為16kb,你還可以選擇在安裝例項時指定一個塊的block大小。對於壓縮表,可以在建表時指定block size,但在記憶體中表現的解壓頁依舊為統一的頁大小。
從物理檔案的分類來看,有日誌檔案、主系統表空間檔案ibdata、undo tablespace檔案、臨時表空間檔案、使用者表空間。
日誌檔案主要用於記錄redo log,InnoDB採用迴圈使用的方式,你可以通過引數指定建立檔案的個數和每個檔案的大小。預設情況下,日誌是以512位元組的block單位寫入。由於現代檔案系統的block size通常設定到4k,InnoDB提供了一個選項,可以讓使用者將寫入的redo日誌填充到4KB,以避免read-modify-write的現象;而Percona Server則提供了另外一個選項,支援直接將redo日誌的block size修改成指定的值。
ibdata是InnoDB最重要的系統表空間檔案,它記錄了InnoDB的核心資訊,包括事務系統資訊、元資料資訊,記錄InnoDB change buffer的btree,防止資料損壞的double write buffer等等關鍵資訊。我們稍後會展開描述。
undo獨立表空間是一個可選項,通常預設情況下,undo資料是儲存在ibdata中的,但你也可以通過配置選項 innodb_undo_tablespaces
來將undo 回滾段分配到不同的檔案中,目前開啟undo tablespace 只能在install階段進行。在主流版本進入5.7時代後,我們建議開啟獨立undo表空間,只有這樣才能利用到5.7引入的新特效:online undo truncate。
MySQL 5.7 新開闢了一個臨時表空間,預設的磁碟檔案命名為ibtmp1,所有非壓縮的臨時表都儲存在該表空間中。由於臨時表的本身屬性,該檔案在重啟時會重新建立。對於雲服務提供商而言,通過ibtmp檔案,可以更好的控制臨時檔案產生的磁碟儲存。
使用者表空間,顧名思義,就是用於自己建立的表空間,通常分為兩類,一類是一個表空間一個檔案,另外一種則是5.7版本引入的所謂General Tablespace,在滿足一定約束條件下,可以將多個表建立到同一個檔案中。除此之外,InnoDB還定義了一些特殊用途的ibd檔案,例如全文索引相關的表文件。而針對空間資料型別,也構建了不同的資料索引格式R-tree。
在關鍵的地方本文註明了程式碼函式,建議讀者邊參考程式碼邊閱讀本文,本文的程式碼部分基於MySQL 5.7.11版本,不同的版本函式名或邏輯可能會有所不同。請讀者閱讀本文時儘量選擇該版本的程式碼。
檔案管理頁
InnoDB 的每個資料檔案都歸屬於一個表空間,不同的表空間使用一個唯一標識的space id來標記。例如ibdata1, ibdata2… 歸屬系統表空間,擁有相同的space id。使用者建立表產生的ibd檔案,則認為是一個獨立的tablespace,只包含一個檔案。
每個檔案按照固定的 page size 進行區分,預設情況下,非壓縮表的page size為16Kb。而在檔案內部又按照64個Page(總共1M)一個Extent的方式進行劃分並管理。對於不同的page size,對應的Extent大小也不同,對應為:
page size | file space extent size |
---|---|
4 KiB | 256 pages = 1 MiB |
8 KiB | 128 pages = 1 MiB |
16 KiB | 64 pages = 1 MiB |
32 KiB | 64 pages = 2 MiB |
64 KiB | 64 pages = 4 MiB |
儘管支援更大的Page Size,但目前還不支援大頁場景下的資料壓縮,原因是這涉及到修改壓縮頁中slot的固定size(其實實現起來也不復雜)。在不做宣告的情況下,下文我們預設使用16KB的Page Size來闡述檔案的物理結構。
為了管理整個Tablespace,除了索引頁外,資料檔案中還包含了多種管理頁,如下圖所示,一個使用者表空間大約包含這些頁來管理檔案,下面會一一進行介紹。
檔案連結串列
首先我們先介紹基於檔案的一個基礎結構,即檔案連結串列。為了管理Page,Extent這些資料塊,在檔案中記錄了許多的節點以維持具有某些特徵的連結串列,例如在在檔案頭維護的inode page連結串列,空閒、用滿以及碎片化的Extent連結串列等等。
在InnoDB裡連結串列頭稱為FLST_BASE_NODE
,大小為FLST_BASE_NODE_SIZE
(16個位元組)。BASE NODE維護了連結串列的頭指標和末尾指標,每個節點稱為FLST_NODE
,大小為FLST_NODE_SIZE
(12個位元組)。相關結構描述如下:
FLST_BASE_NODE
:
Macro | bytes | Desc |
---|---|---|
FLST_LEN | 4 | 儲存連結串列的長度 |
FLST_FIRST | 6 | 指向連結串列的第一個節點 |
FLST_LAST | 6 | 指向連結串列的最後一個節點 |
FLST_NODE
:
Macro | bytes | Desc |
---|---|---|
FLST_PREV | 6 | 指向當前節點的前一個節點 |
FLST_NEXT | 6 | 指向當前節點的下一個節點 |
如上所述,檔案連結串列中使用6個位元組來作為節點指標,指標的內容包括:
Macro | bytes | Desc |
---|---|---|
FIL_ADDR_PAGE | 4 | Page No |
FIL_ADDR_BYTE | 2 | Page內的偏移量 |
該連結串列結構是InnoDB表空間內管理所有page的基礎結構,下圖先感受下,具體的內容可以繼續往下閱讀。
檔案連結串列管理的相關程式碼參閱:include/fut0lst.ic, fut/fut0lst.cc
FSP_HDR PAGE
資料檔案的第一個Page型別為FIL_PAGE_TYPE_FSP_HDR
,在建立一個新的表空間時進行初始化(fsp_header_init
),該page同時用於跟蹤隨後的256個Extent(約256MB檔案大小)的空間管理,所以每隔256MB就要建立一個類似的資料頁,型別為FIL_PAGE_TYPE_XDES
,XDES Page除了檔案頭部外,其他都和FSP_HDR
頁具有相同的資料結構,可以稱之為Extent描述頁,每個Extent佔用40個位元組,一個XDES
Page最多描述256個Extent。
FSP_HDR
頁的頭部使用FSP_HEADER_SIZE
個位元組來記錄檔案的相關資訊,具體的包括:
Macro | bytes | Desc |
---|---|---|
FSP_SPACE_ID | 4 | 該檔案對應的space id |
FSP_NOT_USED | 4 | 如其名,保留位元組,當前未使用 |
FSP_SIZE | 4 | 當前表空間總的PAGE個數,擴充套件檔案時需要更新該值(fsp_try_extend_data_file_with_pages ) |
FSP_FREE_LIMIT | 4 | 當前尚未初始化的最小Page No。從該Page往後的都尚未加入到表空間的FREE LIST上。 |
FSP_SPACE_FLAGS | 4 | 當前表空間的FLAG資訊,見下文 |
FSP_FRAG_N_USED | 4 | FSP_FREE_FRAG連結串列上已被使用的Page數,用於快速計算該連結串列上可用空閒Page數 |
FSP_FREE | 16 | 當一個Extent中所有page都未被使用時,放到該連結串列上,可以用於隨後的分配 |
FSP_FREE_FRAG | 16 | FREE_FRAG連結串列的Base Node,通常這樣的Extent中的Page可能歸屬於不同的segment,用於segment frag array page的分配(見下文) |
FSP_FULL_FRAG | 16 | Extent中所有的page都被使用掉時,會放到該連結串列上,當有Page從該Extent釋放時,則移回FREE_FRAG連結串列 |
FSP_SEG_ID | 8 | 當前檔案中最大Segment ID + 1,用於段分配時的seg id計數器 |
FSP_SEG_INODES_FULL | 16 | 已被完全用滿的Inode Page連結串列 |
FSP_SEG_INODES_FREE | 16 | 至少存在一個空閒Inode Entry的Inode Page被放到該連結串列上 |
在檔案頭使用FLAG(對應上述FSP_SPACE_FLAGS
)描述了建立表時的如下關鍵資訊:
Macro | Desc |
---|---|
FSP_FLAGS_POS_ZIP_SSIZE | 壓縮頁的block size,如果為0表示非壓縮表 |
FSP_FLAGS_POS_ATOMIC_BLOBS | 使用的是compressed或者dynamic的行格式 |
FSP_FLAGS_POS_PAGE_SSIZE | Page Size |
FSP_FLAGS_POS_DATA_DIR | 如果該表空間顯式指定了data_dir,則設定該flag |
FSP_FLAGS_POS_SHARED | 是否是共享的表空間,如5.7引入的General Tablespace,可以在一個表空間中建立多個表 |
FSP_FLAGS_POS_TEMPORARY | 是否是臨時表空間 |
FSP_FLAGS_POS_ENCRYPTION | 是否是加密的表空間,MySQL 5.7.11引入 |
FSP_FLAGS_POS_UNUSED | 未使用的位 |
除了上述描述資訊外,其他部分的資料結構和XDES PAGE(FIL_PAGE_TYPE_XDES
)都是相同的,使用連續陣列的方式,每個XDES PAGE最多儲存256個XDES Entry,每個Entry佔用40個位元組,描述64個Page(即一個Extent)。格式如下:
Macro | bytes | Desc |
---|---|---|
XDES_ID | 8 | 如果該Extent歸屬某個segment的話,則記錄其ID |
XDES_FLST_NODE | 12(FLST_NODE_SIZE) | 維持Extent連結串列的雙向指標節點 |
XDES_STATE | 4 | 該Extent的狀態資訊,包括:XDES_FREE,XDES_FREE_FRAG,XDES_FULL_FRAG,XDES_FSEG,詳解見下文 |
XDES_BITMAP | 16 | 總共16*8= 128個bit,用2個bit表示Extent中的一個page,一個bit表示該page是否是空閒的(XDES_FREE_BIT),另一個保留位,尚未使用(XDES_CLEAN_BIT) |
XDES_STATE
表示該Extent的四種不同狀態:
Macro | Desc |
---|---|
XDES_FREE(1) | 存在於FREE連結串列上 |
XDES_FREE_FRAG(2) | 存在於FREE_FRAG連結串列上 |
XDES_FULL_FRAG(3) | 存在於FULL_FRAG連結串列上 |
XDES_FSEG(4) | 該Extent歸屬於ID為XDES_ID記錄的值的SEGMENT。 |
通過XDES_STATE
資訊,我們只需要一個FLIST_NODE
節點就可以維護每個Extent的資訊,是處於全域性表空間的連結串列上,還是某個btree segment的連結串列上。
IBUF BITMAP PAGE
第2個page型別為FIL_PAGE_IBUF_BITMAP
,主要用於跟蹤隨後的每個page的change buffer資訊,使用4個bit來描述每個page的change buffer資訊。
Macro | bits | Desc |
---|---|---|
IBUF_BITMAP_FREE | 2 | 使用2個bit來描述page的空閒空間範圍:0(0 bytes)、1(512 bytes)、2(1024 bytes)、3(2048 bytes) |
IBUF_BITMAP_BUFFERED | 1 | 是否有ibuf操作快取 |
IBUF_BITMAP_IBUF | 1 | 該Page本身是否是Ibuf Btree的節點 |
由於bitmap page的空間有限,同樣每隔256個Extent Page之後,也會在XDES PAGE之後建立一個ibuf bitmap page。
INODE PAGE
資料檔案的第3個page的型別為FIL_PAGE_INODE
,用於管理資料檔案中的segement,每個索引佔用2個segment,分別用於管理葉子節點和非葉子節點。每個inode頁可以儲存FSP_SEG_INODES_PER_PAGE
(預設為85)個記錄。
Macro | bits | Desc |
---|---|---|
FSEG_INODE_PAGE_NODE | 12 | INODE頁的連結串列節點,記錄前後Inode Page的位置,BaseNode記錄在頭Page的FSP_SEG_INODES_FULL或者FSP_SEG_INODES_FREE欄位。 |
Inode Entry 0 | 192 | Inode記錄 |
Inode Entry 1 | ||
…… | ||
Inode Entry 84 |
每個Inode Entry的結構如下表所示:
Macro | bits | Desc |
---|---|---|
FSEG_ID | 8 | 該Inode歸屬的Segment ID,若值為0表示該slot未被使用 |
FSEG_NOT_FULL_N_USED | 8 | FSEG_NOT_FULL連結串列上被使用的Page數量 |
FSEG_FREE | 16 | 完全沒有被使用並分配給該Segment的Extent連結串列 |
FSEG_NOT_FULL | 16 | 至少有一個page分配給當前Segment的Extent連結串列,全部用完時,轉移到FSEG_FULL上,全部釋放時,則歸還給當前表空間FSP_FREE連結串列 |
FSEG_FULL | 16 | 分配給當前segment且Page完全使用完的Extent連結串列 |
FSEG_MAGIC_N | 4 | Magic Number |
FSEG_FRAG_ARR 0 | 4 | 屬於該Segment的獨立Page。總是先從全域性分配獨立的Page,當填滿32個數組項時,就在每次分配時都分配一個完整的Extent,並在XDES PAGE中將其Segment ID設定為當前值 |
…… | …… | |
FSEG_FRAG_ARR 31 | 4 | 總共儲存32個記錄項 |
檔案維護
從上文我們可以看到,InnoDB通過Inode Entry來管理每個Segment佔用的資料頁,每個segment可以看做一個檔案頁維護單元。Inode Entry所在的inode page有可能存放滿,因此又通過頭Page維護了Inode Page連結串列。
在ibd的第一個Page中還維護了表空間內Extent的FREE、FREE_FRAG
、FULL_FRAG
三個Extent連結串列;而每個Inode Entry也維護了對應的FREE、NOT_FULL
、FULL三個Extent連結串列。這些連結串列之間存在著轉換關係,以高效的利用資料檔案空間。
當建立一個新的索引時,實際上構建一個新的btree(btr_create
),先為非葉子節點Segment分配一個inode entry,再建立root page,並將該segment的位置記錄到root page中,然後再分配leaf segment的Inode entry,並記錄到root page中。
當刪除某個索引後,該索引佔用的空間需要能被重新利用起來。
建立Segment
首先每個Segment需要從ibd檔案中預留一定的空間(fsp_reserve_free_extents
),通常是2個Extent。但如果是新建立的表空間,且當前的檔案小於1個Extent時,則只分配2個Page。
當檔案空間不足時,需要對檔案進行擴充套件(fsp_try_extend_data_file
)。檔案的擴充套件遵循一定的規則:如果當前小於1個Extent,則擴充套件到1個Extent滿;當表空間小於32MB時,每次擴充套件一個Extent;大於32MB時,每次擴充套件4個Extent(fsp_get_pages_to_extend_ibd
)。
在預留空間後,讀取檔案頭Page並加鎖(fsp_get_space_header
),然後開始為其分配Inode Entry(fsp_alloc_seg_inode
)。首先需要找到一個合適的inode page。
我們知道Inode Page的空間有限,為了管理Inode Page,在檔案頭儲存了兩個Inode Page連結串列,一個連結已經用滿的inode page,一個連結尚未用滿的inode page。如果當前Inode Page的空間使用完了,就需要再分配一個inode page,並加入到FSP_SEG_INODES_FREE
連結串列上(fsp_alloc_seg_inode_page
)。對於獨立表空間,通常一個inode
page就足夠了。
當拿到目標inode page後,從該Page中找到一個空閒(fsp_seg_inode_page_find_free
)未使用的slot(空閒表示其不歸屬任何segment,即FSEG_ID置為0)。
一旦該inode page中的記錄用滿了,就從FSP_SEG_INODES_FREE
連結串列上轉移到FSP_SEG_INODES_FULL
連結串列。
獲得inode entry後,遞增頭page的FSP_SEG_ID
,作為當前segment的seg id寫入到inode entry中。隨後進行一些列的初始化。
在完成inode entry的提取後,就將該inode entry所在inode page的位置及頁內偏移量儲存到其他某個page內(對於btree就是記錄在根節點內,佔用10個位元組,包含space id, page no, offset)。
Btree的根節點實際上是在建立non-leaf segment時分配的,root page被分配到該segment的frag array的第一個陣列元素中。
Segment分配入口函式: fseg_create_general
分配資料頁
隨著btree資料的增長,我們需要為btree的segment分配新的page。前面我們已經講過,segment是一個獨立的page管理單元,我們需要將從全域性獲得的資料空間納入到segment的管理中。
Step 1:空間擴充套件
當判定插入索引的操作可能引起分裂時,會進行悲觀插入(btr_cur_pessimistic_insert
),在做實際的分裂操作之前,會先對檔案進行擴充套件,並嘗試預留(tree_height / 16 + 3)個Extent,大多數情況下都是3個Extent。
這裡有個意外場景:如果當前檔案還不超過一個Extent,並且請求的page數小於1/2個Extent時,則如果指定page數,保證有2個可用的空閒Page,或者分配指定的page,而不是以Extent為單位進行分配。
注意這裡只是保證有足夠的檔案空間,避免在btree操作時進行檔案Extent。如果在這一步擴充套件了ibd檔案(fsp_try_extend_data_file
),新的資料頁並未初始化,也未加入到任何的連結串列中。
在判定是否有足夠的空閒Extent時,本身ibd預留的空閒空間也要納入考慮,對於普通使用者表空間是2個Extent + file_size * 1%。這些新擴充套件的page此時並未進行初始化,也未加入到,在頭page的FSP_FREE_LIMIT
記錄的page no標識了這類未初始化頁的範圍。
Step 2:為segment分配page
隨後進入索引分裂階段(btr_page_split_and_insert
),新page分配的上層呼叫棧:
btr_page_alloc
|--> btr_page_alloc_low
|--> fseg_alloc_free_page_general
|--> fseg_alloc_free_page_low
在傳遞的引數中,有個hint page no,通常是當前需要分裂的page no的前一個(direction = FSP_DOWN)或者後一個page no(direction = FSP_UP),其目的是將邏輯上相鄰的節點在物理上也儘量相鄰。
在Step 1我們已經保證了物理空間有足夠的資料頁,只是還沒進行初始化。將page分配到當前segment的流程如下(fseg_alloc_free_page_low
):
- 計算當前segment使用的和佔用的page數
- 使用的page數儲存包括
FSEG_NOT_FULL
連結串列上使用的page數(儲存在inode entry的FSEG_NOT_FULL_N_USED
中) + 已用滿segment的FSEG_FULL
連結串列上page數 + 佔用的frag array page數量; - 佔用的page數包括
FSEG_FREE
、FSEG_NOT_FULL
、FSEG_FULL
三個連結串列上的Extent + 佔用的frag array page數量。
- 使用的page數儲存包括
- 根據hint page獲取對應的xdes entry (
xdes_get_descriptor_with_space_hdr
) - 當滿足如下條件時該hint page可以直接拿走使用:
- Extent狀態為
XDES_FSEG
,表示屬於一個segment - hint page所在的Extent已被分配給當前segment(檢查xdes entry的XDES_ID)
- hint page對應的bit設定為free,表示尚未被佔用
- 返回hint page
- Extent狀態為
- 當滿足條件:1) xdes entry當前是空閒狀態(XDES_FREE);2) 該segment中已使用的page數大於其佔用的page數的7/8 (
FSEG_FILLFACTOR
);3) 當前segment已經使用了超過32個frag page,即表示其inode中的frag array可能已經用滿。- 從表空間分配hint page所在的Extent (
fsp_alloc_free_extent
),將其從FSP_FREE連結串列上移除 - 設定該Extent的狀態為XDES_FSEG,寫入seg id,並加入到當前segment的FSEG_FREE連結串列中。
- 返回hint page
- 從表空間分配hint page所在的Extent (
- 當如下條件時:1) direction != FSP_NO_DIR,對於Btree分裂,要麼FSP_UP,要麼FSP_DOWN;2)已使用的空間小於已佔用空間的7/8; 3)當前segment已經使用了超過32個frag page
- 嘗試從segment獲取一個Extent(
fseg_alloc_free_extent
),如果該segment的FSEG_FREE連結串列為空,則需要從表空間分配(fsp_alloc_free_extent
)一個Extent,並加入到當前segment的FSEG_FREE連結串列上 - direction為FSP_DOWN時,返回該Extent最後一個page,為FSP_UP時,返回該Extent的第一個Page
- 嘗試從segment獲取一個Extent(
- xdes entry屬於當前segment且未被用滿,從其中取一個空閒page並返回
- 如果該segment佔用的page數大於實用的page數,說明該segment還有空閒的page,則依次先看
FSEG_NOT_FULL
連結串列上是否有未滿的Extent,如果沒有,再看FSEG_FREE連結串列上是否有完全空閒的Extent。從其中取一個空閒Page並返回 - 當前已經實用的Page數小於32個page時,則分配獨立的page(
fsp_alloc_free_page
)並加入到該inode的frag array page陣列中,然後返回該block - 當上述情況都不滿足時,直接分配一個Extent(
fseg_alloc_free_extent
),並從其中取一個page返回。
上述流程看起來比較複雜,但可以總結為:
- 對於一個新的segment,總是優先填滿32個frag page陣列,之後才會為其分配完整的Extent,可以利用碎片頁,並避免小表佔用太多空間。
- 儘量獲得hint page;
- 如果segment上未使用的page太多,則儘量利用segment上的page。
上文提到兩處從表空間為segment分配資料頁,一個是分配單獨的資料頁,一個是分配整個Extent
表空間單獨資料頁的分配呼叫函式fsp_alloc_free_page
:
- 如果hint page所在的Extent在連結串列
XDES_FREE_FRAG
上,可以直接使用;否則從根據頭page的FSP_FREE_FRAG
連結串列檢視是否有可用的Extent; - 未能從上述找到一個可用Extent,直接分配一個Extent,並加入到
FSP_FREE_FRAG
連結串列中; - 從獲得的Extent中找到描述為空閒(
XDES_FREE_BIT
)的page。 - 分配該page (
fsp_alloc_from_free_frag
)- 設定page對應的bitmap的
XDES_FREE_BIT
為false,表示被佔用; - 遞增頭page的
FSP_FRAG_N_USED
欄位; - 如果該Extent被用滿了,就將其從
FSP_FREE_FRAG
移除,並加入到FSP_FULL_FRAG
連結串列中。同時對頭Page的FSP_FRAG_N_USED
遞減1個Extent(FSP_FRAG_N_USED
只儲存未滿的Extent使用的page數量); - 對Page內容進行初始化(
fsp_page_create
)。
- 設定page對應的bitmap的
表空間Extent的分配函式fsp_alloc_free_extent
:
- 通常先通過頭page看FSP_FREE連結串列上是否有空閒的Extent,如果沒有的話,則將新的Extent(例如上述step 1對檔案做擴充套件產生的新page,從
FSP_FREE_LIMIT
算起)加入到FSP_FREE
連結串列上(fsp_fill_free_list
):- 一次最多加4個Extent(
FSP_FREE_ADD
); - 如果涉及到xdes page,還需要對xdes page進行初始化;
- 如果Extent中存在類似xdes page這樣的系統管理頁,這個Extent被加入到
FSP_FREE_FRAG
連結串列中而不是FSP_FREE
連結串列; - 取連結串列上第一個Extent為當前使用;
- 一次最多加4個Extent(
- 將獲得的Extent從
FSP_FREE
移除,並返回對應的xdes entry(xdes_lst_get_descriptor
)。
回收Page
資料頁的回收分為兩種,一種是整個Extent的回收,一種是碎片頁的回收。在刪除索引頁或者drop索引時都會發生。
當某個資料頁上的資料被刪光時,我們需要從其所在segmeng上刪除該page(btr_page_free -->fseg_free_page --> fseg_free_page_low
),回收的流程也比較簡單:
- 首先如果是該segment的frag array中的page,將對應的slot設定為FIL_NULL, 並返還給表空間(
fsp_free_page
):- page在xdes entry中的狀態置為空閒;
- 如果page所在Extent處於
FSP_FULL_FRAG
連結串列,則轉移到FSP_FREE_FRAG
中; - 如果Extent中的page完全被釋放掉了,則釋放該Extent(
fsp_free_extent
),將其轉移到FSP_FREE連結串列; - 從函式返回;
- 如果page所處於的Extent當前在該segment的FSEG_FULL連結串列上,則轉移到
FSEG_NOT_FULL
連結串列; - 設定Page在xdes entry的bitmap對應的XDES_FREE_BIT為true;
- 如果此時該Extent上的page全部被釋放了,將其從
FSEG_NOT_FULL
連結串列上移除,並加入到表空間的FSP_FREE
連結串列上(而非Segment的FSEG_FREE
連結串列)。
釋放Segment
當我們刪除索引或者表時,需要刪除btree(btr_free_if_exists
),先刪除除了root節點外的其他部分(btr_free_but_not_root
),再刪除root節點(btr_free_root
)
由於資料操作都需要記錄redo,為了避免產生非常大的redo log,leaf segment通過反覆呼叫函式fseg_free_step
來釋放其佔用的資料頁:
- 首先找到leaf segment對應的Inode entry(
fseg_inode_try_get
); - 然後依次查詢inode entry中的
FSEG_FULL
、或者FSEG_NOT_FULL
、或者FSEG_FREE
連結串列,找到一個Extent,注意著裡的連結串列元組所指向的位置實際上是描述該Extent的Xdes Entry所在的位置。因此可以快速定位到對應的Xdes Page及Page內偏移量(xdes_lst_get_descriptor
); - 現在我們可以將這個Extent安全的釋放了(
fseg_free_extent
,見後文); - 當反覆呼叫
fseg_free_step
將所有的Extent都釋放後,segment還會最多佔用32個碎片頁,也需要依次釋放掉(fseg_free_page_low
) - 最後,當該inode所佔用的page全部釋放時,釋放inode entry:
- 如果該inode所在的inode page中當前被用滿,則由於我們即將釋放一個slot,需要從
FSP_SEG_INODES_FULL
轉移到FSP_SEG_INODES_FREE
(更新第一個page); - 將該inode entry的SEG_ID清除為0,表示未使用;
- 如果該inode page上全部inode entry都釋放了,就從
FSP_SEG_INODES_FREE
移除,並刪除該page。
- 如果該inode所在的inode page中當前被用滿,則由於我們即將釋放一個slot,需要從
non-leaf segment的回收和leaf segment的回收基本類似,但要注意btree的根節點儲存在該segment的frag arrary的第一個元組中,該Page暫時不可以釋放(fseg_free_step_not_header
)
btree的root page在完成上述步驟後再釋放,此時才能徹底釋放non-leaf segment
索引頁
ibd檔案中真正構建起使用者資料的結構是BTREE,在你建立一個表時,已經基於顯式或隱式定義的主鍵構建了一個btree,其葉子節點上記錄了行的全部列資料(加上事務id列及回滾段指標列);如果你在表上建立了二級索引,其葉子節點儲存了鍵值加上聚集索引鍵值。本小節我們探討下組成索引的物理儲存頁結構,這裡預設討論的是非壓縮頁,我們在下一小節介紹壓縮頁的內容。
每個btree使用兩個Segment來管理資料頁,一個管理葉子節點,一個管理非葉子節點,每個segment在inode page中存在一個記錄項,在btree的root page中記錄了兩個segment資訊。
當我們需要開啟一張表時,需要從ibdata的資料詞典表中load元資料資訊,其中SYS_INDEXES系統表中記錄了表,索引,及索引根頁對應的page no(DICT_FLD__SYS_INDEXES__PAGE_NO
),進而找到btree根page,就可以對整個使用者資料btree進行操作。
索引最基本的頁型別為FIL_PAGE_INDEX
。可以劃分為下面幾個部分。
Page Header
首先不管任何型別的資料頁都有38個位元組來描述頭資訊(FIL_PAGE_DATA
, or PAGE_HEADER
),包含如下資訊:
Macro | bytes | Desc |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 在MySQL4.0之前儲存space id,之後的版本用於儲存checksum |
FIL_PAGE_OFFSET | 4 | 當前頁的page no |
FIL_PAGE_PREV | 4 | 通常用於維護btree同一level的雙向連結串列,指向連結串列的前一個page,沒有的話則值為FIL_NULL |
FIL_PAGE_NEXT | 4 | 和FIL_PAGE_PREV類似,記錄連結串列的下一個Page的Page No |
FIL_PAGE_LSN | 8 | 最近一次修改該page的LSN |
FIL_PAGE_TYPE | 2 | Page型別 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 只用於系統表空間的第一個Page,記錄在正常shutdown時安全checkpoint到的點,對於使用者表空間,這個欄位通常是空閒的,但在5.7裡,FIL_PAGE_COMPRESSED型別的資料頁則另有用途。下一小節單獨介紹 |
FIL_PAGE_SPACE_ID | 4 | 儲存page所在的space id |
Index Header
緊隨FIL_PAGE_DATA
之後的是索引資訊,這部分資訊是索引頁獨有的。
Macro | bytes | Desc |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | Page directory中的slot個數 (見下文關於Page directory的描述) |
PAGE_HEAP_TOP | 2 | 指向當前Page內已使用的空間的末尾便宜位置,即free space的開始位置 |
PAGE_N_HEAP | 2 | Page內所有記錄個數,包含使用者記錄,系統記錄以及標記刪除的記錄,同時當第一個bit設定為1時,表示這個page內是以Compact格式儲存的 |
PAGE_FREE | 2 | 指向標記刪除的記錄連結串列的第一個記錄 |
PAGE_GARBAGE | 2 | 被刪除的記錄連結串列上佔用的總的位元組數,屬於可回收的垃圾碎片空間 |
PAGE_LAST_INSERT | 2 | 指向最近一次插入的記錄偏移量,主要用於優化順序插入操作 |
PAGE_DIRECTION | 2 | 用於指示當前記錄的插入順序以及是否正在進行順序插入,每次插入時,PAGE_LAST_INSERT會和當前記錄進行比較,以確認插入方向,據此進行插入優化 |
PAGE_N_DIRECTION | 2 | 當前以相同方向的順序插入記錄個數 |
PAGE_N_RECS | 2 | Page上有效的未被標記刪除的使用者記錄個數 |
PAGE_MAX_TRX_ID | 8 | 最近一次修改該page記錄的事務ID,主要用於輔助判斷二級索引記錄的可見性。 |
PAGE_LEVEL | 2 | 該Page所在的btree level,根節點的level最大,葉子節點的level為0 |
PAGE_INDEX_ID | 8 | 該Page歸屬的索引ID |
Segment Info
隨後20個位元組描述段資訊,僅在Btree的root Page中被設定,其他Page都是未使用的。
Macro | bytes | Desc |
---|---|---|
PAGE_BTR_SEG_LEAF | 10(FSEG_HEADER_SIZE) | leaf segment在inode page中的位置 |
PAGE_BTR_SEG_TOP | 10(FSEG_HEADER_SIZE) | non-leaf segment在inode page中的位置 |
10個位元組的inode資訊包括:
Macro | bytes | Desc |
---|---|---|
FSEG_HDR_SPACE | 4 | 描述該segment的inode page所在的space id (目前的實現來看,感覺有點多餘…) |
FSEG_HDR_PAGE_NO | 4 | 描述該segment的inode page的page no |
FSEG_HDR_OFFSET | 2 | inode page內的頁內偏移量 |
通過上述資訊,我們可以找到對應segment在inode page中的描述項,進而可以操作整個segment。
系統記錄
之後是兩個系統記錄,分別用於描述該page上的極小值和極大值,這裡存在兩種儲存方式,分別對應舊的InnoDB檔案系統,及新的檔案系統(compact page)
Macro | bytes | Desc |
---|---|---|
REC_N_OLD_EXTRA_BYTES + 1 | 7 | 固定值,見infimum_supremum_redundant的註釋 |
PAGE_OLD_INFIMUM | 8 | “infimum\0” |
REC_N_OLD_EXTRA_BYTES + 1 | 7 | 固定值,見infimum_supremum_redundant的註釋 |
PAGE_OLD_SUPREMUM | 9 | “supremum\0” |
Compact的系統記錄儲存方式為:
Macro | bytes | Desc |
---|---|---|
REC_N_NEW_EXTRA_BYTES | 5 | 固定值,見infimum_supremum_compact的註釋 |
PAGE_NEW_INFIMUM | 8 | “infimum\0” |
REC_N_NEW_EXTRA_BYTES | 5 | 固定值,見infimum_supremum_compact的註釋 |
PAGE_NEW_SUPREMUM | 8 | “supremum”,這裡不帶字元0 |
兩種格式的主要差異在於不同行儲存模式下,單個記錄的描述資訊不同。在實際建立page時,系統記錄的值已經初始化好了,對於老的格式(REDUNDANT),對應程式碼裡的infimum_supremum_redundant
,對於新的格式(compact),對應infimum_supremum_compact
。infimum記錄的固定heap no為0,supremum記錄的固定Heap no 為1。page上最小的使用者記錄前節點總是指向infimum,page上最大的記錄後節點總是指向supremum記錄。
具體參考索引頁建立函式:page_create_low
使用者記錄
在系統記錄之後就是真正的使用者記錄了,heap no 從2(PAGE_HEAP_NO_USER_LOW
)開始算起。注意Heap no僅代表物理儲存順序,不代表鍵值順序。
根據不同的型別,使用者記錄可以是非葉子節點的Node指標資訊,也可以是隻包含有效資料的葉子節點記錄。而不同的行格式儲存的行記錄也不同,例如在早期版本中使用的redundant格式會被現在的compact格式使用更多的位元組數來描述記錄,例如描述記錄的一些列資訊,在使用compact格式時,可以改為直接從資料詞典獲取。因為redundant屬於漸漸被拋棄的格式,本文的討論中我們預設使用Compact格式。在檔案rem/rem0rec.cc的頭部註釋描述了記錄的物理結構。
每個記錄都存在rec header,描述如下(參閱檔案include/rem0rec.ic)
bytes | Desc |
---|---|
變長列長度陣列 | 如果列的最大長度為255位元組,使用1byte;否則,0xxxxxxx (one byte, length=0..127), or 1exxxxxxxxxxxxxx (two bytes, length=128..16383, extern storage flag) |
SQL-NULL flag | 標示值為NULL的列的bitmap,每個位標示一個列,bitmap的長度取決於索引上可為NULL的列的個數(dict_index_t::n_nullable),這兩個陣列的解析可以參閱函式rec_init_offsets |
下面5個位元組(REC_N_NEW_EXTRA_BYTES)描述記錄的額外資訊 | …. |
REC_NEW_INFO_BITS (4 bits) | 目前只使用了兩個bit,一個用於表示該記錄是否被標記刪除(REC_INFO_DELETED_FLAG ),另一個bit(REC_INFO_MIN_REC_FLAG)如果被設定,表示這個記錄是當前level最左邊的page的第一個使用者記錄 |
REC_NEW_N_OWNED (4 bits) | 當該值為非0時,表示當前記錄佔用page directory裡一個slot,並和前一個slot之間存在這麼多個記錄 |
REC_NEW_HEAP_NO (13 bits) | 該記錄的heap no |
REC_NEW_STATUS (3 bits) | 記錄的型別,包括四種:REC_STATUS_ORDINARY (葉子節點記錄), REC_STATUS_NODE_PTR (非葉子節點記錄),REC_STATUS_INFIMUM (infimum系統記錄)以及REC_STATUS_SUPREMUM (supremum系統記錄) |
REC_NEXT (2bytes) | 指向按照鍵值排序的page內下一條記錄資料起點,這裡儲存的是和當前記錄的相對位置偏移量(函式rec_set_next_offs_new ) |
在記錄頭資訊之後的資料視具體情況有所不同:
- 對於聚集索引記錄,資料包含了事務id,回滾段指標;
- 對於二級索引記錄,資料包含了二級索引鍵值以及聚集索引鍵值。如果二級索引鍵和聚集索引有重合,則只保留一份重合的,例如pk (col1, col2),sec key(col2, col3),在二級索引記錄中就只包含(col2, col3, col1);
- 對於非葉子節點頁的記錄,聚集索引上包含了其子節點的最小記錄鍵值及對應的page no;二級索引上有所不同,除了二級索引鍵值外,還包含了聚集索引鍵值,再加上page no三部分構成。
Free space
這裡指的是一塊完整的未被使用的空間,範圍在頁內最後一個使用者記錄和Page directory之間。通常如果空間足夠時,直接從這裡分配記錄空間。當判定空閒空間不足時,會做一次Page內的重整理,以對碎片空間進行合併。
Page directory
為了加快頁內的資料查詢,會按照記錄的順序,每隔4~8個數量(PAGE_DIR_SLOT_MIN_N_OWNED
~ PAGE_DIR_SLOT_MAX_N_OWNED
)的使用者記錄,就分配一個slot (每個slot佔用2個位元組,PAGE_DIR_SLOT_SIZE
),儲存記錄的頁內偏移量,可以理解為在頁內構建的一個很小的索引(sparse index)來輔助二分查詢。
Page Directory的slot分配是從Page末尾(倒數第八個位元組開始)開始逆序分配的。在查詢記錄時。先根據page directory 確定記錄所在的範圍,然後在據此進行線性查詢。
增加slot的函式參閱 page_dir_add_slot
頁內記錄二分查詢的函式參閱 page_cur_search_with_match_bytes
FIL Trailer
在每個檔案頁的末尾保留了8個位元組(FIL_PAGE_DATA_END
or FIL_PAGE_END_LSN_OLD_CHKSUM
),其中4個位元組用於儲存page checksum,這個值需要和page頭部記錄的checksum相匹配,否則認為page損壞(buf_page_is_corrupted
)
壓縮索引頁
InnoDB當前存在兩種形式的壓縮頁,一種是Transparent Page Compression,還有一種是傳統的壓縮方式,下文分別進行闡述。
Transparent Page Compression
這是MySQL5.7新加的一種資料壓縮方式,其原理是利用核心Punch hole特性,對於一個16kb的資料頁,在寫檔案之前,除了Page頭之外,其他部分進行壓縮,壓縮後留白的地方使用punch hole進行 “打洞”,在磁碟上表現為不佔用空間 (但會產生大量的磁碟碎片)。 這種方式相比傳統的壓縮方式具有更好的壓縮比,實現邏輯也更加簡單。
對於這種壓縮方式引入了新的型別FIL_PAGE_COMPRESSED
,在儲存格式上略有不同,主要表現在從FIL_PAGE_FILE_FLUSH_LSN
開始的8個位元組被用作記錄壓縮資訊:
Macro | bytes | Desc |
---|---|---|
FIL_PAGE_VERSION | 1 | 版本,目前為1 |
FIL_PAGE_ALGORITHM_V1 | 1 | 使用的壓縮演算法 |
FIL_PAGE_ORIGINAL_TYPE_V1 | 2 | 壓縮前的Page型別,解壓後需要恢復回去 |
FIL_PAGE_ORIGINAL_SIZE_V1 | 2 | 未壓縮時去除FIL_PAGE_DATA後的資料長度 |
FIL_PAGE_COMPRESS_SIZE_V1 | 2 | 壓縮後的長度 |
打洞後的page其實際儲存空間需要是磁碟的block size的整數倍。
傳統壓縮儲存格式
當你建立或修改表,指定row_format=compressed key_block_size=1|2|4|8
時,建立的ibd檔案將以對應的block size進行劃分。例如key_block_size
設定為4時,對應block size為4kb。
壓縮頁的格式可以描述如下表所示:
Macro | Desc |
---|---|
FIL_PAGE_HEADER | 頁面頭資料,不做壓縮 |
Index Field Information | 索引的列資訊,參閱函式page_zip_fields_encode 及page_zip_fields_decode ,在崩潰恢復時可以據此恢復出索引資訊 |
Compressed Data | 壓縮資料,按照heap no排序進入壓縮流,壓縮資料不包含系統列(trx_id, roll_ptr)或外部儲存頁指標 |
Modification Log(mlog) | 壓縮頁修改日誌 |
Free Space | 空閒空間 |
External_Ptr (optional) | 存在外部儲存頁的列記錄指標陣列,只存在聚集索引葉子節點,每個陣列元素佔20個位元組(BTR_EXTERN_FIELD_REF_SIZE ),參閱函式page_zip_compress_clust_ext |
Trx_id, Roll_Ptr(optional) | 只存在於聚集索引葉子節點,陣列元素和其heap no一一對應 |
Node_Ptr | 只存在於索引非葉子節點,儲存節點指標陣列,每個元素佔用4位元組(REC_NODE_PTR_SIZE) |
Dense Page Directory | 分兩部分,第一部分是有效記錄,記錄其在解壓頁中的偏移位置,n_owned和delete標記資訊,按照鍵值順序;第二部分是空閒記錄;每個slot佔兩個位元組。 |
在記憶體中通常存在壓縮頁和解壓頁兩份資料。當對資料進行修改時,通常先修改解壓頁,再將DML操作以一種特殊日誌的格式記入壓縮頁的mlog中。以減少被修改過程中重壓縮的次數。主要包含這幾種操作:
- Insert: 向mlog中寫入完整記錄
- Update:
- Delete-insert update,將舊記錄的dense slot標記為刪除,再寫入完整新記錄
- In-place update,直接寫入新更新的記錄
- Delete: 標記對應的dense slot為刪除
頁壓縮參閱函式 page_zip_compress
頁解壓參閱函式 page_zip_decompress
系統資料頁
這裡我們將所有非獨立的資料頁統稱為系統資料頁,主要儲存在ibdata中,如下圖所示:
ibdata的三個page和普通的使用者表空間一樣,都是用於維護和管理檔案頁。其他Page我們下面一一進行介紹。
FSP_IBUF_HEADER_PAGE_NO
Ibdata的第4個page是Change Buffer的header page,型別為FIL_PAGE_TYPE_SYS
,主要用於對ibuf btree的Page管理。
FSP_IBUF_TREE_ROOT_PAGE_NO
用於儲存change buffer的根page,change buffer目前儲存於Ibdata中,其本質上也是一顆btree,root頁為固定page,也就是Ibdata的第5個page。
IBUF HEADER Page 和Root Page聯合起來對ibuf的資料頁進行管理。
首先Ibuf btree自己維護了一個空閒Page連結串列,連結串列頭記錄在根節點中,偏移量在PAGE_BTR_IBUF_FREE_LIST
處,實際上利用的是普通索引根節點的PAGE_BTR_SEG_LEAF
欄位。Free List上的Page型別標示為FIL_PAGE_IBUF_FREE_LIST
每個Ibuf page重用了PAGE_BTR_SEG_LEAF
欄位,以維護IBUF FREE LIST的前後文件頁節點(PAGE_BTR_IBUF_FREE_LIST_NODE
)。
由於root page中的segment欄位已經被重用,因此額外的開闢了一個Page,也就是Ibdata的第4個page來進行段管理。在其中記錄了ibuf btree的segment header,指向屬於ibuf btree的inode entry。
關於ibuf btree的構建參閱函式 btr_create
FSP_TRX_SYS_PAGE_NO/FSP_FIRST_RSEG_PAGE_NO
ibdata的第6個page,記錄了InnoDB重要的事務系統資訊,主要包括:
Macro | bytes | Desc |
---|---|---|
TRX_SYS | 38 | 每個資料頁都會保留的檔案頭欄位 |
TRX_SYS_TRX_ID_STORE | 8 | 持久化的最大事務ID,這個值不是實時寫入的,而是256次遞增寫一次 |
TRX_SYS_FSEG_HEADER | 10 | 指向用來管理事務系統的segment所在的位置 |
TRX_SYS_RSEGS | 128 * 8 | 用於儲存128個回滾段位置,包括space id及page no。每個回滾段包含一個檔案segment(trx_rseg_header_create ) |
…… | 以下是Page內UNIV_PAGE_SIZE - 1000的偏移位置 | |
TRX_SYS_MYSQL_LOG_MAGIC_N_FLD | 4 | Magic Num ,值為873422344 |
TRX_SYS_MYSQL_LOG_OFFSET_HIGH | 4 | 事務提交時會將其binlog位點更新到該page中,這裡記錄了在binlog檔案中偏移量的高位的4位元組 |
TRX_SYS_MYSQL_LOG_OFFSET_LOW | 4 | 同上,記錄偏移量的低4位位元組 |
TRX_SYS_MYSQL_LOG_NAME | 4 | 記錄所在的binlog檔名 |
…… | 以下是Page內UNIV_PAGE_SIZE - 200 的偏移位置 | |
TRX_SYS_DOUBLEWRITE_FSEG | 10 | 包含double write buffer的fseg header |
TRX_SYS_DOUBLEWRITE_MAGIC | 4 | Magic Num |
TRX_SYS_DOUBLEWRITE_BLOCK1 | 4 | double write buffer的第一個block(佔用一個Extent)在ibdata中的開始位置,連續64個page |
TRX_SYS_DOUBLEWRITE_BLOCK2 | 4 | 第二個dblwr block的起始位置 |
TRX_SYS_DOUBLEWRITE_REPEAT | 12 | 重複記錄上述三個欄位,即MAGIC NUM, block1, block2,防止發生部分寫時可以恢復 |
TRX_SYS_DOUBLEWRITE_SPACE_ID_STORED | 4 | 用於相容老版本,當該欄位的值不為TRX_SYS_DOUBLEWRITE_SPACE_ID_STORED_N時,需要重置dblwr中的資料 |
在5.7版本中,回滾段既可以在ibdata中,也可以在獨立undo表空間,或者ibtmp臨時表空間中,一個可能的分佈如下圖所示(摘自我之前的這篇文章)。
由於是在系統剛啟動時初始化事務系統,因此第0號回滾段頭頁總是在ibdata的第7個page中。
事務系統建立參閱函式 trx_sysf_create
InnoDB最多可以建立128個回滾段,每個回滾段需要單獨的Page來維護其擁有的undo slot,Page型別為FIL_PAGE_TYPE_SYS
。描述如下:
Macro | bytes | Desc |
---|---|---|
TRX_RSEG | 38 | 保留的Page頭 |
TRX_RSEG_MAX_SIZE | 4 | 回滾段允許使用的最大Page數,當前值為ULINT_MAX |
TRX_RSEG_HISTORY_SIZE | 4 | 在history list上的undo page數,這些page需要由purge執行緒來進行清理和回收 |
TRX_RSEG_HISTORY | FLST_BASE_NODE_SIZE(16) | history list的base node |
TRX_RSEG_FSEG_HEADER | (FSEG_HEADER_SIZE)10 | 指向當前管理當前回滾段的inode entry |
TRX_RSEG_UNDO_SLOTS | 1024 * 4 | undo slot陣列,共1024個slot,值為FIL_NULL表示未被佔用,否則記錄佔用該slot的第一個undo page |
回滾段頭頁的建立參閱函式 trx_rseg_header_create
實際儲存undo記錄的Page型別為FIL_PAGE_UNDO_LOG
,undo header結構如下
Macro | bytes | Desc |
---|---|---|
TRX_UNDO_PAGE_HDR | 38 | Page 頭 |
TRX_UNDO_PAGE_TYPE | 2 | 記錄Undo型別,是TRX_UNDO_INSERT還是TRX_UNDO_UPDATE |
TRX_UNDO_PAGE_START | 2 | 事務所寫入的最近的一個undo log在page中的偏移位置 |
TRX_UNDO_PAGE_FREE | 2 | 指向當前undo page中的可用的空閒空間起始偏移量 |
TRX_UNDO_PAGE_NODE | 12 | 連結串列節點,提交後的事務,其擁有的undo頁會加到history list上 |
undo頁內結構及其與回滾段頭頁的關係參閱下圖:
FSP_DICT_HDR_PAGE_NO
ibdata的第8個page,用來儲存資料詞典表的資訊 (只有拿到資料詞典表,才能根據其中儲存的表資訊,進一步找到其對應的表空間,以及表的聚集索引所在的page no)
Dict_Hdr Page的結構如下表所示:
Macro | bytes | Desc |
---|---|---|
DICT_HDR | 38 | Page頭 |
DICT_HDR_ROW_ID | 8 | 最近被賦值的row id,遞增,用於給未定義主鍵的表,作為其隱藏的主鍵鍵值來構建btree |
DICT_HDR_TABLE_ID | 8 | 當前系統分配的最大事務ID,每建立一個新表,都賦予一個唯一的table id,然後遞增 |
DICT_HDR_INDEX_ID | 8 | 用於分配索引ID |
DICT_HDR_MAX_SPACE_ID | 4 | 用於分配space id |
DICT_HDR_MIX_ID_LOW | 4 | |
DICT_HDR_TABLES | 4 | SYS_TABLES系統表的聚集索引root page |
DICT_HDR_TABLE_IDS | 4 | SYS_TABLE_IDS索引的root page |
DICT_HDR_COLUMNS | 4 | SYS_COLUMNS系統表的聚集索引root page |
DICT_HDR_INDEXES | 4 | SYS_INDEXES系統表的聚集索引root page |
DICT_HDR_FIELDS | 4 | SYS_FIELDS系統表的聚集索引root page |
dict_hdr頁的建立參閱函式 dict_hdr_create
double write buffer
InnoDB使用double write buffer來防止資料頁的部分寫問題,在寫一個數據頁之前,總是先寫double write buffer,再寫資料檔案。當崩潰恢復時,如果資料檔案中page損壞,會嘗試從dblwr中恢復。
double write buffer儲存在ibdata中,你可以從事務系統頁(ibdata的第6個page)獲取dblwr所在的位置。總共128個page,劃分為兩個block。由於dblwr在安裝例項時已經初始化好了,這兩個block在Ibdata中具有固定的位置,Page64 ~127 劃屬第一個block,Page 128 ~191劃屬第二個block。
在這128個page中,前120個page用於batch flush時的髒頁回寫,另外8個page用於SINGLE PAGE FLUSH時的髒頁回寫。
外部儲存頁
對於大欄位,在滿足一定條件時InnoDB使用外部頁進行儲存。外部儲存頁有三種類型:
-
FIL_PAGE_TYPE_BLOB
:表示非壓縮的外部儲存頁,結構如下圖所示: -
FIL_PAGE_TYPE_ZBLOB
:壓縮的外部儲存頁,如果存在多個blob page,則表示第一個FIL_PAGE_TYPE_ZBLOB2
:如果存在多個壓縮的blob page,則表示blob鏈隨後的page;
結構如下圖所示:
而在記錄內只儲存了20個位元組的指標以指向外部儲存頁,指標描述如下:
Macro | bytes | Desc |
---|---|---|
BTR_EXTERN_SPACE_ID | 4 | 外部儲存頁所在的space id |
BTR_EXTERN_PAGE_NO | 4 | 第一個外部頁的Page no |
BTR_EXTERN_OFFSET | 4 | 對於壓縮頁,為12,該偏移量儲存了指向下一個外部頁的的page no;對於非壓縮頁,值為38,指向blob header,如上圖所示 |
外部頁的寫入參閱函式 btr_store_big_rec_extern_fields
MySQL5.7新資料頁:加密頁及R-TREE頁
MySQL 5.7版本引入了新的資料頁以支援表空間加密及對空間資料型別建立R-TREE索引。本文對這種資料頁不做深入討論,僅僅簡單描述下,後面我們會單獨開兩篇文章分別進行介紹。
資料加密頁
從MySQL5.7.11開始InnoDB支援對單表進行加密,因此引入了新的Page型別來支援這一特性,主要加了三種Page型別:
FIL_PAGE_ENCRYPTED
:加密的普通資料頁FIL_PAGE_COMPRESSED_AND_ENCRYPTED
:資料頁為壓縮頁(transparent page compression) 並且被加密(先壓縮,再加密)FIL_PAGE_ENCRYPTED_RTREE
:GIS索引R-TREE的資料頁並被加密
對於加密頁,除了資料部分被替換成加密資料外,其他部分和大多數表都是一樣的結構。
加解密的邏輯和Transparent Compression類似,在寫入檔案前加密(os_file_encrypt_page --> Encryption::encrypt
),在讀出檔案時解密資料(os_file_io_complete --> Encryption::decrypt
)
祕鑰資訊儲存在ibd檔案的第一個page中(fsp_header_init --> fsp_header_fill_encryption_info
),當執行SQL ALTER INSTANCE ROTATE INNODB MASTER KEY
時,會更新每個ibd儲存的祕鑰資訊(fsp_header_rotate_encryption
)
預設安裝時,一個新的外掛keyring_file
被安裝並且預設Active,在安裝目錄下,會產生一個新的檔案來儲存祕鑰,位置在$MYSQL_INSTALL_DIR/keyring/keyring,你可以通過引數keyring_file_data來指定祕鑰的存放位置和檔案命名。
當你安裝多例項時,需要為不同的例項指定keyring檔案。
開啟表加密的語法很簡單,在CREATE TABLE或ALTER TABLE時指定選項ENCRYPTION=‘Y’來開啟,或者ENCRYPTION=‘N’來關閉加密。
關於InnoDB表空間加密特性,參閱該commit及官方文件。
R-TREE索引頁
在MySQL 5.7中引入了新的索引型別R-TREE來描述空間資料型別的多維資料結構,這類索引的資料頁型別為FIL_PAGE_RTREE
。
臨時表空間ibtmp
MySQL5.7引入了臨時表專用的表空間,預設命名為ibtmp1,建立的非壓縮臨時表都儲存在該表空間中。系統重啟後,ibtmp1會被重新初始化到預設12MB。你可以通過設定引數innodb_temp_data_file_path來修改ibtmp1的預設初始大小,以及是否允許autoExtent。預設值為 “ibtmp1:12M:autoExtent”。
除了使用者定義的非壓縮臨時表外,第1~32個臨時表專用的回滾段也存放在該檔案中(0號回滾段總是存放在ibdata中)(trx_sys_create_noredo_rsegs
),
日誌檔案ib_logfile
關於日誌檔案的格式,網上已經有很多的討論,在之前的系列文章中我也有專門介紹過,本小節主要介紹下MySQL5.7新的修改。
首先是checksum演算法的改變,當前版本的MySQL5.7可以通過引數innodb_log_checksums
來開啟或關閉redo checksum,但目前唯一支援的checksum演算法是CRC32。而在之前老版本中只支援效率較低的InnoDB本身的checksum演算法。
第二個改變是為Redo log引入了版本資訊(WL#8845),儲存在ib_logfile的頭部,從檔案頭開始,描述如下
Macro | bytes | Desc |
---|---|---|
LOG_HEADER_FORMAT | 4 | 當前值為1(LOG_HEADER_FORMAT_CURRENT),在老版本中這裡的值總是為0 |
LOG_HEADER_PAD1 | 4 | 新版本未使用 |
LOG_HEADER_START_LSN | 8 | 當前iblogfile的開始LSN |
LOG_HEADER_CREATOR | 32 | 記錄版本資訊,和MySQL版本相關,例如在5.7.11中,這裡儲存的是”MySQL 5.7.11”(LOG_HEADER_CREATOR_CURRENT) |
每次切換到下一個iblogfile時,都會更新該檔案頭資訊(log_group_file_header_flush
)
新的版本支援相容老版本(recv_find_max_checkpoint_0
),但升級到新版本後,就無法在異常狀態下in-place降級到舊版本了(除非做一次clean的shutdown,並清理掉iblogfile)。