作者:马金友, 一名给 MySQL 找 bug 的初级 DBA。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文约 1300 字,预计阅读需要 5 分钟。
背景描述
前段时间在队列中领了一个宕机的工单。
工单描述:DBA KILL 了一个修改分区表的 SQL 后,MySQL 就宕机了。
在 error.log
中可知是因为 断言错误 table->get_ref_count() == 0
导致。
2024-06-17T04:55:36.556026Z 303 [ERROR] [MY-013183] [InnoDB] Assertion failure: dict0dict.cc:1894:table->get_ref_count() == 0 thread 139805159565056
复制
简化后的 backtrace
调用如下:
#0 in raise
#1 in abort
#2 in my_server_abort
#3 in my_abort
#4 in ut_dbg_assertion_failed
#5 in dict_table_remove_from_cache_low
#6 in dict_table_remove_from_cache
#7 in dict_partitioned_table_remove_from_cache
#8 in innobase_dict_cache_reset
#9 in mysql_inplace_alter_table
#10 in mysql_alter_table
#11 in Sql_cmd_alter_table::execute
#12 in mysql_execute_command
#13 in dispatch_sql_command
#14 in dispatch_command
#15 in do_command
#16 in handle_connection
#17 in pfs_spawn_thread
#18 in start_thread
#19 in clone复制
这已经是我第三次看到了类似的错误,前两次和同事们尝试复现这类问题:
建一张分区表 在表上执行 DDL kill DDL 查询
但错误都没有复现。
建议客户去打开 corefile
,但因为没有下文,所以也没分析出原因。欣慰的是,这次的工单中带了 corefile
。
断言错误
我们先来这个断言所在的函数 dict_table_remove_from_cache_low
。
/** Removes a table object from the dictionary cache. */
static void dict_table_remove_from_cache_low(
dict_table_t *table, /*!< in, own: table */
bool lru_evict) /*!< in: true if table being evicted
to make room in the table LRU list */
{
dict_foreign_t *foreign;
dict_index_t *index;
ut_ad(table);
ut_ad(dict_lru_validate());
ut_a(table->get_ref_count() == 0);复制
根据注释,这个函数会从 dictionary-object-cache[1] 中删除一个 table object。删除的时候需要确定这个 object 肯定是没有人在使用的。
分析 corefile
/** Get reference count.
@return current value of n_ref_count */
inline uint64_t dict_table_t::get_ref_count() const { return (n_ref_count); }复制
然而我们可以看到 n_ref_count
不是 0
,这代表还有线程在使用这个表。
(gdb) p table->n_ref_count._M_i
$1 = 1复制
InnoDB 也不知道为什么会这样,就只好自杀来处理这样的异常。
检查 table object
通过检查表名可以发现是 test
库下面的 a_1
表。
(gdb) p table->name
$3 = {m_name = 0xfffef40228a0 "test/a_1#p#p0"}复制
但通过检查这个线程运行的 SQL,可以看到修改的表是 test.a
并不是我们之前看到的 a_1
。
(gdb) p thd->m_query_string
$5 = {
str = 0x7f26c8085030 "ALTER TABLE test.a DROP PARTITION pmax",
length = 38
}复制
寻找 _1
后缀
猜测这个 _1
后缀可能是 InnoDB 内部在用。有点像 binlog index 的 .index_crash_safe
,是为了 recovery。
int MYSQL_BIN_LOG::set_crash_safe_index_file_name(const char *base_file_name) {
...
if (fn_format(crash_safe_index_file_name, base_file_name, mysql_data_home,
".index_crash_safe",
MYF(MY_UNPACK_FILENAME | MY_SAFE_PATH | MY_REPLACE_EXT)) ==
...
}复制
通过在 InnoDB 和 mysql-test 代码中检索这个后缀,并没有发现有意义的结果。
这时我有了个奇怪的想法!
会不会是 InnoDB 错删 table object 了?
table object 来源
通过检查上层帧得知,table object 来自函数 dict_partitioned_table_remove_from_cache
。
该函数扫描 data dictionary cache[2] 中的每一个对象。
size_t name_len = strlen(name);
for (uint32_t i = 0; i < hash_get_n_cells(dict_sys->table_id_hash); ++i) {
dict_table_t *table;
table =
static_cast<dict_table_t *>(HASH_GET_FIRST(dict_sys->table_hash, i));
while (table != nullptr) {
dict_table_t *prev_table = table;
table = static_cast<dict_table_t *>(HASH_GET_NEXT(name_hash, prev_table));
.. // step 2 ......
}
}复制
并检查这个对象的 table_name
是否匹配需要删除的表。
if ((strncmp(name, prev_table->name.m_name, name_len) == 0) &&
dict_table_is_partition(prev_table)) {
btr_drop_ahi_for_table(prev_table);
dict_table_remove_from_cache(prev_table);
}复制
在条件中。
strncmp(name, prev_table->name.m_name, name_len) == 0
复制
仔细看上面的条件可以发现,strncmp 仅比较字符串 name (test/a
) 和 prev_table->name.m_name
(test/a_1#p#p0
) 的前 name_len
(6) 个字节。
如果这些字节匹配并且是分区表,那么 InnoDB 将从字典缓存中移除该表。
(gdb) p name
$7 = 0x7f26ecdf4070 "test/a"
(gdb) p prev_table->name.m_name
$8 =0xfffef40228a0 "test/a_1#p#p0"
(gdb) p name_len
$9 = 6复制
内存布局
在黄色矩形中,test/a
的前 6 个字节与 test/a_1#p#p0
的前 6 个字节匹配。
复现条件分析
在客户描述的基础上,我们还需要:
存在一张分区表:表名和 DDL 的查询前 n 个字符相同。 这张分区表在 dictionary-object-cache
中。
复现步骤
创建表。
create database test;
create table test.a ( x int)
PARTITION BY RANGE (x) (
PARTITION p0 VALUES LESS THAN (10000),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
create table test.a_1 like test.a;复制
将表 test.a_1
加载到数据字典缓存中。
select count(*) from test.a_1;
复制
在 shell 中终止 ALTER 查询。
while true; do { mysql -BNe 'select concat("kill ",id ,";") from information_schema.processlist where state = "committing alter table to storage engine";' | mysql -vvv ; } ; done
复制
在第二个 shell 中执行 ALTER 查询。
while true; do { mysql -BNe "ALTER TABLE test.a ADD PARTITION (PARTITION pmax VALUES LESS THAN MAXVALUE);" ; mysql -BNe " ALTER TABLE test.a DROP PARTITION pmax;" ; }; done
复制
Bug reports
因为这是一个 upstream 分支上的 bug,所以我们在 percona 中创建 report[3] 并且关联到 mysql[4]。
https://bugs.mysql.com/bug.php?id=115352
仅提交者可见
修复
这个 bug 源自 InnoDB 匹配表名问题。为了解决这个问题,我们应该添加更多条件来检查表对象的名称后面是否跟着 #p#
(PART_SEPARATOR
)。
strncmp(
dict_name::PART_SEPARATOR,
prev_table->name.m_name + name_len,
dict_name::PART_SEPARATOR_LEN
) == 0复制
在红色矩形中添加条件后,test/a_1#p#p0
将不会与 test/a
匹配。
Patch
percona-server 8.0.39
index 5c1e6896638..f99114d055e 100644
--- a/storage/innobase/dict/dict0dict.cc
+++ b/storage/innobase/dict/dict0dict.cc
@@ -2006,7 +2006,8 @@ void dict_partitioned_table_remove_from_cache(const char *name) {
}
if ((strncmp(name, prev_table->name.m_name, name_len) == 0) &&
- dict_table_is_partition(prev_table)) {
+ dict_table_is_partition(prev_table) &&
+ (strncmp(dict_name::PART_SEPARATOR, prev_table->name.m_name + name_len, dict_name::PART_SEPARATOR_LEN) == 0)) {
btr_drop_ahi_for_table(prev_table);
dict_table_remove_from_cache(prev_table);
}复制
Oracle 最终在 MySQL 8.0.40[5] 中修复了这个问题,提交记录[6]。
data-dictionary-object-cache: https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-object-cache.html
[2]Dictionary Object Cache: https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-object-cache.html
[3]PS-9264: https://perconadev.atlassian.net/browse/PS-9264
[4]bug#115352: https://bugs.mysql.com/bug.php?id=115352
[5]MySQL 8.0.40: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-40.html
[6]8.0.40 提交记录: https://github.com/mysql/mysql-server/commit/e63c53efe3eecd4c8e487feb475da02e1b13e390
本文关键字:#MySQL# #Percona# #Bug# #InnoDB#
✨ Github:https://github.com/actiontech/sqle
📚 文档:https://actiontech.github.io/sqle-docs/
💻 官网:https://opensource.actionsky.com/sqle/
👥 微信群:请添加小助手加入 ActionOpenSource
🔗 商业支持:https://www.actionsky.com/sqle