• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

MySQL主库 crash-safe 与 binlog 关系?

MySQL 彭东稳 7年前 (2016-12-07) 26395次浏览 已收录 0个评论

一、什么是CrashSafe?

CrashSafe指MySQL服务器宕机重启后,能够保证:

– 所有已经提交的事务的数据仍然存在。

– 所有没有提交的事务的数据自动回滚。

Innodb通过Redo Log和Undo Log可以保证以上两点。为了保证严格的CrashSafe,必须要在每个事务提交的时候,将Redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个innodb_flush_log_at_trx_commit系统变量,用户可以根据应用的需求自行调整。

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宕机,而操作系统正常工作时,数据的完整性。

当数据crash recovery时,通过redo日志将所有已经在存储引擎内部提交的事务应用redo log恢复,所有已经prepare但是没有commit的事务将会应用undo log做rollback。然后客户端连接时就能看到已经提交的数据存在数据库内,未提交的事务被回滚。

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

二、带Binlog的CrashSafe

当启动Binlog后,事务会产生Binlog Event,这些Event被看做事务数据的一部分。因此要保证事务的Binlog Event和InnoDB引擎中的数据的一致性。所以带Binlog的CrashSafe要求MySQL宕机重启后能够保证:

– 所有已经提交的事务的数据仍然存在。

– 所有没有提交的事务的数据自动回滚。

– 所有已经提交了的事务的Binlog Event也仍然存在。

– 所有没有提交事务没有记录Binlog Event。

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

sync_binlog参数是用来控制MySQL二进制日志刷盘时机,在MySQL 5.7.7之前,sync_binlog的默认值是0,在这种情况下,根据操作系统刷其他文件的机制来刷新二进制日志到磁盘,MySQL不会同步到磁盘中去而是依赖操作系统来刷新binary log。从MySQL 5.7.7版本开始开始,sync_binlog的默认值为1,它是最安全的选择,但会影响性能。

0 – 二进制日志从不进行二进制日志FSYNC(同步)到磁盘上,而是依赖操作系统刷盘机制来刷新二进制日志到磁盘。如果存在主从复制,那么主库dump线程会在flush阶段后进行binlog传输到从库。

1 – 在没有组提交特性之前,每个事务在commit时都必须要FSYNC二进制日志到磁盘;有了组提交特性后,就成了每次组提交时进行FSYNC刷盘。如果存在主从复制,主库dump线程会在sync阶段后进行binlog传输。

N – binlog将在指定次数组提交后进行FSYNC刷盘。如果存在主从复制,主库dump线程会在flush阶段后进行binlog传输。

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

三、MySQL的Two Phase Commit(2PC)

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

– 自动为每个事务分配一个唯一的ID(XID)。

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

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

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

3.1 分布式事务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,全局唯一。

3.2 事务的协调者Binlog

Binlog在2PC中充当了事务的协调者(Transaction Coordinator)。由Binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如下:

MySQL主库 crash-safe 与 binlog 关系?

分解此图,当事务提交时(执行commit语句),分别对应以下几个阶段:

1. 协调者准备阶段(Prepare Phase)

此时SQL已经成功执行,并生成XID信息及redo和undo的内存日志。告诉引擎做Prepare完成第一阶段,Prepare方法实际上什么也没做,将事务状态设为TRX_PREPARED,调用fsync刷新redo log到磁盘(就是把XID写入到磁盘)。

2. 协调者提交阶段(Commit Phase)

2.1 记录协调者日志,即Binlog日志。

如果事务涉及的所有存储引擎的Prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog(write()将binary log内存日志数据写入文件系统缓存,fsync()将binary log文件系统缓存日志数据永久写入磁盘)。此时,事务已经铁定要提交了。否则,调用ha_rollback_trans方法回滚事务,而SQL语句实际上也不会写到binlog。

2.2 告诉引擎做Commit。

最后,调用引擎的Commit完成事务的提交。并且会对事务的undo log从prepare状态设置为提交状态(可清理状态),刷新Commit Log到Redo Log,将事务设为TRX_NOT_STARTED状态。

PS:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。另外需要注意的一点就是,SQL语句产生的Redo日志会一直刷新到磁盘(master thread每秒fsync redo log),而Binlog是事务commit时才刷新到磁盘,如果binlog太大则commit时会慢。

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

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

