前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >诡异宕机,为何分区表会匹配到错误的表名?

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

作者头像
爱可生开源社区
发布2025-03-27 10:26:30
发布2025-03-27 10:26:30
6200
代码可运行
举报
运行总次数:0
代码可运行
作者:马金友, 一名给 MySQL 找 bug 的初级 DBA。

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

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

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

背景描述

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

工单描述:DBA KILL 了一个修改分区表的 SQL 后,MySQL 就宕机了。

error.log 中可知是因为 断言错误 table->get_ref_count() == 0 导致。

代码语言:javascript
代码运行次数: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 调用如下:

代码语言:javascript
代码运行次数:0
运行
复制
#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

代码语言:javascript
代码运行次数:0
运行
复制
/** 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

代码语言:javascript
代码运行次数:0
运行
复制
/** 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,这代表还有线程在使用这个表。

代码语言:javascript
代码运行次数:0
运行
复制
(gdb) p table->n_ref_count._M_i
$1 = 1

InnoDB 也不知道为什么会这样,就只好自杀来处理这样的异常。

检查 table object

通过检查表名可以发现是 test 库下面的 a_1 表。

代码语言:javascript
代码运行次数:0
运行
复制
(gdb) p table->name
$3 = {m_name = 0xfffef40228a0 "test/a_1#p#p0"}

但通过检查这个线程运行的 SQL,可以看到修改的表是 test.a 并不是我们之前看到的 a_1

代码语言:javascript
代码运行次数:0
运行
复制
(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。

代码语言:javascript
代码运行次数:0
运行
复制
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] 中的每一个对象。

代码语言:javascript
代码运行次数:0
运行
复制
 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 是否匹配需要删除的表。

代码语言:javascript
代码运行次数:0
运行
复制
 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);
   }

在条件中。

代码语言:javascript
代码运行次数:0
运行
复制
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 将从字典缓存中移除该表。

代码语言:javascript
代码运行次数:0
运行
复制
(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 中。

复现步骤

创建表。

代码语言:javascript
代码运行次数:0
运行
复制
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 加载到数据字典缓存中。

代码语言:javascript
代码运行次数:0
运行
复制
select count(*) from test.a_1;

在 shell 中终止 ALTER 查询。

代码语言:javascript
代码运行次数:0
运行
复制
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 查询。

代码语言:javascript
代码运行次数:0
运行
复制
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)。

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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#

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爱可生开源社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景描述
  • 断言错误
  • 分析 corefile
    • 检查 table object
    • 寻找 _1 后缀
    • table object 来源
    • 内存布局
  • 复现条件分析
  • 复现步骤
  • Bug reports
  • 修复
  • Patch
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档