Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >诡异宕机,为何分区表会匹配到错误的表名?

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

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

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

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

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

背景描述

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
运行
AI代码解释
复制
#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
运行
AI代码解释
复制
/** 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
运行
AI代码解释
复制
/** 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
运行
AI代码解释
复制
(gdb) p table->n_ref_count._M_i
$1 = 1

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

检查 table object

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(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
运行
AI代码解释
复制
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
运行
AI代码解释
复制
 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
运行
AI代码解释
复制
 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
运行
AI代码解释
复制
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
运行
AI代码解释
复制
(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
运行
AI代码解释
复制
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
运行
AI代码解释
复制
select count(*) from test.a_1;

在 shell 中终止 ALTER 查询。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
运行
AI代码解释
复制
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
运行
AI代码解释
复制
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
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
GreatSQL删除分区慢的跟踪
某业务系统,每天凌晨会删除分区表的一个分区(按天分区),耗时较久,从最开始的30秒,慢慢变为1分钟+,影响到交易业务的正常进行。 在测试环境进行了模拟,复现了删除分区慢的情况,本次基于GreatSQL8.0.25-17进行测试,官方mysql版本也存在相同问题。
GreatSQL社区
2023/08/11
3140
GreatSQL删除分区慢的跟踪
MySQL-8.0 | 数据字典最强解读
数据字典(Data Dictionary)中存储了诸多数据库的元数据信息如图1所示,包括基本Database, table, index, column, function, trigger, procedure,privilege等;以及与存储引擎相关的元数据,如InnoDB的tablespace, table_id, index_id等。MySQL-8.0在数据字典上进行了诸多优化,本文将对其进行逐一介绍。
数据和云
2019/05/13
4K0
详解MySQL-8.0数据字典
提示:公众号展示代码会自动折行,建议横屏阅读 ---- 1. 引言 ---- 数据字典(Data Dictionary)中存储了诸多数据库的元数据信息如图1所示,包括基本Database, table, index, column, function, trigger, procedure,privilege等;以及与存储引擎相关的元数据,如InnoDB的tablespace, table_id, index_id等。MySQL-8.0在数据字典上进行了诸多优化,本文将对其进行逐一介绍。 图1 2.
腾讯数据库技术
2019/05/16
6.8K0
详解MySQL-8.0数据字典
故障分析 | MySQL 8.0 中多字段虚拟列引发的宕机
在这篇文章中我会分享一个在 MySQL 8.0.35 版本中修复的一个宕机 bug,以及怎样通过错误日志、corefile 和 gdb 发现的这个 bug。
爱可生开源社区
2024/10/29
3500
故障分析 | MySQL 8.0 中多字段虚拟列引发的宕机
MySQL分区表MAXVALUE can only be used in last partition definition错误解决方案
贺春旸的技术博客
2023/09/28
7210
第41期:MySQL 哈希分区表
提到分区表,一般按照范围(range)来对数据拆分居多,以哈希来对数据拆分的场景相来说有一定局限性,不具备标准化。接下来我用几个示例来讲讲 MySQL 哈希分区表的使用场景以及相关改造点。
爱可生开源社区
2022/05/10
1.2K0
深度解读 MySQL 8.0 数据字典重构:源码解析与实践
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
喵手
2024/10/29
1860
深度解读 MySQL 8.0 数据字典重构:源码解析与实践
三十二、分区表
分区表就是按照某种规则将同一张表的数据分段划分到多个存储位置。对数据的分区存储提高了数据库的性能,被分去存储的数据在物理上是多个文件,但在逻辑上仍然是一个表,对表的任何操作都和没有分区之前一样。在执行增删改查等操作时,数据库会自动通过找到对应的分区,然后执行操作。
喵叔
2021/06/29
6120
MySQL分区表姿势
分区的功能不是在存储引擎层实现的。因此不只是InnoDB才支持分区。MyISAM、NDB都支持分区操作。
保持热爱奔赴山海
2019/09/17
5.8K0
MySQL分区表
随着业务的发展,当然现在比较流行的微服务无非就是业务垂直拆分+功能水平拆分,应用加节点是比较简单的,但是每个业务的单库单表扛不住了;数据库分库分表相对来说更复杂一点,但是分区表可以继续支持业务发展两三年,人手有限的情况下,我觉得分布表更合适一点。架构的终极目标是用最小的人力成本来满足就构建维护系统的需求。
只喝牛奶的杀手
2019/09/02
4.7K0
MySQL分区表
MySQL 5.7 分区表性能下降的案例分析
告知MySQL5.7.18的使用者分区表使用中存在的陷阱,避免在该版本上继续踩坑。同时通过对源码的讲解,升级MySQL5.7.18时分区表性能下降的根本原因,向MySQL源码爱好者展示分区表实现中锁的运用。
星哥玩云
2022/08/17
7020
MySQL 5.7 分区表性能下降的案例分析
分区表场景下的 SQL 优化
本文讲述了一位DBA在数据库调优过程中,对分区表进行SQL优化,通过修改表分区、增加索引、调整查询语句等手段,有效提高了查询效率。同时,提醒大家在遇到SQL性能瓶颈问题时,需要先提供足够的信息,以便更好地进行优化。
叶金荣
2017/07/04
9810
mysql 获取分区的最大值_MySQL分区表测试「建议收藏」
mysql> Create table engine1(id int) engine=innodb partition by range(id)(partition po values less than(10));
全栈程序员站长
2022/09/05
3.1K0
Innodb系统表-结构解析
数据文件和系统文件都是由多个数据页组成,每个数据页16K(默认),每个数据页都有不同的作用,有以下几种类型(storage/innobase/include/fil0fil.h):
donghy
2022/09/17
5380
(3) MySQL分区表使用方法
使用起来和不分区是一样的,看起来只有一个数据库,其实有多个分区文件,比如我们要插入一条数据,不需要指定分区,MySQL会自动帮我们处理
用户1214487
2022/03/26
1.3K0
(3) MySQL分区表使用方法
MySQL分区表
在最近的项目中,我们需要保存大量的数据,而且这些数据是有有效期的,为了提供查询效率以及快速删除过期数据,我们选择了MySQL的分区机制。把数据按照时间进行分区。 分区类型 ---- Range分区:最为常用,基于属于一个给定连续区间的列值,把多行分配给分区。最常见的是基于时间字段. 基于分区的列最好是整型,如果日期型的可以使用函数转换为整型。 List分区:LIST分区和RANGE分区类似,区别在于LIST是枚举值列表的集合,RANGE是连续的区间值的集合。 Hash分区:基于给定的分区个数,将
十毛
2019/04/01
5.2K0
MySQL分区表
深度解析auto-increment自增列&quot;Duliplicate key&quot;问题
提示:公众号展示代码会自动折行,建议横屏阅读 问题描述 近期,线上有个重要Mysql客户的表在从5.6升级到5.7后master上插入过程中出现"Duplicate key"的错误,而且是在主备及RO实例上都出现。以其中一个表为例,迁移前通过“show create table” 命令查看的auto increment id为1758609, 迁移后变成了1758598,实际对迁移生成的新表的自增列用max求最大值为1758609。用户采用的是Innodb引擎,而且据运维同学介绍,之前碰到过类似问题,重启
腾讯数据库技术
2018/12/03
2.3K0
深度解析auto-increment自增列&quot;Duliplicate key&quot;问题
MySQL普通表转换为分区表实战指南
本文将详细指导新手开发者如何将MySQL中的普通表转换为分区表。分区表在处理庞大数据集时展现出显著的性能优势,不仅能大幅提升查询速度,还能有效简化数据维护工作。通过掌握这一技巧能够更好地应对数据密集型应用带来的挑战,为系统的高效运行奠定坚实基础。
公众号:码到三十五
2024/06/12
4900
MySQL普通表转换为分区表实战指南
经验分享|MySQL分区实战(RANGE)
在 MySQL 中, InnoDB存储引擎长期以来一直支持表空间的概念。在 MySQL 8.0 中,同一个分区表的所有分区必须使用相同的存储引擎。但是,也可以为同一 MySQL 服务器甚至同一数据库中的不同分区表使用不同的存储引擎。
六月暴雪飞梨花
2023/11/30
6220
经验分享|MySQL分区实战(RANGE)
你真的了解MySQL 8.0 数据字典吗?
作者:叶盛,腾讯云数据库TDSQL开发工程师,从事数据库内核开发工作。 在MySQL中,数据字典信息内容包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。可是包含这些元数据的数据字典不仅仅存在于数据库系统表中(information_schema,mysql,sys),还存在于server层和InnoDB存储引擎中的部分文件里,比如每个表都有一个对应的.frm文件来保存表结构的信息,.opt文件用来用来记录每个库的字符等信息,.TRN和.TRG文件用来存放触发器的
腾讯云数据库 TencentDB
2020/09/04
1.3K0
相关推荐
GreatSQL删除分区慢的跟踪
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档