暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

诡异宕机,为何分区表会匹配到错误的表名?

作者:马金友, 一名给 MySQL 找 bug 的初级 DBA。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文约 1300 字,预计阅读需要 5 分钟。



本文从一个工单引入,讲述了 MySQL Bug#115352 从故障定位、Bug 提交到修复的全过程。

背景描述

前段时间在队列中领了一个宕机的工单。

工单描述: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

复制

这已经是我第三次看到了类似的错误,前两次和同事们尝试复现这类问题:

  1. 建一张分区表
  2. 在表上执行 DDL
  3. 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)      /*!< intrue 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 个字节匹配。


复现条件分析

在客户描述的基础上,我们还需要:

  1. 存在一张分区表:表名和 DDL 的查询前 n 个字符相同。
  2. 这张分区表在 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 truedo  {  mysql -BNe  'select concat("kill ",id ,";") from information_schema.processlist where state = "committing alter table to storage engine";' | mysql -vvv ; } ; done

复制

在第二个 shell 中执行 ALTER 查询。

while truedo { 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]

参考资料
[1]

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#





故障分析 | MySQL 8.0 中多字段虚拟列引发的宕机
故障分析 | 如何解决由触发器导致 MySQL 内存溢出?
故障分析 | 查询 ps.data_locks 导致 MySQL hang 住
故障分析 | TCP 缓存超负荷导致的 MySQL 连接中断


✨ Github:https://github.com/actiontech/sqle

📚 文档:https://actiontech.github.io/sqle-docs/

💻 官网:https://opensource.actionsky.com/sqle/

👥 微信群:请添加小助手加入 ActionOpenSource

🔗 商业支持:https://www.actionsky.com/sqle


文章转载自爱可生开源社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论