3.3 协调者日志Xid_log_event

作为协调者,Binlog需要将事务的XID记入日志,供恢复时使用。Xid_log_event有以下几个特点:

– 仅记录query_id

因为前缀部分不变,server_id已经记录在Event Header中,Xid_log_event中只记录query_id部分。

– 标志事务的结束

在Binlog中相当于一个事务的COMMIT语句。

– DDL没有BEGIN,所以也没有Xid_log_event。

– 仅InnoDB的DML会产生Xid_log_event。

因为MyISAM不支持2PC所以不能用Xid_log_event,但会有COMMIT Event。

四、恢复(Recovery)

这个机制是如何保证MySQL的CrashSafe的呢,我们来分析一下。这里我们假设用户设置了sync_binlog & 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回滚这些事务。

2. 恢复过程

从上面的事务状态可以看出:恢复时事务要提交还是回滚,是由Binlog来决定的。

– 事务的Xid_log_event存在,就要提交。

– 事务的Xid_log_event不存在,就要回滚。

恢复的过程非常简单:

– 扫描最后一个Binlog文件(进行rotate binlog文件时,确保老的binlog文件对应的事务已经提交),提取其中的XID。

– 重做检查点以后的redo日志,读取事务的undo段信息,搜集处于prepare阶段的事务链表,将事务的xid与binlog中的xid对比,若存在,则提交,否则就回滚。

疑问1:如果事务的Binlog Event只记录了一部分怎么办?

只有最后一个事务的Event会发生这样的情况。在恢复时,binlog会自动的将这个不完整的事务Events从Binlog文件中给清除掉。

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

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

3. Xid_log_event Checkpoint

这个机制和binlog的文件切换有关,在切换到一个新的Binlog文件前:

– 要等待当前Binlog文件中的所有事务都已经在InnoDB中提交了。

– 告诉InnoDB刷Redo Log到硬件存储。

通过这个机制可以保证在做恢复时,除了最后一个Binlog文件中的事务,其他文件中的事务在InnoDB中一定是已经提交的状态。

五、CrashSafe的写盘次数

前面说到要想保证CrashSafe的前提就要设置sync_binlog & 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()

六、设置”双1″对性能的影响

出于安全考虑,强烈推荐设置”双1″。”双1″会增大每个事务的RT,但得益于MySQL的组提交机制,高并发下”双1″对系统整体tps的影响在可接受范围内。

测试配置为:HDD的8 core虚机,由于测试结果和系统IO能力有很大关系,仅供参考。

sysbench oltp.lua 10张表每张表100w记录(qps/并发数)

MySQL主库 crash-safe 与 binlog 关系?

对更新同一行这样无法有效并行的场景,”双1″对性能的影响非常大。

sysbench update_non_index.lua 1张表1条记录(qps/并发数)

MySQL主库 crash-safe 与 binlog 关系?

对不能有效并行的Slave replay,存在同样的问题。

通过指定tx-rate执行sysbench的update_non_index.lua脚本压测30秒,完成后检查主备延迟。

可以发现在Slave被配置为”双1″的情况下,延迟非常严重,1000以上的QPS就会出现延迟,非”双1″下QPS到5000以上才会出现延迟(主库配置为”双1″)。

sysbench update_non_index.lua 1张表100w条记录 128并发(延迟/qps)

MySQL主库 crash-safe 与 binlog 关系?

七、总结

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

另外,前面已经说了redo与binlog的两阶段提交。 两阶段提交,首先redo log prepare,然后写binlog,最后redo log commit。当在不同阶段宕机时,当我们重启服务进行恢复数据时会看到不同的日志。

  • 如果redo log prepare之后,binlog之前宕机,则回滚事务,日志如下:

  • 如果binlog写入之后宕机,则恢复事务,日志如下:

什么情况下会出现binlog写入了,但是实际这条数据不存在库中?innodb_flush_log_at_trx_commit=0, sync_binlog=1,此时redo log没有刷盘,binlog刷盘了,recover的时候不会根据binlog恢复。所以强烈建议这两个参数都设置成1。

<参考>

https://yq.aliyun.com/articles/88599


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (2)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!