MySQL的CrashSafe和Binlog的关系

1、什么是CrashSafe

CrashSafe指MySQL服务器宕机重启后,能够保证:
- 所有已经提交的事务的数据仍然存在。
- 所有没有提交的事务的数据自动回滚。
前面的文章讲过,Innodb通过Redo Log和Undo Log可以保证以上两点。为了保证严格的CrashSafe,必须要在每个事务提交的时候,将Redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个系统变量,用户可以根据应用的需求自行调整。

- innodb_flush_log_at_trx_commit
  0 - 每N秒将Redo Log Buffer的记录写入Redo Log文件,并且将文件刷入硬件存储1次。N由innodb_flush_log_at_timeout控制。
  1 - 每个事务提交时,将记录从Redo Log Buffer写入Redo Log文件,并且将文件刷入硬件存储。
  2 - 每个事务提交时,仅将记录从Redo Log Buffer写入Redo Log文件。Redo Log何时刷入硬件存储由操作系统和innodb_flush_log_at_timeout决定。这个选项可以保证在MySQL宕机,而操作系统正常工作时,数据的完整性。

那么CrashSafe和Binlog有什么关系呢?

2、带Binlog的CrashSafe

当启动Binlog后,事务会产生Binlog Event,这些Event被看做事务数据的一部分。因此要保证事务的Binlog Event和InnoDB引擎中的数据的一致性。所以带Binlog的CrashSafe要求MySQL宕机重启后能够保证:
- 所有已经提交的事务的数据仍然存在。
- 所有没有提交的事务的数据自动回滚。
- 所有已经提交了的事务的Binlog Event也仍然存在。
- 所有没有提交事务没有记录Binlog Event。

  这些要求很好理解,如果重启后数据还在,但是Binlog Event没有了,就没办法复制到其他节点上了。如果重启后,数据没了,但是Binlog Event还在,那么不存在的数据就会被复制到其他节点上,从而导致主从的不一致。

为了保证带Binlog的CrashSafe,MySQL内部使用的两阶段提交(Two Phase Commit)。

3、MySQL的Two Phase Commit(2PC)

在开启Binlog后,MySQL内部会自动将普通事务当做一个XA事务来处理:

- 自动为每个事务分配一个唯一的ID

- COMMIT会被自动的分成Prepare和Commit两个阶段。

- Binlog会被当做事务协调者(Transaction Coordinator),Binlog Event会被当做协调者日志。

想了解2PC,可以参考文档:https://en.wikipedia.org/wiki/Two-phase_commit_protocol。

- 分布式事务ID(XID)
使用2PC时,MySQL会自动的为每一个事务分配一个ID,叫XID。XID是唯一的,每个事务的XID都不相同。XID会分别被Binlog和InnoDB记入日志中,供恢复时使用。MySQ内部的XID由三部分组成:
- 前缀部分
  前缀部分是字符串"MySQLXid"
- Server ID部分
  当前MySQL的server_id
- query_id部分
  为了保证XID的的唯一性,数字部分使用了query_id。MySQL内部会自动的为每一个语句分配一个query_id,全局唯一。
参考代码:sql/xa。h的 struct xid_t结构。

- 事务的协调者Binlog
Binlog在2PC中充当了事务的协调者(Transaction Coordinator)。由Binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如下:
1. 协调者准备阶段(Prepare Phase)
    告诉引擎做Prepare,InnoDB更改事务状态,并将Redo Log刷入磁盘。
2. 协调者提交阶段(Commit Phase)
    2.1 记录协调者日志,即Binlog日志。
    2.2  告诉引擎做commit。

注意:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。

在MySQ的代码中将协调者叫做tc_log。在MySQL启动时,tc_log将被初始化为mysql_bin_log对象。参考

sql/mysqld.cc中的init_server_components():
if (opt_bin_log)
   tc_log= &mysql_bin_log;

而在事务提交时,会依次执行:

tc_log->prepare();
tc_log->commit();

参考代码:sql/handler.cc中的ha_commit_trans()。

当mysql_bin_log是tc_log时,prepare和commit的代码在sql/binlog.cc中:

MYSQL_BIN_LOG::prepare()
MYSQL_BIN_LOG::commit()

- 协调者日志Xid_log_event
作为协调者,Binlog需要将事务的XID记入日志,供恢复时使用。Xid_log_event有以下几个特点:
- 仅记录query_id
  因为前缀部分不变,server_id已经记录在Event Header中,Xid_log_event中只记录query_id部分。
- 标志事务的结束  
  在Binlog中相当于一个事务的COMMIT语句。
  一个事务在Binlog中看起来时这样的:  

Query_log_event("BEGIN");
DML产生的events;               
Xid_log_event;                    

- DDL没有BEGIN,也没有Xid_log_event。

- 仅InnoDB的DML会产生Xid_log_event
  因为MyISAM不支持2PC所以不能用Xid_log_event,但会有COMMIT Event。

Query_log_event("BEGIN");
DML产生的events;
Query_log_event("COMMIT");

问题:Query_log_event("COMMIT")和Xid_log_event有不同的影响吗?
- Xid_log_event中的Xid可以帮助master实现CrashSafe。
- Slave的CrashSafe不依赖Xid_log_event

事务在Slave上重做时,会重新产生XID。所以Slave服务器的CrashSafe并不依赖于Xid_log_event。Xid_log_event和Query_log_event("COMMIT"),只是作为事务的结尾,告诉Slave Applier去提交这个事务。因此二者在Slave上的影响是一样的。


4、恢复(Recovery)

这个机制是如何保证MySQL的CrashSafe的呢,我们来分析一下。这里我们假设用户设置了以下参数来保证可靠性:

