
Percona 8.0.30中"show engine innodb status"导致coredump排查
1. 异常崩溃
GreatSQL合并Percona-Server 8.0.30的Beta版测试中,QA报了一个crash的bug:
2. 捕风捉影
2.1 信息缺失
通常有core文件的话定位bug不算太难。
第一时间使用GDB打开core文件,切到相应函数调用栈尝试打印关键变量的信息:
但无论尝试任何变量,使用gdb打印时均会报:“gdb: No symbol "xxx" in current context”。
最初怀疑是GDB版本过低,安装devtoolset-11并使用高版本GDB后问题依旧。检查出包时的编译记录,发现cmake后,"CMAKE_CXX_FLAGS_RELWITHDEBINFO"后的编译选项多了"-g1"。
原因是新版本多了编译选项“MINIMAL_RELWITHDEBINFO”,该选项在打包时自动开启。
向打包脚本添加"-DMINIMAL_RELWITHDEBINFO=OFF"后重新打包。经验证,此时GDB能够attach到新部署的数据库实例并打印变量。
2.2 问题复现过程
- 本地编译安装。本地操作系统为ubuntu20.04,使用源码编译安装数据库实例后导入数据,未能复现crash。
- 使用新打的包在另一台物理机部署实例并导入数据测试,未复现crash。新部署的数据库实例在一台配置较高的物理机上,而出现问题的机器是一台8 core,16G ram的虚拟机。
- 重新在发生crash的虚拟机上部署实例并导入数据测试,依旧未复现crash。
- 为排除参数差异的影响:经比对修改后虚拟机上两实例的配置除basedir、datadir、log文件位置、端口号等无法改为一致的外,其余参数均相等。依旧未能复现crash。
- 在发生crash的数据库实例上,使用新包替换可执行程序,执行导入测试,能够复现crash。
2.3 问题分析
经上述尝试后,开始基于原始环境复现问题并分析core文件中的异常内存数据。
- 经多次复现,每次crash的位置并不完全一致。
- 为缩小问题范围,将benchmarksql导入数据的woker改为1,验证是否和多session并发写入有关。并发度改为1后,经7-10分钟,crash再次发生。说明问题和多线程并发导入无关。
由于crash时的位置不同,选择较早crash时的core文件进行分析,堆栈如下(发生在parser阶段):
切到"#3",打印mem_root_deque<Item*>
对象的内容:
- 发现其头部内容存在非法地址。
- 按字符串格式输出内存内容时,发现其尾部内容被填充了一小段
"INSERT INTO bmsql_xxxx ..."
。
切到"#7",“p parser_state->m_lip->m_buf_length
”输出原始sql语句的长度,将gdb定向到log文件,调整print element足够大后执行“p parser_state->m_lip->m_buf
”,导出原始sql语句到log文件。 此时可发现异常内存的内容为当前语句的头部。
由于“mem_root_deque”使用的内存是从当前THD->mem_root分配的,基本可判断是当前session的"thd->mem_root"分配异常。即已分配出去的内存被重新分配给其它对象,最终造成coredump。
3. 误打误撞
以上信息还不够准确定位问题。为进一步缩小排查范围,尝试将benchmarksql在load数据期间发送的语句记录到general_log,将general_log中的语句导出到文本,使用文本中固定的语句尝试触发crash并验证crash时间点是否一致。
重启数据库实例、清理并重建database、开启general_log、启动benchmarksql导入。一段时间后数据库crash,一切按预期进行。
打开general_log,找benchmarksql对应的session时发现general_log多了部分内容,每隔几秒执行一次。
看到多的这部分内容,心里一震,原来QA部署的数据库实例会被平台纳管。 莫非是这些语句导致benchmarksql导入数据时crash?
4. 真相大白
写了个简单脚本,循环向数据库实例发送指定文件中的sql语句
file repeat_sql_file.sh:
再将general log中的语句取出到文件monit.sql,执行命令"file repeat_sql_file.sh monit.sql"。然后启动benchmarksql导入数据。
仅仅过了15秒,数据库实例crash。
接下来要找到肇事sql。采用二分查找法先注释一半内容逐步验证,重复几次后将肇事语句锁定到了DBA最常用的一条语句:
下载Percona-8.0.30官方源码并按上述步骤编译安装部署后,重现了crash!
继续验证MySQL官方的8.0.30,发现该crash没有触发。
5. Release notes review
既然已明确是Percona-Server的bug,那就先到MySQL和Percona官网看新版本release note有没有和"show engine innodb status"相关的bugfix。
首先是Percona-8.0.31,未看到相关bugfix说明,只看到一个从库crash的bugfix。
(https://docs.percona.com/percona-server/8.0/release-notes/8.0.31-23.html#bug-fixes)
但release notes list中看到多了一个8.0.30-update:其中有一个bugfix涉及到"show engine innodb status"
(https://docs.percona.com/percona-server/8.0/release-notes/8.0.30-22.upd.html#bug-fixes)
6. 源码分析
定位Percona 8.0.30 PS-8351相关patch,可看到Percona修改了一些代码,而这部分代码在MySQL 8.0.30上是未被修改的:
其调用逻辑为:
而在trx_print_low中:很明显作为参数传递下去的"trx->mysql_thd"归属于实例中其它活跃的session,而不是归属于当前执行"show engine innodb status"的session
继续看convert_string:执行"show engine innodb status"的session使用了其它session的mem_root开辟了内存空间。
因此,一旦“thd->convert_string()
”使用thd->memroot申请内存,就会出现两个线程并发操作thd->memroot。由于对同一mem_root的操作不是线程安全的,两个线程分配的内存空间可能存在重叠。
随着"show engine innodb status"线程的运行,重叠内存区域的内容可能会被填上部分原始sql语句。回顾上文中打印mem_root_deque<Item*>
对象的内存内容,其尾部也确实包含了原始sql语句的开头。
7. 总结
- 排查问题时尽量规避外界的影响。
- 如果生产系统使用了Percona-8.0.30,请慎用"show engine innodb status"。
目前MySQL-8.0.32版本的release note中已有相关的patch:
(https://github.com/mysql/mysql-server/commit/14cc6bab13ba24c75654e87df4b3c66e55277fcb)
从commit信息看,该commit来自Percona。
解决了show engine innodb status乱码问题的同时,不再使用“convert_string()”。
本文转载自公众号:阿里云数据库
