前言
最近线上遇到一个问题,用户对表执行了 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:
- optimize 出现问题后,通过
repair table xxx
先把表索引修好; - 调大
myisam_sort_buffer_size
的值,确保后续的 optimize 不会出问题。
MyISAM 引擎注定是要被时代抛弃的,不安全、不能并发插入(一个例外是 Concurrent Inserts)、表级锁等等,另外 InnoDB 的读性能早就超过了 MyISAM,引用2张官方性能对比的图来看吧:
InnoDB 比 MyISAM 快了 3-6 倍!
快点抛弃 MyISAM 吧 ^_^