Innodb表空間、段、區描述頁分析與磁盤存儲空間管理
從一個整體方向結構上看,表空間大的結構圖如下
-
表空間:表空間文件,存放數據庫數據的載體,對於系統表空間通常是ibdata1,開啟獨立表空間文件
innodb_file_per_table=1
后,對應的表空間為.ibd后綴的表空間文件 -
數據段(segment):邏輯上的概念,與數據庫中的索引相映射,數據表由多個段(索引)組成,段的類型有數據段(葉子結點)、索引段(非葉節點),一個索引占據兩個段,分別對應葉節點和非葉節點,默認地,對於一張只有主鍵的表,會默認存在兩個段,分別對應主索引的非葉節點和葉節點段
-
區(extent):
- 對於小表,可以忽略區的概念,表基本由段以及離散的數據page頁構成,
- 隨着表空間數據量的增長,系統對於磁盤空間的分配按區(extent)來進行,extent代表一組連續的page,默認64個page(1M大小),extent的作用是提高page分配效率,通常來講批量分配從效率上總是優於單一的page分配,另外對於數據連續性方面也更優
-
頁:mysql存儲的基礎單元,一個頁的大小通常是16k,innodb將xxx.ibd(表空間文件)按page切分
實際上的ibd文件構成
數據在物理文件上按16k被切分成一個一個的數據頁,每個頁有自己的頁編號(pageNo)
每一個表空間文件前面三個頁通常稱之為管理頁(元數據頁)
段與區的內容比較負責,主要是負責磁盤空間的管理、連續空間申請,介紹段、區的詳細內容之前,我們先來看一下表空間文件的元數據頁信息
元數據頁概述
-
表空間文件的第一個16kb,pageno=0的數據頁稱之為File Space Header,主要有兩部分內容構成,fsp_header/xdes_entry
- fsp_header記錄整個表空間具體信息
- xdes_entry記錄當前表空間連續的256個extent信息,每間隔256M空間就會插入一個xdes_entry,不同的是后續的xdes_entry頁面不包含fsp_header
-
表空間文件的第二個16kb,pageno=1的數據頁稱之為Insert Buffer Bitmap,插入緩沖位圖頁,記錄后續連續16384個數據頁(256M)的插入緩沖位圖信息,可以看到每隔256M的表空間間距均會新增兩個數據頁,一個是xdes_entry描述頁,一個是Insert Buffer Bitmap描述頁
-
表空間的文件的第三個16kb,pageno=2的數據頁記錄的是File Segment inode段信息記錄頁,也稱之為inode信息頁,一個inode信息頁記錄85個segment段信息,一個索引占據兩個段描述符號,通常情況下對於獨立表空間文件,此頁面通常只需要一個即可滿足大部分需求,感興趣的可以試試如下的數據表
CREATE TABLE `test_fsp` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL DEFAULT '0',
`b` int(11) NOT NULL DEFAULT '0',
`c` int(11) NOT NULL DEFAULT '0',
`d` int(11) NOT NULL DEFAULT '0',
`e` int(11) NOT NULL DEFAULT '0',
`f` int(11) NOT NULL DEFAULT '0',
`g` int(11) NOT NULL DEFAULT '0',
`h` int(11) NOT NULL DEFAULT '0',
`i` int(11) NOT NULL DEFAULT '0',
`j` int(11) NOT NULL DEFAULT '0',
`k` int(11) NOT NULL DEFAULT '0',
`l` int(11) NOT NULL DEFAULT '0',
`m` int(11) NOT NULL DEFAULT '0',
`n` int(11) NOT NULL DEFAULT '0',
`o` int(11) NOT NULL DEFAULT '0',
`p` int(11) NOT NULL DEFAULT '0',
`q` int(11) NOT NULL DEFAULT '0',
`r` int(11) NOT NULL DEFAULT '0',
`s` int(11) NOT NULL DEFAULT '0',
`t` int(11) NOT NULL DEFAULT '0',
`u` int(11) NOT NULL DEFAULT '0',
`v` int(11) NOT NULL DEFAULT '0',
`w` int(11) NOT NULL DEFAULT '0',
`x` int(11) NOT NULL DEFAULT '0',
`y` int(11) NOT NULL DEFAULT '0',
`z` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `ix_a` (`a`),
KEY `ix_b` (`b`),
KEY `ix_c` (`c`),
KEY `ix_d` (`d`),
KEY `ix_e` (`e`),
KEY `ix_f` (`f`),
KEY `ix_g` (`g`),
KEY `ix_h` (`h`),
KEY `ix_i` (`i`),
KEY `ix_j` (`j`),
KEY `ix_k` (`k`),
KEY `ix_l` (`l`),
KEY `ix_m` (`m`),
KEY `ix_n` (`n`),
KEY `ix_o` (`o`),
KEY `ix_p` (`p`),
KEY `ix_q` (`q`),
KEY `ix_r` (`r`),
KEY `ix_s` (`s`),
KEY `ix_t` (`t`),
KEY `ix_u` (`u`),
KEY `ix_v` (`v`),
KEY `ix_w` (`w`),
KEY `ix_x` (`x`),
KEY `ix_y` (`y`),
KEY `ix_z` (`z`),
KEY `ix_ab` (`a`,`b`),
KEY `ix_cd` (`c`,`d`),
KEY `ix_ef` (`e`,`f`),
KEY `ix_gh` (`g`,`h`),
KEY `ix_ij` (`i`,`j`),
KEY `ix_kl` (`k`,`l`),
KEY `ix_mn` (`m`,`n`),
KEY `ix_op` (`o`,`p`),
KEY `ix_qr` (`q`,`r`),
KEY `ix_st` (`s`,`t`),
KEY `ix_uv` (`u`,`v`),
KEY `ix_wx` (`w`,`x`),
KEY `ix_yz` (`y`,`z`),
KEY `ix_abc` (`a`,`b`,`c`),
KEY `ix_edf` (`e`,`d`,`f`),
KEY `ix_ghi` (`g`,`h`,`i`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
此表包含26個英文字母數據列+id主鍵列,索引數量1+26+13+3 = 43
,總共需要的段的數據量數目是43*2=86,超過了85個,此時在執行mysql_page_info test/test_fsp.ibd
可以看到輸出File Segment inode: 2
, 變成2了,亦即此時需要兩個inode數據頁才能滿足需求,不過一般情形下一張表的索引數不會這么多
FSP_HDR/XDES_ENTRY
整體結構如下:
可以看到在除了page0頁之外的xdes描述頁,fsp header均為空,xdes_entry中記錄256個entry,每個entry記錄一個區的信息
fsp header結構如下:
- Space Id(4)/FSP_SPACE_ID 記錄當前表空間ID
- (Unused) (4) 暫未使用的空間
- Highest page number in file (size) (4) / FSP_SIZE 代表當前表空間數據頁的數量計算方式為(ibd_file_size*1024/16)
- Highest page number initialized (free limit) (4) /FSP_FREE_LIMIT 當前表空間文件中未被初始化的數據頁起始位置,通常當表空間數據頁不足的時候,需要從此位置開始申請數據頁
- Flags (4) /FSP_SPACE_FLAGS 標志位
- Number of pages used in "FREE_FRAG" list (4) /FSP_FRAG_N_USED FREE_FRAG 空閑鏈表上的數據頁數量
- List base node for "FREE" list (16) / FSP_FREE 所有的Page均為空閑的Extent鏈表
- List base node for "FREE_FRAG" list (16) / FSP_FREE_FRAG Extend中的page部分被使用的鏈表
- List base node for "FULL_FRAG" list (16) / FSP_FULL_FRAG Extend中的page全部被使用的鏈表
- Next Unused Segment ID (8) /FSP_SEG_ID 表示下一個未被使用的Segment ID
- List base node for "FULL_INODES" list (16)/FSP_SEG_INODES_FULL 表示Segment Page的所有的Inode均已被使用
- List base node for "FREE_INODES" list (16)/FSP_SEG_INODES_FREE 表示Segment Page存在空閑的Inode
文件鏈表(List base node/List Node):
-
包含一個描述鏈表長度的4字節
-
兩個6字節的"First"/"Last"指針構成的雙向兩表,通常這部分用於表頭節點,記錄整個鏈表的起始與結束位置
- 包含"Prev"/"Next"指針構成雙向鏈表的節點,這部分通常用於構成鏈表的節點部分(比如Xdes_entry中的數據結構),,鏈表由pageNo(4)+offset(2)構成,prev(first)/next(last)一共12字節(頁的大小為16KB,所以2字節(16bit)的頁內偏移即可)
First、Prev/Last、Next的大小均為6字節的大小,fsp_header中主要記錄段(indodes)、區/頁(frag) 的分配鏈表信息以及整個表空間整體的數據頁數量、segment數量等元素信息
表空間的文件分配規則大致如下所述
-
對於新表,依據建表語句索引個數多少,此階段對於系統存儲空間按照碎片頁的方式分配,亦即此時存儲空間的申請基本是16kb*N的的方式遞增
-
隨着數據的插入,當數據量逐漸增多,數據容量逐漸超過32個碎片頁的大小時候(通常比這個會小點,系統申請空間通常會預留一部分空閑空間,此處舉例說明分配流程)新的磁磁盤空間按區申請(1M)
- 此處的32跟segment的結構有關系,后續會說明
-
隨着更多的數據插入,系統按照1M的方式逐漸擴張表空間文件,當表空間的容量達到32M,系統會以4M(4個區大小)容量的方式擴充表空間的容量大小
前面提到過,對於表空間文件,需要盡量滿足數據頁在磁盤存儲上的連續性,通常情況下批量分配能夠保證一組數據頁的連續性,在平衡小表空間使用率的情況下,上述分配方式能夠盡量的減小磁盤空間的浪費,同時對於大表也能保證后續分配數據頁在整體上都是連續的
page0的第二部分內容即位xdes_entry,每個xdes_entry大小為40字節,一共256個xdes_entry,記錄接下來的256個區(256M表空間)的信息,xdes_entry的結構如下:
- File Segment ID (8) / FSEG_ID 當前的區所屬的段號,0代表當前區內的頁面通常分屬於不同的段,亦即當前區里面的頁大概率是碎片頁,具體還需要參見state字段
- List node for XDES list (12) 區塊的文件雙向鏈表
- State (4) 當前區的狀態
- FREE:歸屬於 FSP_FREE 鏈表(參見FSP_HEADER小結的FSP_FREE)
- FREE_FRAG:歸屬於 FSP_FREE_FRAG 鏈表 (參見FSP_HEADER小結的FSP_FREE_FRAG)
- FULL_FRAG:歸屬於 FSP_FULL_FRAG 鏈表 (參見FSP_HEADER小結的FSP_FULL_FRAG)
- FSEG:歸屬於某個Segment,亦即此時上述FSEG_ID在state為此值時候值才會大於0(有意義的segment id)
- Page State Bitmap (16) 2 bits per page, 1=free, 2=clean 16字節(16*8/2 = 64),代表一個區中64個頁的狀態
Inode(File Segment)
pageno=2的頁為文件段的元數據信息描述頁(此處略去pageno=1的插入緩沖位圖數據頁是因為此數據頁與表空間的存儲分配沒有特別大的關系,關聯性不大,后續有機會會專門介紹MySql的插入緩沖相關的內容)
照例先看數據結構圖:
12字節的文件鏈表頭以及85個entry,單個entry192字節,Entry的結構如下
- FSEG ID (8)/FSEG_ID 與entry對應的SegmentId ,為0表示當前segment id暫未使用
- Number of used pages in "NOT_FULL" list (4)/FSEG_NOT_FULL_N_USED FSEG_NOT_FULL鏈表上被使用的Page數量
- List base node for "FREE" list (16)/FSEG_FREE 空閑extent鏈表,完全沒有被使用並分配給該Segment的Extent鏈表
- List base node for "NOT_FULL" list (16)/FSEG_NOT_FULL 至少有一個page分配給當前Segment的Extent鏈表,全部用完時,轉移到FSEG_FULL上,全部釋放時,則歸還給當前表空間FSP_FREE鏈表
- List base node for "FULL" list (16)/FSEG_FULL 分配給當前segment且Page完全使用完的Extent鏈表
- Magic Number = 97937874 (4)/FSEG_MAGIC_N Magic Number,
- Fragment Array Entry 0 (4)/FSEG_FRAG_ARR(0) 屬於該Segment的獨立Page,總是先從全局分配獨立的Page,當填滿32個數組項時,每次分配時都分配一個完整的Extent,並在xdes_entry中將其Segment ID設置為當前值
- Fragment Array Entry 31 (4)/FSEG_FRAG_ARR(31) 總共能存儲32個數據頁(編號)碎片,可以看到碎片頁的分配數量為什么是32了
小結
可以看到,以下的幾個元數據頁的信息在結構上有幾個共同點:
- 對於文件鏈表表頭,均保存了鏈表的長度、表頭、表尾的指針指向
- 對於段、區元數據信息的描述,有專門的數據描述頁,對於整個表空間文件存儲空間的管理都是通過這些元數據頁信息來分配的
- Free/Not Full/Full 鏈表結構分別用於記錄完全空閑(按塊分配)、局部空閑(碎片分配)、滿空間的結構
- 對於存儲空間的管理具備在邏輯上是一個層次化的結構(參見表空間文件的整體結構圖)
- 頁的大小均是固定的16KB(此處不涉及壓縮數據的范疇),不同的是里面內部的數據結構不同決定了數據頁的類型、用途
- 設計上均采取header/body的形式(計算機科學中很多場景都會使用此中數據組織方式,場景的網絡層協議棧分層設計)
下面我們可以來看看完整的邏輯、物理結構文件映射圖:
自底向上,可以分為這幾部分
- 第一層為表空間數據文件物理結構,數據頁按照16kb的方式划分,數據頁之間用雙向鏈表的方式串接(后續介紹數據頁結構會描述),每64個數據頁(1M)的空間划分為一個區
- 第二層為區的邏輯划分,每一個xdes_entry數據頁描述256個區,xdes_entry數據頁之間的也用雙向鏈表銜接
- 第三層為Inode的描述結構,這里面有數據碎片頁的描述信息,更多的是對於下層區的管理與描述
- 第四層即為索引段的邏輯結構來,可以看到圖中的箭頭指向以及數據頁的pageno標號
附錄
-
文章大部分內容思路來源於https://blog.jcole.us/innodb/
英文好的同學可以直接閱讀原版內容