故障分析 | MySQL 主从切换后数据不一致了?
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问题解决步骤
在新从库跳过该表的复制,并启动复制进程追平数据。 由于该表数据量不大,需要和业务确认该表的重要性并协调时间,在该表不发生任何改变时,在新主库导出数据并导入新从库中。然后在新从库取消跳过该表复制的操作。 后续要求应用将该表改造为 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答疑解惑
针对上面疑问一的解释。
通过查看官方文档可以知道,表设置为 MEMORY 存储引擎后,在 binlog 中只会以 statement 的方式记录(即使已经设置 binlog 格式为 row )。
delete from memory_table;
什么情况下才会触发呢,是一旦发生重启就会触发吗?经测试发现,应是数据库重启后首次访问该内存表时,才会触发清空操作。
5总结
要想保证数据库主从一致性,首先远离非事务表。
参考资料
[1] memory-engine: https://dev.mysql.com/doc/refman/8.0/en/memory-storage-engine.html replication-features-memory: https://dev.mysql.com/doc/refman/8.0/en/replication-features-memory.html
发表评论