sync_binlog=1
innodb_flush_log_at_trx_commit=1

- 恢复前事务的状态
在恢复开始前事务有以下几种状态:
- InnoDB中已经提交
  根据前面2PC的过程,可知Binlog中也一定记录了该事务的的Events。所以这种事务是一致的不需要处理。
- InnoDB中是prepared状态,Binlog中有该事务的Events。
  需要通知InnoDB提交这些事务。
- InnoDB中是prepared状态,Binlog中没有该事务的Events。
  因为Binlog还没记录,需要通知InnoDB回滚这些事务。
- Before InnoDB Prepare
  事务可能还没执行完,因此InnoDB中的状态还没有prepare。根据2PC的过程,Binlog中也没有该事务的events。 需要通知InnoDB回滚这些事务。

- 恢复过程
从上面的事务状态可以看出:恢复时事务要提交还是回滚,是由Binlog来决定的。
- 事务的Xid_log_event存在,就要提交。
- 事务的Xid_log_event不存在,就要回滚。

恢复的过程非常简单:
- 从Binlog中读出所有的Xid_log_event
- 告诉InnoDB提交这些XID的事务
- InnoDB回滚其它的事务
疑问1:如果事务的Binlog Event只记录了一部分怎么办?
只有最后一个事务的Event会发生这样的情况。在恢复时,binlog会自动的将这个不完整的事务Events从Binlog文件中给清除掉。

疑问2:随着长时间的运行,Binlog中会积累了很多Xid_log_event,读取所有的Xid_log_event会不会效率很低?

当然很低,所以Binlog中有一个机制来保证恢复时只用读取最后一个Binlog文件中的Xid_log_event。这种机制很像一个简单的Xid_log_event的checkpoint机制。

- Xid_log_event Checkpoint

这个机制和binlog的文件切换有关,在切换到一个新的Binlog文件前:
- 要等待当前Binlog文件中的所有事务都已经在InnoDB中提交了。
- 告诉InnoDB刷Redo Log到硬件存储。
通过这个机制可以保证在做恢复时,除了最后一个Binlog文件中的事务,其他文件中的事务在InnoDB中一定是已经提交的状态。

参考代码:

sql/binlog.cc中:

MYSQL_BIN_LOG::recovery()

MYSQL_BIN_LOG::new_file_impl()

MYSQL_BIN_LOG::inc_prep_xids()

MYSQL_BIN_LOG::dec_prep_xids()

5、CrashSafe的写盘次数

前面说道要想保证CrashSafe就要设置下面两个参数为1:
sync_binlog=1
innodb_flush_log_at_trx_commit=1
下面我们来看看这两个参数的作用。
- sync_binlog
sync_binlog是控制Binlog写盘的,1表示每次都写。由于Binlog使用了组提交(Group Commit)的机制,它代表一组事务提交时必须要将Binlog文件写入硬件存储1次。
- innodb_flush_log_at_trx_commit的写盘次数
这个变量是用来控制InnoDB commit时写盘的方法的。现在commit被分成了两个阶段,到底在哪个阶段写盘,还是两个阶段都要写盘呢?
- Prepare阶段时需要写盘
 2PC要求在Prepare时就要将数据持久化,只有这样,恢复时才能提交已经记录了Xid_log_event的事务。
- Commit阶段时不需要写盘
 如果Commit阶段不写盘,会造成什么结果呢?已经Cmmit了的事务,在恢复时的状态可能是Prepared。由于恢复时,Prepared的事务可以通过Xid_log_event来提交事务,所以在恢复后事务的状态就是正确的。因此在Commit阶段不需要写盘。

总的来说保证MySQL服务的CrashSafe需要写两次盘。在2PC的过程中,InnoDB只在prepare阶段时,写一次盘。Binlog在commit阶段,会设置一个参数告诉InnoDB不要写盘。这个参数是thd->durability_property= HA_IGNORE_DURABILITY;代码在sql/binlog.cc的MYSQL_BIN_LOG::ordered_commit()中。

- Prepare阶段写盘优化

我们知道Binlog使用了Group Commit机制来减少IO,提高性能。Prepare有没有可能做Group Commit呢?只要我们能保证任何事务的Redo Log是在它的Binlog Event写入Binlog文件前,被刷入了持久存储就可以。优化后的做法是:
1. 协调者准备阶段(Prepare Phase)
   设置thd->durability_property告诉InnoDB不写盘。 告诉引擎做Prepare,InnoDB更改事务状态。
2. 协调者提交阶段(Commit Phase)
   2.1.1 获取一组事务。
   2.1.2 通知InnoDB将Redo Log写入硬件存储。
   2.1.3 将这组事务的Binlog Event写入Binlog文件。
   2.2 告诉引擎做commit。

这个结合了Binlog Group Commit机制的改进对性能的提升还是很显著的。而且这个改进是中国的社区用户阿里云的翟卫祥同学提出并提供的代码补丁。详情可参考MySQL的bug页面:http://bugs.mysql.com/bug.php?id=73202。

参考代码:sql/binlog.cc中的MYSQL_BIN_LOG::process_flush_stage_queue()

6、总结

MySQL通过两阶段提交的方式来保证CrashSafe。CrashSafe需要Server层、Binlog和InnoDB的协同工作才能完成。由于DDL和MyISAM不支持事务性,因此没办法保证CrashSafe。

注意:本文的代码都是指MySQL-5.7中的代码,其他版本可能会有不一致。

本文来自云栖社区合作伙伴“DBGEEK”

上一篇:Node.js中常见的异步/等待设计模式


下一篇:sql点滴—mysql中查询表的信息