Optimize MyISAM 表导致索引文件损坏

前言

最近线上遇到一个问题,用户对表执行了 Optimize 后,发现数据都不见了,select count(*) xxx 返回值为 0。在了解了用户的场景后,本地复现了下,确实是这样,如下:

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|    10230 |
+----------+
1 row in set (0.00 sec)

mysql> optimize table t1;
+---------+----------+----------+--------------------------------------+
| Table   | Op       | Msg_type | Msg_text                             |
+---------+----------+----------+--------------------------------------+
| test.t1 | optimize | error    | myisam_sort_buffer_size is too small |
| test.t1 | optimize | status   | OK                                   |
+---------+----------+----------+--------------------------------------+
2 rows in set (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

通过观察文件变化,可以快速确认的是索引文件坏掉了。在 Optimize 之前,文件大小和 MD5 如下:

$du -s test/t1.*
12      test/t1.frm
1924    test/t1.MYD
216     test/t1.MYI

$md5sum test/t1.*
b1da352c2ba355f940e26bd57e1a9f48  test/t1.frm
46785a243dec91f4df9601921d6d801d  test/t1.MYD
313d82d2960dae4a0e58594d2f1c97e1  test/t1.MYI

Optimize 之后:

$du -s test/t1.*
12      test/t1.frm
1924    test/t1.MYD
4       test/t1.MYI

$md5sum test/t1.*
b1da352c2ba355f940e26bd57e1a9f48  test/t1.frm
46785a243dec91f4df9601921d6d801d  test/t1.MYD
e739452f3313de04af12e5d38d1b4661  test/t1.MYI

bug 分析

有了复现 case 后,查问题就不难了,整个代码中报 “myisam_sort_buffer_size is too smal” 这个错的只有 2 处,掏出 gdb 全都加上断点。

代码用的是目前最新的 5.6.29 版本,堆栈如下:

#0  _create_index_by_sort() at mysql-server/storage/myisam/sort.c:156
#1  mi_repair_by_sort() at mysql-server/storage/myisam/mi_check.c:2436
#2  ha_myisam::repair() at mysql-server/storage/myisam/ha_myisam.cc:1101
#3  ha_myisam::optimize() at mysql-server/storage/myisam/ha_myisam.cc:1015
#4  handler::ha_optimize() at mysql-server/sql/handler.cc:4270
#5  mysql_admin_table() at mysql-server/sql/sql_admin.cc:648
#6  Sql_cmd_optimize_table::execute() at mysql-server/sql/sql_admin.cc:1126
#7  mysql_execute_command() at mysql-server/sql/sql_parse.cc:4975
#8  mysql_parse() at mysql-server/sql/sql_parse.cc:6385
#9  dispatch_command() at mysql-server/sql/sql_parse.cc:1339
...

Optimize 实际是通过 repair 表来实现的,这里 MySQL 判断需要通过排序的方式来做 repair,但是在 _craete_index_by_sort() 中因为 myisam_sort_buffer_size 设置的 buffer 空间太小,就返回错误了,然后 mi_repair_by_sort 设置 param->retry_repair=1,表示再做一次 repair。重试逻辑在 ha_myisam.cc:1015:

1015  if ((error= repair(thd,param,1)) && param.retry_repair)
1016  {
1017    sql_print_warning("Warning: Optimize table got errno %d on %s.%s, retrying",
1018                      my_errno, param.db_name, param.table_name);
1019    param.testflag&= ~T_REP_BY_SORT;
1020    error= repair(thd,param,1);
1021  }

索引文件损坏就在第二次 repair 过程中,stack 如下:

#0  mi_sort_index () at mysql-server/storage/myisam/mi_check.c:1986
#1  ha_myisam::repair () at mysql-server/storage/myisam/ha_myisam.cc:1125
#2  ha_myisam::optimize () at mysql-server/storage/myisam/ha_myisam.cc:1020
#3  handler::ha_optimize () at mysql-server/sql/handler.cc:4270
#4  mysql_admin_table () at mysql-server/sql/sql_admin.cc:648
#5  Sql_cmd_optimize_table::execute () at mysql-server/sql/sql_admin.cc:1126
#6  mysql_execute_command () at mysql-server/sql/sql_parse.cc:4975
#7  mysql_parse () at mysql-server/sql/sql_parse.cc:6385
#8  dispatch_command () at mysql-server/sql/sql_parse.cc:1339
...

mi_sort_index() 中会重建索引,但是在第一次 repairt 时,会调用 mi_drop_all_indexes() 将所有的删掉,state->key_del[i]= HA_OFFSET_ERROR;,所以 mi_sort_index() 中重建索引时,会跳过所有的 index,导致新生成的索引文件是空的,代码在 mi_check.cc:1986:

1986    if (share->state.key_root[key] != HA_OFFSET_ERROR)
1987    {
1988      index_pos[key]=param->new_file_pos;       /* Write first block here */
1989      if (sort_one_index(param,info,keyinfo,share->state.key_root[key],
1990                         new_file))
1991        goto err;
1992    }

总结

这个 bug 已经提给官方了,见 #80503。在分析 bug 之前,我先 Google 了一把,发现 12 年就有人发现了这个 bug,还是 5.1 版本的,不过有意思的是作为系统的 bug 提给了 Debian 社区,而不是给 MySQL 社区 。

官方对 MyISAM 引擎已经基本放弃冶疗了,所以这个 bug 估计也不太会修,即使修的话也比较慢。

在 bug 修复之前,如果遇到这个问题,可以采用下面的 workaround:

  1. optimize 出现问题后,通过 repair table xxx 先把表索引修好;
  2. 调大 myisam_sort_buffer_size 的值,确保后续的 optimize 不会出问题。

MyISAM 引擎注定是要被时代抛弃的,不安全、不能并发插入(一个例外是 Concurrent Inserts)、表级锁等等,另外 InnoDB 的读性能早就超过了 MyISAM,引用2张官方性能对比的图来看吧:

InnoDB OLTP RO MyISAM OLTP RO

InnoDB 比 MyISAM 快了 3-6 倍!

快点抛弃 MyISAM 吧 ^_^

fungo /
Published under (CC) BY-NC-SA in categories MySQL  tagged with bugs