一、innodb_ruby 介绍
如果你想了解 MySQL InnoDB 内部结构,不好意思,官方在 5.7 版本之前并没有什么好的工具。所以你只能自己写一个工具去分析表空间文件或使用别人开源的工具,比如innodb_ruby。在 MySQL 5.7 版本,官方加强了innochecksum工具,使之也可以简单分析表空间,比如查看页类型统计信息等。但跟 Jeremy Cole 大神带来的Innodb格式化分析工具 innodb_ruby 相比还是弱了点(可以对比一下相同的功能)。innodb_ruby 工具的强大之处在于可以将 InnoDB 内部存储的信息多方位格式化的解释给大家,让人更清晰的理解 InnoDB 的内部结构。它是一款非常轻量级的应用,同时存在 bug 风险,不建议给生产环境使用。但是我们在测试环境就可以很好的了解该工具和该工具带来的便利。
README中是这样说的:
- 一款学习工具,让你很轻松的理解 InnoDB 架构。
- 一款教学工具,通过 ruby 的 irb 的交互式界面,可以看到 InnoDB 的架构与发生的变化。
- 一款调查工具,InnoDB 引擎不会告诉你它在磁盘正在做那些任务或者做过那些任务。例如,我们可以获取页的饱和度,每页具体含有多少记录,B+Tree 的具体架构等详细内容。
- 一款调试工具,当一些行为或操作会影响到数据结构的时候,可以通过 innodb_ruby 工具直观的审查,确保与期望的结果相同。
二、innodb_ruby 安装
此工具是使用 Ruby 写的,所以自然要安装 Ruby 环境。然后使用 RubyGems 进行安装 innodb_ruby,所以自然也要安装 RubyGems。我这里在 CentOS 下,直接就是用 Yum 安装了。
1 |
$ yum install ruby ruby-devel rubygems |
或安装指定版本 ruby
1 2 3 4 5 |
$ yum install centos-release-scl-rh $ yum install rh-ruby23 -y $ scl enable rh-ruby23 bash $ ruby -v ruby 2.3.8p459 (2018-10-18 revision 65136) [x86_64-linux] |
以 root 身份安装:
1 |
$ sudo gem install innodb_ruby |
以指定用户身份安装:
1 |
$ sudo gem install --user-install innodb_ruby |
运行:
1 |
$ innodb_space ... |
Note
RubyGems(简称 gems)是一个用于对 Ruby 组件进行打包的 Ruby 打包系统。 它提供一个分发 Ruby 程序和库的标准格式,还提供一个管理程序包安装的工具。RubyGems 的功能类似于 Linux 下的 yum 工具,使用它可以方便的从远程服务器下载并安装 Rails。
三、使用方法
有两种方式运行 innodb_space。
1 2 3 |
$ innodb_space --help | more innodb_space -s ibdata1 [-T tname [-I iname]] [options] innodb_space -f tname.ibd [-r ./desc.rb -d DescClass] [options] <mode> |
针对单个空间文件(ibdata 或 .ibd):
选项 | 参数 | 描述 |
---|---|---|
-F | <文件名> | 加载表空间文件(系统表空间或独立表空间) |
针对将自动加载每个文件的表空间文件的系统表空间:
选项 | 参数 | 描述 |
---|---|---|
-s | <文件名> | 加载系统表空间文件(例如ibdata1) |
-T | <表名> | 使用给定的表名,格式DB_NAME/TABLE_NAME。 |
-I | <索引名称> | 使用给定的索引名称。 |
四、功能介绍
先创建一些测试数据。
1 2 3 4 5 6 7 8 9 10 11 12 |
create table t( a int not null, b varchar(8000), c int not null, primary key (a), key idx_c (c) ) engine=innodb; insert into t select 1,repeat('a',7000),1; insert into t select 2,repeat('a',7000),2; insert into t select 3,repeat('a',7000),3; insert into t select 4,repeat('a',7000),4; |
在这个表中,插入 b 列长度为 7000,因此可以人为的方式使目前每个页(每页 16k)只能存放两条记录。
- Space File Structure(表空间文件结构)
system-spaces
在该模式下,innodb_space 列出所有物理对象的数量。
1 2 3 4 5 |
$ innodb_space -s ibdata1 system-spaces name pages indexes (system) 13056 6 test/t 9 2 ... |
space-indexes
列出表空间中所有索引统计信息(系统空间或每个文件表空间)
1 2 3 4 5 6 |
$ innodb_space -s ibdata1 -T test/t space-indexes id name root fseg used allocated fill_factor 264 PRIMARY 3 internal 1 1 100.00% 264 PRIMARY 3 leaf 3 3 100.00% 265 idx_c 4 internal 1 1 100.00% 265 idx_c 4 leaf 0 0 0.00% |
id:表示此索引的ID。
name:索引的名称,PRIMARY代表的就是聚集索引,因为InnoDB表是聚集索引组织表,行记录就是聚集索引;idx_c就是辅助索引的名称。
root:索引中根节点的page号,可以看出聚集索引的Root节点是第3号page(为什么是从第3号Page开始,关于0、1、2号Page看下文space-page-type-regions),辅助索引的根节点是从第4号Page开始。
fseg:page的说明,internal表示非叶子节点或属于根节点,leaf表示叶子节点(也就是数据页)。
used:索引使用了多少个page,可以看出聚集索引的非叶子节点使用了1个page,叶子节点使用了3个page;辅助索引idx_c的叶子节点使用了1个page。
allocated:索引分配了多少个page,可以看出聚集索引的非叶子节点分配了1个page,叶子节点分配了3个page;辅助索引idx_c的叶子节点分配了1个page。
fill_factor:索引的填充度,used/allocated表示填充度,也就是实际使用的大小百分比。
另外,这些信息同样在MySQL系统视图中可以查询的到,如下SQL(space等于0的为系统表)。
1 2 3 4 5 6 7 8 9 10 |
SELECT b.name, a.name, index_id, TYPE, a.space, a.PAGE_NO FROM information_schema.INNODB_SYS_INDEXES a, information_schema.INNODB_SYS_TABLES b WHERE a.table_id = b.table_id AND a.space <> 0; |
space-page-type-regions
遍历空间中的所有页面,统计每个类型的页共占用了多少页。
1 2 3 4 5 6 7 |
$ innodb_space -s ibdata1 -T test/t space-page-type-regions start end count type 0 0 1 FSP_HDR 1 1 1 IBUF_BITMAP 2 2 1 INODE 3 7 5 INDEX 8 8 1 FREE (ALLOCATED) |
start:从第几个page开始。
end:从第几个page结束。
count:占用了多少个page。
type:page的类型。
从上面的结果可以看出:“FSP_HDR”、“IBUF_BITMAP”、“INODE”是分别占用了0,1,2号的page,从3号page开始才是存放数据和索引的页(Index),占用了3~7号的page,共5个page。
接下来,根据得到的聚集索引和辅助索引的根节点来获取索引上的其他page的信息。
space-page-type-summary
遍历所有页面并按类型打印总页数总计。
1 2 3 4 5 6 7 |
$ innodb_space -s ibdata1 -T test/t space-page-type-summary type count percent description INDEX 5 55.56 B+Tree index INODE 1 11.11 File segment inode IBUF_BITMAP 1 11.11 Insert buffer bitmap FSP_HDR 1 11.11 File space header ALLOCATED 1 11.11 Freshly allocated |
space-extents-illustrate
统计所有的页在表空间的饱和度信息,每个页面显示彩色块(按index/purpose着色),根据页面中的数据量调整大小。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ innodb_space -s ibdata1 -T test/t space-extents-illustrate Start Page ╭────────────────────────────────────────────────────────────────╮ 0 │███▁▁▄▇▄░ │ ╰────────────────────────────────────────────────────────────────╯ Legend (█ = 1 page): Page Type Pages Ratio █ System 3 33.33% █ Index 266 (test/t.PRIMARY) 4 44.44% █ Index 267 (test/t.idx_c) 1 11.11% ░ Free space 1 11.11% |
space-lsn-age-illustrate
统计所有的页在表空间的饱和度信息,每个页面显示彩色块 (按页面修改LSN的年龄着色)。
1 2 3 4 5 6 7 8 9 |
$ innodb_space -s ibdata1 -T test/t space-lsn-age-illustrate Start Page ╭────────────────────────────────────────────────────────────────╮ 0 │████████ │ ╰────────────────────────────────────────────────────────────────╯ Legend (█ = 1 page): Min LSN <─────────────────────────────────────────> Max LSN 133718587851 ███████████████████████████████████████████ 133718640766 |
- Page Structure(页结构)
page-account
解释单个页面的用途。
1 2 3 4 5 6 7 8 9 10 11 |
$ innodb_space -s ibdata1 -T test/t -p 3 page-account Accounting for page 3: Page type is INDEX (B+Tree index, table and index data stored in B+Tree structure). Extent descriptor for pages 0-63 is at page 0, offset 158. Extent is not fully allocated to an fseg; may be a fragment extent. Page is marked as used in extent descriptor. Extent is in free_frag list of space. Page is in fragment array of fseg 1. Fseg is in internal fseg of index 266. Index root is page 3. Index is test/t.PRIMARY. |
page-dump
打印页结构信息,需要了解InnoDB页结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
$ innodb_space -s ibdata1 -T test/t -p 3 page-dump #<Innodb::Page::Index:0x00000001e582c8>: fil header: {:checksum=>926525874, :offset=>3, :prev=>nil, :next=>nil, :lsn=>133718640734, :type=>:INDEX, :flush_lsn=>0, :space_id=>133} fil trailer: {:checksum=>712405307, :lsn_low32=>574654558} page header: {:n_dir_slots=>2, :heap_top=>162, :garbage_offset=>0, :garbage_size=>0, :last_insert_offset=>154, :direction=>:right, :n_direction=>2, :n_recs=>3, :max_trx_id=>0, :level=>1, :index_id=>266, :n_heap=>5, :format=>:compact} fseg header: {:leaf=> <Innodb::Inode space=<Innodb::Space file="test/t.ibd", page_size=16384, pages=9>, fseg=2>, :internal=> <Innodb::Inode space=<Innodb::Space file="test/t.ibd", page_size=16384, pages=9>, fseg=1>} sizes: header 120 trailer 8 directory 4 free 16210 used 174 record 42 per record 14.00 page directory: [99, 112] system records: {:offset=>99, :header=> {:next=>126, :type=>:infimum, :heap_number=>0, :n_owned=>1, :min_rec=>false, :deleted=>false, :length=>5}, :next=>126, :data=>"infimum\x00", :length=>8} {:offset=>112, :header=> {:next=>112, :type=>:supremum, :heap_number=>1, :n_owned=>4, :min_rec=>false, :deleted=>false, :length=>5}, :next=>112, :data=>"supremum", :length=>8} garbage records: records: {:format=>:compact, :offset=>126, :header=> {:next=>140, :type=>:node_pointer, :heap_number=>2, :n_owned=>0, :min_rec=>true, :deleted=>false, :nulls=>[], :lengths=>{}, :externs=>[], :length=>5}, :next=>140, :type=>:clustered, :key=>[{:name=>"a", :type=>"INT", :value=>1}], :row=>[], :sys=>[], :child_page_number=>5, :length=>8} {:format=>:compact, :offset=>140, :header=> {:next=>154, :type=>:node_pointer, :heap_number=>3, :n_owned=>0, :min_rec=>false, :deleted=>false, :nulls=>[], :lengths=>{}, :externs=>[], :length=>5}, :next=>154, :type=>:clustered, :key=>[{:name=>"a", :type=>"INT", :value=>2}], :row=>[], :sys=>[], :child_page_number=>6, :length=>8} {:format=>:compact, :offset=>154, :header=> {:next=>112, :type=>:node_pointer, :heap_number=>4, :n_owned=>0, :min_rec=>false, :deleted=>false, :nulls=>[], :lengths=>{}, :externs=>[], :length=>5}, :next=>112, :type=>:clustered, :key=>[{:name=>"a", :type=>"INT", :value=>4}], :row=>[], :sys=>[], :child_page_number=>7, :length=>8} |
page-records
汇总页面内的所有记录。
1 2 3 4 |
$ innodb_space -s ibdata1 -T test/t -p 3 page-records Record 126: (a=1) → #5 Record 140: (a=2) → #6 Record 154: (a=4) → #7 |
上面的结果是解析聚集索引根节点页的信息,1行就代表使用了1个page。所以,叶子节点共使用了3个page,根节点使用了1个page,这是一棵高度为2的B+树,跟space_indexes的解析结果一致。
Record 126: (a=1) → #5
a = 1,代表的就是表中a为1的记录,因为a是主键。
-> #5,代表的是指向5号page。
Record 126: (a=5) → #5,整行的意思就是5号page的a最小值是1,包含了1~2的行记录。
注意:page号并不是连续的。如果想打印出具体行数据,直接“-p 5”就可以打印出具体记录,聚集索引的叶子节点是包含了行记录的所有数据。
同理,解析辅助索引idx_c,但是需要注意的是,在解析辅助索引是,需要加上“-I idx_c”。
1 2 3 4 5 |
$ innodb_space -s ibdata1 -T test/t -I idx_c -p 4 page-records Record 125: (c=1) → (a=1) Record 138: (c=2) → (a=2) Record 151: (c=3) → (a=3) Record 164: (c=4) → (a=4) |
从上面可以看出,辅助索引idx_c的key是c列,对应主键。叶子节点共使用了1个page,这棵B+树高度为1。你可以多构造一点数据,这样,这里显示的信息与3号page类似,比如“(c=1) -> #9”,然后在9号页面会显示“(c=1) → (a=1)”。
page-directory-summary
页目录字典记录。
1 2 3 4 5 6 7 8 9 10 11 |
$ innodb_space -s ibdata1 -T test/redis -p 3 page-directory-summary slot offset type owned key 0 99 infimum 1 1 174 node_pointer 4 () 2 238 node_pointer 4 () 3 302 node_pointer 4 () 4 366 node_pointer 4 () 5 430 node_pointer 4 () 6 494 node_pointer 4 () 7 558 node_pointer 4 () 8 112 supremum 6 |
从owned可以看出,每个字典一般包含4~8个数据记录,这里的infimum和supremum是page的下(上)确界。
page-illustrate
详细说明一个页面的内容,并且根据类型进行着色显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ innodb_space -s ibdata1 -T test/redis -p 3 page-illustrate Offset ╭────────────────────────────────────────────────────────────────╮ 0 │█████████████████████████████████████▋██████████████████████████│ 64 │█████████▋███████████████████▋████████████▋████████████▋ ████▋██ │ 128 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██ │ 192 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 256 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 320 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 384 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 448 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 512 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 576 │█▋ ████▋███▋ ████▋███▋ ████▋███▋ ████▋██│ 640 │█▋ | 704 │ │ ....... 16256 │ │ 16320 │ █▋█▋█▋█▋█▋█▋█▋█▋███████▋ │ ╰────────────────────────────────────────────────────────────────╯ Legend (█ = 1 byte): Region Type Bytes Ratio █ FIL Header 38 0.23% █ Index Header 36 0.22% █ File Segment Header 20 0.12% █ Infimum 13 0.08% █ Supremum 13 0.08% █ Record Header 165 1.01% █ Record Data 132 0.81% █ Page Directory 18 0.11% █ FIL Trailer 8 0.05% ░ Garbage 0 0.00% Free 15941 97.30% |
- Index Structure(索引结构)
index-recurse
通过递归整个B+树(通过递归扫描所有页面,而不仅仅是按列表的叶子页面)来执行索引扫描(执行完整索引扫描):
1 2 3 4 5 6 |
$ innodb_space -s ibdata1 -T test/t -I PRIMARY index-recurse ROOT NODE #3: 3 records, 42 bytes NODE POINTER RECORD ≥ (a=1) → #5 LEAF NODE #5: 1 records, 7029 bytes RECORD: (a=1) → (b="aaaaaaaaaa.., c=1) .... |
可以看到主键(这里是a)与记录的对应关系。
查看辅助索引,可以看到辅助索引是直接索引他对应的主键。
1 2 3 4 5 6 |
$ innodb_space -s ibdata1 -T test/t -I idx_c index-recurse ROOT NODE #4: 4 records, 52 bytes RECORD: (c=1) → (a=1) RECORD: (c=2) → (a=2) RECORD: (c=3) → (a=3) RECORD: (c=4) → (a=4) |
index-record-offsets
将索引作为索引递归进行递归处理,但在索引页中打印每条记录的偏移量。
1 2 3 4 5 6 |
$ innodb_space -s ibdata1 -T test/t -I PRIMARY index-record-offsets page_offset record_offset 5 128 6 128 6 7157 7 128 |
index-level-summary
打印给定级别的所有索引页面的摘要信息。一般level 0就是叶子节点、大于level 0的就是非叶子节点(可能是非叶子节点或根节点,要看B+树高度)。
1 2 3 4 5 |
$ innodb_space -s ibdata1 -T test/t -I PRIMARY -l 0 index-level-summary page index level data free records min_key 5 266 0 7029 9223 1 a=1 6 266 0 14058 2194 2 a=2 7 266 0 7029 9223 1 a=4 |
- Record Structure(记录结构)
record-dump
给定记录偏移量,打印记录的详细说明及其包含的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ innodb_space -s ibdata1 -T test/t -p 5 -R 128 record-dump Record at offset 128 Header: Next record offset : 112 Heap number : 2 Type : conventional Deleted : false Length : 8 System fields: Transaction ID: 176582861 Roll Pointer: Undo Log: page 441, offset 272 Rollback Segment ID: 123 Insert: true Key fields: a: 1 Non-key fields: b: "aaaaa..." c: 1 |
- Record History(记录历史)
record-history
显示给定记录的unod日志。
1 2 3 |
$ innodb_space -s ibdata1 -T test/t -p 5 -R 128 record-history Transaction Type Undo record (n/a) insert (a=-1017027736) → () |
undo-history-summary
显示所有变动过的记录。
1 |
$ innodb_space -s ibdata1 undo-history-summary |
- Additional exploration(额外探索)
space-lists
显示表空间列表统计信息(free, free_frag, full_frag, free_inodes, and full_inodes),包括列表中的第一个和最后一个页面的列表长度和列表节点信息。
1 2 3 4 5 6 7 |
$ innodb_space -s ibdata1 space-lists name length f_page f_offset l_page l_offset free 82 0 3478 0 3918 free_frag 3 0 1638 0 638 full_frag 7 0 158 0 318 full_inodes 4 2 38 498 38 free_inodes 3 323 38 412 38 |
space-list-iterate
迭代列表中的所有范围,并在列表中显示范围或inode。
1 |
$ innodb_space -s ibdata1 space-list-iterate -L free_frag |
space-inodes-summary
打印表空间中每个inode的摘要信息。
1 |
$ innodb_space -s ibdata1 space-inodes-summary |
<参考>
https://github.com/jeremycole/innodb_ruby/wiki
https://blog.csdn.net/chuck_perry/article/details/64441289