问题

在Master-Slave均使用TokuDB复制下:

  1. Slave仍在同步数据时异常Crash(例如外界执行kill -9
  2. 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

    TokuDB在Commit前会保证Redo日志写入磁盘

    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重启后,接下来要怎么做?

  1. 因为检查到A Commit,通知B Commit,数据一致
  2. 让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阶段,会调用:

【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相对了Master少了部分数据

解决