15

MYSQL binlog优化几点思考

 3 years ago
source link: https://zhuanlan.zhihu.com/p/147459036
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MYSQL binlog优化几点思考

工程师,期望当一个坐台歌手,个人主页:www.d-kai.me

问题

问题1:如何解决事务提交时flush redo log带来的性能损失

WAL是实现事务持久性(D)的一个常用技术,基本原理是将事务的修改记录redo log。redo log顺序追加写入。事务提交时,只需要保证事务的redo log落盘即可,通过redo log的顺序写代替页面的随机写提升数据库系统的性能。但是,该方案必须要求每个事务提交时都将其生成的redo log进行一次刷盘,效率不高。

问题2:binlog和引擎层事务提交的顺序问题

对于单个事务而言,日志写入顺序是先redo log再binlog,只要维持该顺序即可维持正确性。但对于一个高并发的数据库系统而言,每时每刻可能都会存在众多并发执行的事务。我们还需要通过一定的手段来维护Server层binlog和引擎层事务提交的顺序一致性。

维护这种顺序一致性其实是为了保证备份工具Xtrabackup的正确性。

当 binlog 作为协调者,如果其中记录的事务顺序和存储引擎层记录的顺序不一样的话,备份工具(Innodb Hot Backup)拿到备份集的位点可能会存在空洞。因为备份工具会拷贝 redo 日志,在 redo 的头部会记录最后一个提交的事务对应的 binlog 位点,备份集建立之后就会根据这个位点继续从主库 dump binlog。

假如有三个事务 T1,T2,T3 已经 fsync 到 binlog 文件中,三个事务的在文件中的位点分别是 100,200,300,但是在引擎层的只有 T1 和 T3 完成了 commit 并记录到 redo 中,最后一个 commit 的事务 T3 位点是 300。此时通过备份工具拿到的数据就是这样的状态,备份集启动的时候会走崩溃恢复的流程,prepare 事务被回滚(备份集不会备份 binlog 文件,对应上个小节 xid 集合为空),自位点 300 继续从主库同步binlog并apply,导致 T2 在备库就丢失了。

因此,我们必须设计一种机制来保证Server层的binlog写入顺序和存储引擎层的事务提交顺序保持一致。

问题3:同时写redo和binlog带来的性能下降

问题1中提到每次的事务提交会带来性能问题,而这个问题在引入binlog后会变得更加严重。每个事务提交都会增加一次文件IO,且需要刷盘。如果系统并发比较高,那么这些IO将会成为拖慢整体性能的瓶颈。

解决方案

问题1:Redo log组提交技术

redo组提交技术思想很简单:通过将多个事务redo log的刷盘动作合并,减少刷盘次数。Innodb的日志系统里面,每条redo log都有一个LSN(Log Sequence Number)。事务将日志拷贝到redo log buffer时,都会获取当前最大的LSN,且LSN单调递增,因此可以保证不同事务的LSN不会重复。那么假设三个事务Trx1、Trx2、Trx3的日志的最大LSN分别为LSN1、LSN2、LSN3(LSN1 < LSN2 < LSN3),它们同时进行提交,那么如果trx3率先执行提交,它会要求刷盘至LSN3处,这样就顺便将Trx1、Trx2的redo log也刷了,Trx1和Trx2会判断自己的LSN小于当前已落盘的最大LSN,就无需再次刷盘。

问题2:内部XA事务

开启binlog情况下,引入内部XA事务来协调上层和存储引擎层,具体来说,在事务提交时引入两个阶段:

prepare:将redo log刷盘操作以确保data页和undo页的更新已经刷新到磁盘,设置事务状态为PREPARE状态;

commit:1). 写binlog并刷盘,2).调用引擎层事务提交接口。将事务状态设置为COMMIT。

如此两阶段提交主要是要保证数据库崩溃时的正确性。因为一旦binlog落盘了,它就可能被下游节点消费。这种事务必须在重启后被commit而非rollback。而对于binlog未落盘的事务,崩溃恢复时直接回滚。

具体来说,故障恢复时,扫描最后一个binlog文件(在flush阶段,如果binlog大小超过阀值,进行rotate binlog文件,会保证该文件记录的最后一个事务一定被提交),提取其中的xid。重做检查点以后的redo日志,读取事务的undo段信息,搜集处于prepare阶段的事务列表,将事务的xid与binlog中记录的xid对比,若存在,则提交,否则就回滚。

MySQL5.6以前,为了保证数据库binlog的写入顺序和InnoDB层的事务提交顺序一致,MySQL数据库内部使用了prepare_commit_mutex锁。

具体来说,在两阶段提交引擎层 prepare 的时候加锁,在引擎层 commit 之后释放锁:

innobase_xa_prepare()
write() and fsync() binary log
innobase_commit()

这样确实可以保证 binlog 和 innodb 的事务顺序一致,但是这把锁会导致所有的事务串行化执行,且每次提交都会至少调用多次fsync,效率很低。这也是接下来需要探讨并解决的一个问题。

问题4

参考redo log优化技术,引入组提交技术来优化binlog的写入性能。

考虑未优化时事务提交流程:

  1. prepare:该阶段刷存储引擎层(innodb)的redo log并将事务状态设置为PREPARED(更新undo page上事务状态),该阶段不涉及binlog
  2. commit:写binlog日志并刷盘,同时引擎层释放锁,释放回滚段、设置事务状态为COMMITTED等

所谓的组提交技术其本质上是将耗时的commit步骤进行更细粒度的拆分,具体来说:

将步骤2的commit 分为三个阶段:

Flush:写binlog,但不sync
Sync: 调用 fsync 操作将文件落盘
Commit :调用存储引擎接口提交事务

这里的fsync是耗时操作,因此我们希望能攒足够多的写入后才进行一次fsync调用,在这里使用batch技术。其原理是:上述步骤中的每个阶段都有一个对应的任务链表,每个进入该阶段的线程会将自己的任务加入至该链表中,链表加锁以保证正确性。第一个加入该链表的线程会成为Leader,后续的线程成为Follower。链表中的所有任务组成一个Batch,由Leader负责执行,而Follower则等待其任务完成即可。

一旦某阶段的链表任务执行完成,这些任务会进入下一个阶段,同样加入该阶段的任务链表,重复上述执行流。

如此设计有以下几点好处:

  1. 使用Leader执行而非每个线程各自执行可有效减少write/fsync等调用次数,提高效率
  2. 可保证事务写binlog和引擎层提交的顺序一致
  3. 多事务可并发执行,而不再需要被prepare_commit_mutex锁强制串行化

除此之外,MYSQL还对prepare阶段刷redo log进行了进一步优化。原来的设计是多事务可并发地刷redo log,同样效率不够高。可以将prepare阶段的redo log刷盘放在commit阶段的Flush阶段执行。但有个小问题需要说明的是:优化前每个线程各自负责自己的redo log的落盘,且知道需要flush的redo log的lsn,如果改为在Flush阶段由其Leader线程统一落盘,此时它不了解每个线程的redo log的lsn,因此它简单粗暴地flush至log_sys的最大lsn,这就保证了要提交事务的redo log一定可以被落盘。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK