故障分析 | MySQL 主从切换后数据不一致了?

admin 2024-07-24 205 阅读 0评论

1背景

近期生产上出现了个故障,某一套部署了 MySQL 主库的服务器由于硬件问题导致服务器宕机重启。由于该库部署了相关高可用组件,在服务器重启之后,该数据库服务被高可用组件拉起,并和新主库搭建了复制关系。

目前听起来没什么问题,蹊跷的是只复制了一会儿就报错了,复制中断报错信息大致是:Could not execute update_rows event on table xx; Can't find record in xx,Error_Code: 1032; handler error HA_ERR_KEY_NOT_FOUND;... 这到底是怎么回事呢?

2故障排查过程

(1)首先,第一反应是怀疑 主从切换是不是做的强制切换,导致新主上丢数据了?通过相关高可用组件的日志可以看到,切换时主从的 GTID 是一致的,即大致可认为该库在切换时未丢数据。无问题。

(2)再排查一些 slave 相关配置参数,看看是否是参数问题导致主从数据有不一致的潜在风险。无问题。

(3)接下来,在新从库执行 show slave status\G 时,发现新从库上自身比新主库多了一个 GTID。有问题。

(4)通过 mysqlbinlog -vvvv binlogs.xx --include-gtids=xx 解析这个多出的 GTID。发现该事务涉及的表和复制中断报错的表竟然是同一张表!

内容大致如下(本文中所有的表均是个人用于复现用的测试表,而非生产环境的表)。

BEGIN
/*!*/;
# at 349
#240716 12:33:45 server id 1002  end_log_pos 509 CRC32 0xbea1877e       Query   thread_id=9     exec_time=0     error_code=0
use `sky`/*!*/;
SET TIMESTAMP=1721104425/*!*/;
DELETE FROM `sky`.`sky_test` /* generated by server, implicitly emptying in-memory table */
/*!*/;
# at 509
#240716 12:33:45 server id 1002  end_log_pos 584 CRC32 0xd0443172       Query   thread_id=9     exec_time=0     error_code=0
SET TIMESTAMP=1721104425/*!*/;
COMMIT

分析上述内容可以发出以下疑问:

疑问一:明明已经设置 binlog 格式为 row,而且这张表实际上有多行数据,为什么 binlog 只记录了一条 SQL 语句?

疑问二:这个提示信息:/* generated by server, implicitly emptying in-memory table */ 好像和正常的不一样。看到 in-memory table,意识到好像不对劲,猜想这张表不会是内存表吧?

进一步查看该表的表结构。

mysql> show create table sky.sky_test\G
*************************** 1. row ***************************
       Table: sky_test
Create Table: CREATE TABLE `sky_test` (
  `id` int DEFAULT NULL
) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

果不其然,该表为内存(MEMORY[1])表。

因此,可以得出初步的结论:因为该表是内存表,新从库重启后数据库自身做了一个清空内存表的操作;而此时新主库对该表进行了更新操作,在新从库中无法找到相关的行记录,进而中断复制抛出错误。

3问题解决步骤

  1. 在新从库跳过该表的复制,并启动复制进程追平数据。
  2. 由于该表数据量不大,需要和业务确认该表的重要性并协调时间,在该表不发生任何改变时,在新主库导出数据并导入新从库中。然后在新从库取消跳过该表复制的操作。
  3. 后续要求应用将该表改造为 InnoDB 事务表。

实际上,为避免应用随意创建非事务表,可以在创建数据库实例时,通过一些参数设置,在数据库层面规避该问题,比如调整sql_mode和disabled_storage_engines参数。

当然如果非要用内存(MEMORY)表,想要避免这种类似的情况,官网[2] 给出的建议如下:

Note that the replica still has outdated data in a MEMORY table during the interval between the source's restart and its first use of the table. To avoid this interval when a direct query to the replica could return stale data, you can set the init_file system variable to name a file containing statements that populate the MEMORY table on the source at startup.

请注意,在源数据库重启和首次使用表之间的间隔期间,副本中的 MEMORY 表仍然包含过时的数据。为了避免在直接查询副本时可能返回陈旧数据的间隔,您可以设置 init_file 系统变量,以命名一个文件,该文件包含在启动时填充源数据库的 MEMORY 表的语句。

即便如此,还是不建议在 MySQL 中使用非事务表。

4答疑解惑

  1. 针对上面疑问一的解释。

    通过查看官方文档可以知道,表设置为 MEMORY 存储引擎后,在 binlog 中只会以 statement 的方式记录(即使已经设置 binlog 格式为 row )。

  2. delete from memory_table; 什么情况下才会触发呢,是一旦发生重启就会触发吗?

    经测试发现,应是数据库重启后首次访问该内存表时,才会触发清空操作。

5总结

要想保证数据库主从一致性,首先远离非事务表。

参考资料

[1]

memory-engine: https://dev.mysql.com/doc/refman/8.0/en/memory-storage-engine.html

[2]

replication-features-memory: https://dev.mysql.com/doc/refman/8.0/en/replication-features-memory.html


发表评论

快捷回复: 表情:
Addoil Applause Badlaugh Bomb Coffee Fabulous Facepalm Feces Frown Heyha Insidious KeepFighting NoProb PigHead Shocked Sinistersmile Slap Social Sweat Tolaugh Watermelon Witty Wow Yeah Yellowdog
提交
评论列表 (有 0 条评论, 205人围观)