作者:xuty
本文起源:原创投稿
* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。
一、景象
有个 MySQL 5.7 开发库异样挂掉后,奔溃复原始终处于如下地位,且继续了 2 小时左右才起来。十分纳闷这段时间 MySQL 到底做了什么事件?竟然须要这么长时间。
虽说这里虚拟机的 IOPS 并不是很高,但也相对不须要这么久吧?而且从日志输入来看,这块应该也不是在做真正的数据恢复,那么也能够排除是大事务回滚导致的耗时长,那么起因到底是啥呢?
值得注意的是,这台开发库下面有将近 1500 个库和上万张表,难道 MySQL 解体复原时长 和 表的数量 也存在肯定关系嘛?
二、剖析栈帧
在 MySQL 解体复原时,用 pstack
打了栈帧,再用 pt-pmp
工具剖析栈帧后显示如下:
pread64(libpthread.so.0),os_file_io(os0file.cc:5435),
os_file_pread(os0file.cc:5612),os_file_read_page(os0file.cc:5612),
os_file_read_no_error_handling_func(os0file.cc:6069),
pfs_os_file_read_no_error_handling_func(os0file.ic:341),
Datafile::read_first_page(os0file.ic:341),Datafile::validate_first_page(fsp0file.cc:551),
Datafile::validate_to_dd(fsp0file.cc:404),fil_ibd_open(fil0fil.cc:3969),
dict_check_sys_tables(dict0load.cc:1465),dict_check_tablespaces_and_store_max_id(dict0load.cc:1525),
innobase_start_or_create_for_mysql(srv0start.cc:2329),innobase_init(ha_innodb.cc:4048),
ha_initialize_handlerton(handler.cc:838),plugin_initialize(sql_plugin.cc:1197),
plugin_init(sql_plugin.cc:1538),init_server_components(mysqld.cc:4033),
mysqld_main(mysqld.cc:4673),__libc_start_main(libc.so.6),_start
依据函数名字,感觉像是在 遍历校验每个表空间文件的有效性?,难道 MySQL 解体复原时会额定进行校验操作?貌似和表数量扯上点关系了。
三、GDB 调试
Server version: 5.7.26-log MySQL Community Server (GPL)
间接去剖析源码感觉有点找不到切入点,因为不晓得失常启动是不是也是这样的函数调用。
为了晓得 失常启动 与 解体复原 的区别,先在本地的 MySQL 5.7.26 环境中用 GDB 调试 MySQL 启动过程,看下失常启动和解体复原的函数调用有哪些区别,再针对性的去剖析源码比拟好。
-- 将之前的栈帧弄成了树状,便于剖析
>innobase_init
| >innobase_start_or_create_for_mysql
| | >dict_check_tablespaces_and_store_max_id
| | | >dict_check_sys_tables
| | | | >fil_ibd_open
| | | | | >Datafile::validate_to_dd
| | | | | | >Datafile::validate_first_page
| | | | | | | >Datafile::read_first_page
| | | | | | | | >pfs_os_file_read_no_error_handling_func
| | | | | | | | | >os_file_read_no_error_handling_func
| | | | | | | | | | >os_file_read_page
| | | | | | | | | | | >os_file_pread
| | | | | | | | | | | | >os_file_io
失常启动 GDB 调试后果:
从上到下,每次打一个断点函数,发现到 Datafile::validate_to_dd
这个函数时,MySQL 失常启动就不会执行,看样子是 fil_ibd_open
函数中做了某些判断。
解体复原 GDB 调试后果:
一边用 sysbench 压,一边间接 kill - 9 过程就能够模仿解体复原,同样从上到下,顺次打断点函数,发现会走到 Datafile::validate_to_dd
这个函数中,Continue 后会始终断点在这个函数上,阐明外层包装了一层循环会遍历所有表,如果持续减少断点函数的话,发现绝大部分表会持续走上来,直到os_file_io
,而小局部表则不会持续走上来。
四、源码剖析
4.1. fil_ibd_open
咱们先去 fil_ibd_open
函数中看下,进入 Datafile::validate_to_dd
函数的判断条件,发现次要和一个 validate
参数无关,如果为 false
则能够跳过检测,为 true
则须要进入 Datafile::validate_to_dd
函数。
4.2. innobase_start_or_create_for_mysql
而后咱们须要看下 validate
参数的定义,剖析解体复原与失常启动的区别。
发现 validate 参数
最早是在 innobase_start_or_create_for_mysql
函数中定义的,并且其正文曾经解释的十分具体。
- 失常启动:间接为每张表的创立 space object 即可,不须要关上 ibd 文件的 header page 进行表空间校验。
- 解体复原:为了数据字典的一致性,须要遍历关上所有 ibd 文件的 header page 进行表空间校验。
validate 这个参数表明当一个表空间被关上时,同时会去读取其 ibd 文件的头页(header page)来验证数据字典的一致性,而当数据库蕴含许多 ibd 文件时,这个过程就会比拟久,所以只在解体复原且非强制复原时执行表空间校验操作!
4.3. recv_needed_recovery & srv_force_recovery
接着咱们来看下决定 validate 值的 2 个参数:recv_needed_recovery
与 srv_force_recovery
,默认解体复原时,recv_needed_recovery = 1 而 srv_force_recovery = 0,所以 validate = true,即须要进行表空间校验。
bool validate = recv_needed_recovery && srv_force_recovery == 0;
// 跳过表空间校验
validate = false
// 执行表空间校验
validate = true
先看下 recv_needed_recovery
参数,默认为 0。MySQL 在启动时会比对 checkpoint_lsn
与 flush_lsn
。如果不相等,就会调用 recv_init_crash_recovery
办法将 recv_needed_recovery
置为 1
。只有当 MySQL 失常敞开时,这 2 个 lsn 才会相等。另外一个小发现,MySQL 5.7 中服务起来后,什么操作都不做,checkpoint_lsn 永远会落后 9,所以即便你什么都不做,间接 kill -9 过程,也算是解体重启。
LOG
---
Log sequence number 2563079308
Log flushed up to 2563079308
Pages flushed up to 2563079308
Last checkpoint at 2563079299
再来看下 srv_force_recovery
参数,默认值为 0,如果设置了 innodb_force_recovery,那么 srv_force_recovery 的值就等于 innodb_force_recovery 的值,即只有配置了强制复原,srv_force_recovery 就会大于 0。
4.4. dict_check_tablespaces_and_store_max_id
最初看下 dict_check_tablespaces_and_store_max_id
函数,依据正文介绍,这个函数会查看所有在数据字典中发现的表空间,先查看每个共享表空间,而后查看每个独立表空间。
在 解体复原 中,局部表空间曾经在解决 redolog 时被关上(对应之前 GDB 调试时局部表未持续走上来),而其余没有被关上的表空间,将会通过比拟数据字典中的 space_id 与表空间文件是否统一的形式进行验证(也就是之前所说的 表空间校验过程)。
五、测试验证
到这里,原理大略曾经晓得了,次要就是:MySQL 在解体复原时,会遍历关上所有 ibd 文件的 header page 验证数据字典的准确性,如果 MySQL 中蕴含了大量表,这个校验过程就会比拟耗时。
那么咱们能够模仿下这个场景,进一步验证,比方在 测试库 中用 sysbench 建 50W 张空表,而后模仿非正常敞开,比照下解体复原时长。
MySQL 实例 | 表总数 | 磁盘类型 | 启动类型 | 时长 |
---|---|---|---|---|
开发库(5.7.20) | 57w | HDD | 解体复原 | 2 小时左右 |
测试库(5.7.26) | 空库 | SSD | 解体复原 | 秒级 |
测试库(5.7.26) | 50w | SSD | 失常启动 | 40 秒左右 |
测试库(5.7.26) | 50w | SSD | 解体复原 | 7 分钟左右 |
测试库(8.0.18) | 50w | SSD | 失常启动 | 4 分钟左右 |
测试库(8.0.18) | 50w | SSD | 解体复原 | 10 分钟左右 |
能够看到 MySQL 下解体复原的确和表数量无关,表总数越大,解体复原工夫越长。另外磁盘 IOPS 也会影响解体复原工夫,像这里开发库的 HDD IOPS 较低,因而面对大量的表空间,校验速度就十分迟缓。
另外一个发现,MySQL 8 下失常启用时竟然也会进行表空间校验,而故障复原时则会额定再进行一次表空间校验,等于校验了 2 遍。不过 MySQL 8.0 里多了一个个性,即表数量超过 5W 时,会启用多线程扫描,放慢表空间校验过程。
MySQL 8.0.21 开始能够通过 innodb_validate_tablespace_paths
参数敞开失常启动时的表空间校验过程。
六、如何跳过校验
MySQL 5.7 下有办法能够跳过解体复原时的表空间校验过程嘛?查阅了材料,办法次要有两种:
- 配置 innodb_force_recovery
能够使 srv_force_recovery != 0,那么 validate = false,即能够跳过表空间校验。理论测试的时候设置 innodb_force_recovery =1,也就是强制复原跳过坏页,就能够跳过校验,而后重启就是失常启动了。通过这种长期形式能够防止解体复原后十分耗时的表空间校验过程,疾速启动 MySQL,集体目前临时未发现有什么隐患。
-
应用共享表空间代替独立表空间
这样就不须要关上 N 个 ibd 文件了,只须要关上一个 ibdata 文件即可,大大节俭了校验工夫。自从听了姜老师讲过应用共享表空间代替独立表空间解决 drop 大表时性能抖动的原理后,感觉共享表空间在很多业务环境下,反而更有劣势。
bool validate = recv_needed_recovery && srv_force_recovery == 0; // 跳过表空间校验 validate = false // 执行表空间校验 validate = true
长期冒出另外一种解决想法,即用 GDB 调试解体复原,通过长期批改 validate 变量值让 MySQL 跳过表空间验证过程,而后让 MySQL 失常敞开,重新启动就能够失常启动了。
然而理论测试发现,如果以 debug 模式运行,的确能够长期批改 validate 变量,跳过表空间验证过程,然而 debug 模式下代码运行效率大打折扣,反而耗时更长。而以非 debug 模式运行,则无奈批改 validate 变量,想法幻灭。
附录:
https://dev.mysql.com/worklog…
http://blog.symedia.pl/2015/1…
https://www.percona.com/commu…
https://jira.mariadb.org/brow…