由于个人能力有限,文中可能存在错误,并且很多细节没有深入分析,欢迎批评指正。
问题描述
某日,接收到生产环境从库复制异常告警信息,数据库版本为 MySQL 5.5.18
。
登陆数据库,查看复制信息如下(截取部分):
...
Relay_Master_Log_File: mysql-bin.000323
Slave_IO_Running: Yes
Slave_SQL_Running:NO
...
Last_Errno: 1317
Last_Error: Query partially completed on the master (error on master: 1317) and was aborted. There is a chance that your master is inconsistent at this point. If you are sure that your master is ok, run this query manually on the slave and then restart the slave with SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; START SLAVE; . Query: 'update robot_heart_record set status_new='0',create_time=now() where robot_id='760b6ba795ef4b28b4a054136e69d510''
...
Relay_Master_Log_Pos: 647994218
...
检查数据库错误日志,错误信息如下:
231226 9:25:22 [ERROR] Slave SQL: Query partially completed on the master (error on master: 1317) and was aborted. There is a chance that your master is inconsistent at this point. If you are sure that your master is ok, run this query manually on the slave and then restart the slave with SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; START SLAVE; . Query: 'update robot_heart_record set status_new='0',create_time=now() where robot_id='760b6ba795ef4b28b4a054136e69d510'',Error_code: 1317
231226 9:25:22 [ERROR] Error running query,slave SQL thread aborted. Fix the problem,and restart the slave SQL thread with "SLAVE START". we stopped at log 'mysql-bin.000323' position 647994218
由上述信息,可以看出从库在回放主库日志到 position 647994218
后,执行中断,错误代码为 1317
。同时根据报错信息 Query partially completed on the master (error on master: 1317) and was aborted
提示,主库执行时中断,但部分执行完成,这里需注意,该现象并不符合事务的原子性。
通过排查发现,robot_heart_record 表存储引擎为 MyISAM
,该引擎并不支持事务。
简化表结构如下:
create table `t1` (
`id` int(11) not null auto_increment,
`uid` int(11) default null,
`status` varchar(20) default null,
`create_time` datetime default null,
primary key (`id`),
key `uid` (`uid`)
) engine=MyISAM default charset=utf8;
另,通过对主库 binlog 日志解析发现,数据库 binlog_format
设置的是 mixed
模式(这里需要注意,在后续模拟中,不同模式下主从复制表现也存在差异)。
问题分析
1.MyISAM 引擎不支持事务,执行中断后,部分操作仍能执行成功;
2.binlog_format 模式为 mixed 时,实际上是 statement 和 row 模式的混合,针对某些操作,只记录执行的 sql (比如复制报错信息中的 update 语句);
3.通过场景模拟,检查不同存储引擎和日志模式下的执行差异。
场景模拟
1.创建测试表 t1、t2,其中 t1 为 MyISAM 表,t2 为 INNODB 表;
# t1 , MyISAM 表
drop table if exists t1;
create table `t1` (
`id` int(11) not null auto_increment,
`uid` int(11) default null,
`status` varchar(20) default null,
`create_time` datetime default null,
primary key (`id`),
key `uid` (`uid`)
) engine=MyISAM default charset=utf8;
# t2 , INNODB 表
drop table if exists t2;
create table `t2` (
`id` int(11) not null auto_increment,
`uid` int(11) default null,
`status` varchar(20) default null,
`create_time` datetime default null,
primary key (`id`),
key `uid` (`uid`)
) engine=innodb default charset=utf8;
2.写入测试数据,这里通过存储过程分别写入;
# t1 表写入数据
drop procedure if exists idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while (i<=100000) do
insert into t1 (uid,status,create_time) values (i, i, now());
set i=i+1;
end while;
end;;
delimiter ;
call idata();
# t2 表写入数据
drop procedure if exists idata2;
delimiter ;;
create procedure idata2()
begin
declare i int;
set i=1;
while (i<=100000) do
insert into t2 (uid,status,create_time) values (i, i, now());
set i=i+1;
end while;
end;;
delimiter ;
call idata2();
3.测试在 mixed 模式下,不同引擎的表在模拟写入中断(ctrl + c)后的差异;
测试 SQL :
# 全表更新
update t1 set status='100001' where uid<='100000';
update t2 set status='100001' where uid<='100000';
结果如下:
测试表名称 | 执行前 | 执行后主库 | 执行后从库 | 主从复制状态 |
---|---|---|---|---|
t1(MyISAM) | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 76432 . | select count(*) from t2 where status=‘100001’; 结果为 0 . | 复制异常,报错 1317,复现问题. |
t2(INNODB) | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 0 . | 复制正常. |
4.测试在 row 模式下,不同引擎的表在模拟写入中断(ctrl + c)后的差异;
测试 SQL :
# 全表更新
update t1 set status='100001' where uid<='100000';
update t2 set status='100001' where uid<='100000';
结果如下:
测试表名称 | 执行前 | 执行后主库 | 执行后从库 | 主从复制状态 |
---|---|---|---|---|
t1(MyISAM) | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 75961 . | select count(*) from t2 where status=‘100001’; 结果为 75961 . | 复制正常. |
t2(INNODB) | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 0 . | select count(*) from t2 where status=‘100001’; 结果为 0 . | 复制正常. |
5.其他测试
测试 SQL :
# 创建测试表 t3 ,与 t1 相同的表结构
drop table if exists t3;
create table `t3` (
`id` int(11) not null auto_increment,
`uid` int(11) default null,
`status` varchar(20) default null,
`create_time` datetime default null,
primary key (`id`),
key `uid` (`uid`)
) engine=MyISAM default charset=utf8;
# 执行 SQL ,模拟取消中断(ctrl + c)执行
insert into t3 select * from t1;
row 模式下,测试结果:
测试表名称 | 执行前 | 执行后主库 | 执行后从库 | 主从复制状态 |
---|---|---|---|---|
t3(MyISAM) | select count(*) from t3; 结果为 0 . | select count(*) from t3; 结果为 0 . | select count(*) from t3; 结果为 86877 . | 复制正常. |
根据官方解释,针对非事务型表的数据复制,可能会出现某些行为导致数据不同步。但这里主库已记录对应行的 insert 日志,从库基于row 模式的日志回放也是正常的,但主库却没有写入成功,这里的行为暂时也没完全理解,还有待继续排查。
When the storage engine type of the replica is nontransactional, transactions on the source that mix updates of transactional and nontransactional tables should be avoided because they can cause inconsistency of the data between the source transactional table and the replica nontransactional table. That is, such transactions can lead to source storage engine-specific behavior with the possible effect of replication going out of synchrony. MySQL does not issue a warning about this currently, so extra care should be taken when replicating transactional tables from the source to nontransactional tables on the replicas.
mixed 模式下,测试结果:
测试表名称 | 执行前 | 执行后主库 | 执行后从库 | 主从复制状态 |
---|---|---|---|---|
t3(MyISAM) | select count(*) from t3; 结果为 0 . | select count(*) from t3; 结果为 0 . | select count(*) from t3; 结果为 0 . | 复制异常,报错 1317. |
解析主库 binlog 记录的信息如下:
BEGIN
/*!*/;
# at 175
#231227 16:10:01 server id 1 end_log_pos 269 Query thread_id=9213763 exec_time=0 error_code=1317
use test/*!*/;
SET TIMESTAMP=1703664601/*!*/;
insert into t3 select * from t1
/*!*/;
# at 269
#231227 16:10:01 server id 1 end_log_pos 338 Query thread_id=9213763 exec_time=0 error_code=0
SET TIMESTAMP=1703664601/*!*/;
COMMIT
测试总结
1.MyISAM 表,当 binlog format 设置为 mixed 时,执行更新操作,取消中断执行,从库复制报错 1317 ,主库更新了部分数据,从库未更新;当 binlog format 设置为 row 时,执行更新操作,取消中断执行,从库复制正常,主库、从库均更新了相同部分的数据;
2.INNODB 表,不管 binlog format 设置为 mixed 或者 row ,执行更新操作,取消中断执行,主从复制均保持正常,主从库均无数据变化。
3.MyISAM 表,当 binlog format 设置为 mixed 时,执行复制表操作,取消中断执行,从库复制报错 1317 ,主库从库均无更新;当 binlog format 设置为 row 时,执行复制表操作,取消中断执行,从库复制正常,但主库无数据写入、从库却写入部分数据,数据不一致。
问题处理
问题中,报错 SQL 为 update robot_heart_record set status_new='0',create_time=now() where robot_id='760b6ba795ef4b28b4a054136e69d510'
,且业务逻辑仅会根据 robot_id 进行查询、更新等操作,在从库执行 stop slave;set global sql_slave_skip_counter=1; start slave;
跳过报错即可。
使用建议
1.非必要,建议使用 INNODB 作为表存储引擎;
2.在主从模式下,建议设置 binlog format 为 row 模式,相对更加安全。
遗留问题
1.MyISAM 表,采用 insert into … select 方式复制数据执行机制探究。