问题
在Master-Slave均使用TokuDB复制下:
- Slave仍在同步数据时异常Crash(例如外界执行kill -9
) - Slave重启后执行START SLAVE,会有丢数据的可能性
【例】
- Slave上的表test ,CREATE TABLE test (c varchar(20), PRIMARY KEY(c))
- kill -9
- 假设Slave此时在执行INSERT INTO t VALUES (“test_data”)
- Slave重启后,START SLAVE
- 停止Master的写入
- 等待Slave跟Master完成同步
有可能性:Slave上的test中没有“test_data”
背景知识
一些配置
tokudb_commit_sync
relay_log_info_repository = TABLE & relay_log_recovery = ON
在Slave上回放一个事务时,使得以下两个操作具有原子性
对事务的执行
- Master_log_name/Master_log_pos的更新
log_slave_updates
开启这个参数后,从库会将SQL线程执行的事务同时写入到自身的Binlog里
分析
两阶段提交时,Coordinator Crash
Coordinator / Storage Engine A / Storage Engine B:
- A Prepare
- B Prepare
- A Commit
- Coordinator Crash …
那么当Coordinator重启后,接下来要怎么做?
- 因为检查到A Commit,通知B Commit,数据一致
- 让B自行处理(B会回滚),数据不一致
接下来看MySQL Server如何处理这个场景
MySQL基于Binlog的Crash Recovery过程
很重要的一点是,MySQL基于Binlog的Crash Recovery的过程,会使用Binlog辅助引擎完成处于Prepare阶段(还未Commit)的事务
mysqld进程 (MySQL Server层,Coordinator角色) / Binlog / InnoDB / TokuDB
【Prepare】
- Binlog Prepare
- TokuDB Prepare
- InnoDB Prepare
【Commit】
Commit过程的核心函数是ordered_commit,分为三个阶段:Flush Stage / Sync Stage / Commit Stage(详细参考MySQL 5.6 Binlog Group Commit),也就是说一定是Binlog先Commit,然后是各个存储引擎的提交
- Binlog Commit (Flush Stage和Sync Stage)
- InnoDB Commit
- mysqld Crash …
- msyqld 重启
这里说一下MySQL Server重启后的Crash Recovery,可以很清晰的看到MySQL Server是如何使用Binlog来辅助各个存储引擎来进行Crash Recovery
Slave Crash Safe
当开启了relay_log_info_repository = TABLE & relay_log_recovery = ON
Slave在执行Relay Log时,会使用两阶段提交来保证如下两个操作的原子性:
- 对事务的执行
- Master_log_name/Master_log_pos的更新
相应的代码在 Xid_log_event::do_apply_event 中
rli_ptr->flush_info最终会调用相应存储引擎的update_row来对mysql.slave_relay_log_info表进行更新
默认情况下mysql.slave_relay_log_info使用InnoDB,那么Slave上一个事务的执行流程为(以INSERT为例):
【例1】
- 【1】BEGIN
- 【2】INSERT INTO
VALUES … - 【3】ha_innobase::update_row(更新mysql.slave_relay_log_info,COMMIT之前)
- 【4】COMMIT(Xid_log_event)
一个事务里调用的引擎都需要通过函数register_ha来注册到Server层,形成一个链表(ha_list)
因为链表采用类似头部插入的方式,所以两阶段提交的遍历顺序和引擎的注册顺序相反【例1】中形成的链表为InnoDB=>TokuDB
Commit阶段,会调用:
- Xid_log_event::do_commit
- trans_commit
- ha_commit_trans:两阶段提交的执行者,参考Transaction(一):2 Phase Commit
- trans_commit
【ha_commit_trans】
这里省略了跟文章无关的很多细节
MYSQL_BIN_LOG::prepare => ha_prepare_low
ha_prepare_low会遍历所有在这个事务中已注册到Server层的存储引擎,逐个调用Prepare:
- innobase_xa_prepare
- tokudb_xa_prepare
MYSQL_BIN_LOG::commit => ha_commit_low(如果开启了Binlog,会调用ordered_commit函数)
ha_commit_low会遍历所有在这个事务中注册到Server层的存储引擎,逐个调用Commit:
- innobase_commit
- tokudb_commit
那么当如下场景发生时:
- innobase_xa_prepare
- tokudb_xa_prepare
- innobase_commit(Master_log_name/Master_log_pos已经更新,因为Commit了,所以无法回滚)
- mysqld Crash …
MySQL Server重启后,因为没有了Binlog的辅助Recovery,TokuDB会将已经Prepare的事务回滚(如果有Binlog,Binlog查找到这个事务的Xid,通知TokuDB调用commit_by_xid来提交这个事务)
此时
- Master_log_name/Master_log_pos已经更新
- 但TokuDB上该事物被回滚
- Slave重启后也不会继续执行这个事务(Master_log_name/Master_log_pos已经更新到这个事务之后的位置)
解决
- 将slave_relay_log_info改为TokuDB表(ALTER TABLE),回避两阶段提交
- 开启Slave的Binlog(log_slave_updates = 1),使用Binlog来辅助存储引擎的Crash Recovery,数据一致
参考
【1】Enabling crash-safe slaves with MySQL 5.6
【2】MySQL Crash-Safe 复制
【3】mysql源码学习笔记:基于binlog的recovery机制
【4】MySQL · 功能分析 · 5.6 并行复制实现分析