关于sysbench:故障分析-崩溃恢复巨慢原因分析

作者: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 函数中定义的,并且其正文曾经解释的十分具体。

  1. 失常启动:间接为每张表的创立 space object 即可,不须要关上 ibd 文件的 header page 进行表空间校验。
  2. 解体复原:为了数据字典的一致性,须要遍历关上所有 ibd 文件的 header page 进行表空间校验。

validate 这个参数表明当一个表空间被关上时,同时会去读取其ibd文件的头页(header page)来验证数据字典的一致性,而当数据库蕴含许多ibd文件时,这个过程就会比拟久,所以只在解体复原且非强制复原时执行表空间校验操作!

4.3. recv_needed_recovery & srv_force_recovery

接着咱们来看下决定 validate 值的 2 个参数:recv_needed_recoverysrv_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_lsnflush_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 下有办法能够跳过解体复原时的表空间校验过程嘛?查阅了材料,办法次要有两种:

  1. 配置 innodb_force_recovery

    能够使 srv_force_recovery != 0 ,那么 validate = false,即能够跳过表空间校验。理论测试的时候设置 innodb_force_recovery =1 ,也就是强制复原跳过坏页,就能够跳过校验,而后重启就是失常启动了。通过这种长期形式能够防止解体复原后十分耗时的表空间校验过程,疾速启动 MySQL,集体目前临时未发现有什么隐患。

  2. 应用共享表空间代替独立表空间

    这样就不须要关上 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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理