数据库内核月报

数据库内核月报 - 2015 / 05

MySQL · 捉虫动态 · 5.6 与 5.5 InnoDB 不兼容导致 crash

Author: 襄洛

bug 背景

RDS的备份工具用的是 Percona-XtraBackup(后面简称PXB),这个工具包里有2个重要的工具,innobackupex和xtrabackup,后者是C编译出的二进制文件,负责备份 InnoDB 数据,前者是一个Perl 脚本,对后者进行封装,同时负责备份非 InnoDB 数据。xtrabackup 二进制里内嵌了InnoDB引擎,所以能很好的处理InnoDB数据。在2.2版本之前,PXB 分别针对不同版本的 MySQL 源码(5.1/5.5/5.6)编译了不同版本的xtrabackup,以便备份不同版本的MySQL数据,然而在2.2之后,PXB官方觉得5.6已经可以很好的兼容5.1和5.5,所以就只针对 5.6.22 版本的代码编译了一个 xtrabackup 二进制文件,关于这个改动可以看官方的BP #single binary。故事就由此发生。。。

bug 描述

当我们对5.5版本的备份集进行还原的时候,xtrabackup crash 了,报错信息如下:

2015-05-12 19:03:08 7fd9d8258720  InnoDB: Assertion failure in thread 140573610968864 in file pars0pars.cc line 865
InnoDB: Failing assertion: sym_node->table != NULL
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.6/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
11:03:08 UTC - xtrabackup got signal 6 ;
This could be because you hit a bug or data is corrupted.
This error can also be caused by malfunctioning hardware.
We will try our best to scrape up some info that will hopefully help
diagnose the problem, but since we have already crashed,
something is definitely wrong and this may fail.

Thread pointer: 0x176aeb0
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
stack_bottom = 0 thread_stack 0x10000
xtrabackup(my_print_stacktrace+0x35) [0x9f5331]
xtrabackup(handle_fatal_signal+0x2bb) [0x7f801b]
/lib64/libpthread.so.0() [0x3530c0f500]
/lib64/libc.so.6(gsignal+0x35) [0x35f40328a5]
/lib64/libc.so.6(abort+0x175) [0x35f4034085]
xtrabackup() [0x76bfb0]
xtrabackup(pars_update_statement(upd_node_t*, sym_node_t*, void*)+0x30) [0x76c8d8]
xtrabackup(yyparse()+0xcb1) [0xa5ef27]
xtrabackup(pars_sql(pars_info_t*, char const*)+0xaf) [0x76e06d]
xtrabackup(que_eval_sql(pars_info_t*, char const*, unsigned long, trx_t*)+0x85) [0x78eeb2]
xtrabackup(row_drop_table_for_mysql(char const*, trx_t*, bool, bool)+0xa98) [0x720a0c]
xtrabackup(row_mysql_drop_temp_tables()+0x24c) [0x721503]
xtrabackup(recv_recovery_rollback_active()+0x2c) [0x753ebe]
xtrabackup(innobase_start_or_create_for_mysql()+0x17aa) [0x7293c4]
xtrabackup() [0x607a00]
xtrabackup() [0x610204]
xtrabackup(main+0x8b8) [0x611674]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x35f401ecdd]
xtrabackup() [0x604369]

bug 分析

从crash的信息InnoDB: Assertion failure in thread 140573610968864 in file pars0pars.cc line 865,可以定位的crash的代码点

862|                sym_node->table = dict_table_open_on_name(
863|                        sym_node->name, TRUE, FALSE, DICT_ERR_IGNORE_NONE);
864|
865|                ut_a(sym_node->table != NULL);

可以看到,是 InnoDB 在试图打开一张表的时候,打开失败,直接 assert 了。

分析调用堆栈,是xtrabackup在恢复数据的时候,启动了内嵌的 InnoDB 引擎,在活跃事务回滚的时候,会将备份时候存在的临时表全部 drop 掉。在删除表的时候,除了要删除表本身,还需要删除在 InnoDB 系统表中的记录,删除记录是通过内部执行sql的方式做的que_eval_sql,其中有这么一段sql

4195|                           "DELETE FROM SYS_TABLESPACES\n"
4196|                           "WHERE SPACE = space_id;\n"
4197|                           "DELETE FROM SYS_DATAFILES\n"
4198|                           "WHERE SPACE = space_id;\n"

SYS_TABLESPACES 和 SYS_DATAFILES 这2个系统表是在5.6中才有的,5.5是没有的,所以在调用 dict_table_open_on_name 的时候就打不开 SYS_TABLESPACES 表,导致CRASH。

bug修复

这里给一个简单的修复方法,就是在调用 que_eval_sql 删除记录前判断下当前数据是否是5.6的,如果是就传原来的sql,如果不是,就传去掉 SYS_TABLESPACES 和 SYS_DATAFILES 的sql。PXB官方已经确认这个bug #1399471,目前尚未修复,应该会在下个版本修掉。 如果等不及的话,可以有2种选择:

  1. 代码修复,用这里提供的方法;
  2. 版本回退,在恢复的时候,临时用PXB 2.1版本中的 xtrabackup_55 替换 xtrabckup。

bug 影响

从上面的分析可以得到,要触发这个bug,需要这些条件:

  1. PXB版本是2.2以上
  2. MySQL版本是5.5/5.1
  3. 备份的时候有持有临时表的回话。

当满足上面这些条件,恢复数据就会crash。

PXB备份出来的idb数据时间点是不一致的,恢复数据时需要应用redo将数据追到一个一致的点,效果上就相当于 MySQL 异常关闭,buffer pool 有数据没有落盘,然后重启应用redo做崩溃恢复。所以这个场景只有在用PXB才容易出现,对正常使用的mysqld来说一般是不会的,因为我们在升级的时候都是正常关闭 mysqld,这是临时表都被清理干净了,后再用新版mysqld启动,所以新版mysqld启动时就不需要做drop临时表的操作,自然不会crash了。