Skip to content

35 MySQL半同步能提高主备数据的一致性吗?

你好,我是俊达。

上一讲中,我留了一个问题,就是在默认的情况下,MySQL的数据复制是异步的,当主库意外崩溃后,备库是否有可能比主库多执行一些事务?

回答这个问题,需要对主库事务提交的过程,以及数据复制的细节有更深入的了解。这一讲我们就来看一看事务的提交过程,分析Binlog是什么时候发送给备库的,还有Semi-Sync插件在中间起到的作用。这里我假定只使用了InnoDB存储引擎,开启了GTID,并且使用了ROW格式Binlog,这也是推荐的使用方式。

MySQL数据复制回顾

下面这张数据复制的架构图,你应该在上一讲中就看到过了。

图片

我们回顾一下。

  1. 用户线程在执行事务的过程中不断产生Binlog事件。Binlog事件的内容和事务中执行的语句相关,同时也受参数binlog_format影响。产生的Binlog事件会先缓存到Binlog Cache中。每一个用户线程都有各自独立的Binlog Cache。

  2. 提交事务时,用户线程将Binlog Cache中的数据写入到Binlog文件。一个事务会产生多个Binlog事件,这些事件在Binlog文件中是连续存放的。一个线程在往Binlog文件中写入数据时,其他线程要等待之前的线程写完数据,才能写入Binlog文件。

  3. 用户线程将Binlog事件写入到文件或将Binlog文件同步到磁盘后,通知Dump线程有新的binlog事件产生,通知的具体时机跟参数sync_binlog的设置有关系。

  4. 对每一个备库上的IO线程,主库上都会创建一个Dump线程。Dump线程负责将已提交事务的Binlog事件发送给IO线程。

  5. IO线程接收到Binlog事件后,将事件写入RelayLog文件。

  6. SQL线程从RelayLog文件中解析出Binlog事件,并进行应用。启用了多线程复制时,SQL线程也称为协调线程。备库上参数replica_parallel_workers设置为大于1的值,就能开启多线程复制。参数replica_parallel_type用来控制并行复制的方式,建议设置为LOGICAL_CLOCK。

  7. 如果启用了多线程复制,SQL线程会将Binlog事件分发给各个Worker线程。

  8. SQL线程或Worker线程执行Binlog事件。

如果主库完成了事务提交,但是Dump线程由于各种原因没有及时将Binlog发送出去(可能是主备之间的网络有抖动),这个时候如果主库崩溃了,切换到了备库,备库就比主库少执行了一些事务。

如果主库提交事务时,Binlog在完成持久化之前(可能是IO hang),Dump线程已经将Binlog发送给了备库,这个时候如果主库崩溃了,切换到了备库,备库就会比主库多执行一些事务。

这两种情况是不是有可能发生呢?我们先来看一下主库上事务提交的过程。

主库事务提交过程

Binlog Cache

用户线程执行过程中生成的Binlog事件会先缓存到Binlog Cache。Binlog Cache内部分配了一块内存空间,大小由参数binlog_cache_size控制。Binlog事件先写入到内存空间中,如果事务生成的Binlog超过了binlog_cache_size,就会将数据写到临时文件中。临时文件的大小由参数max_binlog_cache_size控制。如果事务生成的Binlog超过了参数max_binlog_cache_size的限制,事务会失败。

图片

二阶段提交

如果使用InnoDB存储引擎,并且开启了Binlog,为了保障InnoDB和Binlog中数据的一致性,MySQL会使用两阶段提交协议来提交事务。

图片

如上图所示,MySQL事务的提交分为Prepare和Commit两个阶段。在Prepare阶段,依次进行Binlog Prepare和InnoDB Prepare,只有当Binlog和InnoDB都Prepare成功后,事务才能进行后续的提交步骤。

  1. Binlog Prepare:Binlog Prepare时会计算事务的commit_parent。使用多线程复制时,会根据事务的commit_parent来判断该事务是否可以并行执行。

  2. InnoDB Prepare:31讲中说过,InnoDB会给每个事务分配Undo段。InnoDB Prepare时,需要将Undo段的状态设置为TRX_UNDO_PREPARED,并且在Undo段中记录事务的XID。MySQL中每个事务都有各自的XID,在一个实例的生命周期中(也就是实例没有重启),XID是唯一的。

InnoDB Prepare完成后,需要刷新REDO日志,这样才能保证TRX_UNDO_PREPARED状态不会丢失。如果数据库在事务完成Prepare后崩溃了,那么在下次启动时,会读取到状态为TRX_UNDO_PREPARED的事务,但是由于事务的Binlog还没来得及持久化,因此也无法在Binlog中找到这些事务的XID事件,MySQL会回滚这些事务。

Prepare成功后,进入到Commit阶段。在Commit阶段依次执行下面这几个步骤。

  1. 生成XID事件,并将XID写入到Binlog Cache中。

  2. Flush Binlog:将Binlog Cache中的数据写入到Binlog文件中。如果开启了GTID,这时会先生成GTID事件,并将GTID事件先写入到Binlog文件。如果参数sync_binlog没有设置为1,执行完Binlog Flush后,通知Dump线程读取Binlog。

  3. 持久化Binlog。这一步是否真正进行Binlog持久化还依赖参数sync_binlog的设置。如果sync_binlog设置为1,则在这一步将binlog持久化到磁盘中,并通知Dump线程读取Binlog。Binlog持久化之后,事务事实上就已经完成提交了,只不过这时其他会话暂时还无法读取到当前事务所做的修改。如果这一步完成后MySQL服务器崩溃了,在实例重启时,会在Undo段中读取到状态为TRX_UNDO_PREPARED的事务。因为Binlog已经完成了持久化,所以会在Binlog中读取到事务的XID事件,因此会将事务状态设置为已经提交。

  4. InnoDB Commit:在InnoDB存储引擎层执行Commit操作,包括释放Undo段,将事务从活动事务列表中移除,持久化Redo日志等。这一步完成后,实例中的其他会话就能读取到当前事务所做的修改了。

所以,如果参数sync_binlog没有设置为1时,理论上存在备库比主库执行更多事务的可能性。

组提交

接下来我们来看一下组提交。MySQL使用了组提交(Group Commit)技术来提交事务。并发提交事务的会话会先进入到一个队列,最先进入队列的线程称为Leader,负责提交操作,其它线程等待Leader线程完成提交操作。提交的过程分为Flush Binlog、Sync Binlog、Commit三个阶段,每个阶段都有相应的队列。

Binlog Flush阶段

线程完成Prepare后,调用MYSQL_BIN_LOG::commit函数,开始进行提交阶段的工作。

下面图中,框框里是MySQL源码中的函数名,你可以到源码中查看具体的代码实现。

图片

Binlog Flush的执行过程大致如下:

  1. 提交事务的线程进入Flush队列。第一个进入队列的线程成为Leader往下执行。其他线程成为follower,等待Leader提交完成后再进行后续处理。

  2. Leader线程执行Flush操作,主要包括下面这些操作。

  3. 将队列中的线程提取出来,同时清空队列,以便后续提交事务的线程可以加入队列。

  4. 持久化REDO日志。REDO日志持久化之后,即使实例发生崩溃重启,事务的Prepared状态也不会丢失。Leader线程持久化REDO日志时,其他线程的REDO日志也自动完成了持久化。

  5. 依次将队列中所有线程的Binlog Cache都刷到Binlog文件中。

  6. 如果开启了GTID_MODE,则给事务分配一个GTID。

  7. 给事务分配last_committed、sequence_number,生成GTID事件,并将GTID事件写入到binlog文件中。注意这里GTID事件需要直接写入Binlog文件,而不是加入Binlog Cache,因为GTID事件要放在事务的其他事件之前。last_committed值的计算方式跟参数binlog_transaction_dependency_tracking的设置相关。

  8. 将binlog cache中的事件写入到binlog文件中。这里只是将事件写到Binlog文件,还没有进行fsync操作。

  9. 如果sync_binlog != 1,唤醒Dump线程发送binlog。

Binlog Sync 阶段

Binlog Flush阶段的Leader线程完成Binlog Flush后,进入到Binlog Sync阶段。

图片

Binlog Sync阶段依次执行下面这些步骤。

  1. 线程先加入sync队列。第一个加入队列的成为leader,执行sync操作。其他线程成为follower,等待leader线程完成提交操作后再执行。

  2. 如果设置了参数binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,等待一定的时间,让更多的线程加入到group中。

  3. 根据sync_binlog设置来决定是否将binlog刷新到磁盘。如果sync_binlog设置为1,则每次提交时都需要将binlog文件刷新(fsync)到磁盘。如果sync_binlog为0,就不刷新binlog文件。如果sync_binlog的设置值N大于1,那么就每N次执行一次刷新。Binlog文件刷新到磁盘后,事务实际上就已经完成了持久化,但是事务的修改对其它会话暂时还不可见。如果此时数据库发生崩溃重启,从UNDO段中可以获取到处于Prepared状态的事务。因为事务的XID事件已经持久化到了Binlog文件中,MySQL会将事务状态设置为已经提交。

  4. 如果sync_binlog为1,唤醒dump线程发送binlog

Commit阶段

Binlog Sync阶段的Leader线程完成相关处理后,进入到Commit阶段。

图片

  1. 如果参数binlog_order_commits设置为ON,则sync阶段的leader线程加入commit队列。第一个加入队列的线程成为leader,进行后续的提交操作。其他线程作为follower,等待leader完成提交后再继续处理。如果binlog_order_commits设置为OFF,则线程不需要进入commit队列,在调用after_sync钩子函数后,然后直接进入到第5步唤醒其它等待提交的事务,由各个事务各自完成提交动作。

  2. 如果启用了semi-sync,并且rpl_semi_sync_master_wait_point设置为after_sync,则在这里等待binlog同步到备库。

  3. 依次执行以下几个步骤。

  4. 更新max_committed变量。下一个group进行提交的时候,会根据变量max_committed的值来设置last_committed值。

  5. 在InnoDB存储引擎内提交事务。InnoDB引擎内部的提交需要进行一系列操作。包括:

  6. 设置事务回滚段的状态。

  7. 如果开启了GTID_MODE,在回滚段中记录事务的GTID。

  8. 将事务从活动事务列表中移除。这一步完成之后,其它会话就能看到这个事务修改的数据了。

  9. 更新gtid相关变量:gtid_executed、gtid_purged,更新gtid_executed表。

  10. 如果启用了semi-sync,并且rpl_semi_sync_master_wait_point设置为after_commit,则在这里等待binlog同步到备库。

  11. 唤醒其他等待提交的线程(signal_done)。

  12. 执行finish_commit,完成commit。

Binlog同步

最后,我们来看一下Binlog是怎么从主库同步到备库的。同步操作由主库的Dump线程和备库的IO线程一起配合完成。

图片

IO 线程

备库上使用START SLAVE或START REPLICA命令启动复制。当参数skip_slave_start、skip_replica_start为OFF时,实例启动时也会自动启动复制。

  1. IO线程基于master_info中的信息,和主库建立连接。如果建立连接失败,还会进行重连。master_info以文件或表的形式存储。我建议你把参数master_info_repository设置为TABLE,将master_info存储在表中。

  2. 和主库建立连接后,获取主库的一些基础信息,包括下面这些信息。

  3. 主库的UUID;

  4. 主库的server id、GTID_MODE;

  5. 获取主库的当前时间,计算主库和备库之间的时间差clock_diff_with_master,在计算备库延迟时需要用到这个信息。

  6. 发送Dump Binlog命令。

  7. 如果启用了GTID并且使用了GTID AUTO Position,就发送COM_BINLOG_DUMP_GTID命令,命令的参数中包括备库的GTID_EXECUTED。

  8. 如果没有使用AUTO Position,就发送COM_BINLOG_DUMP命令,命令参数中包括需要读取的Binlog文件名和位点。

  9. 接收主库Dump线程发送过来的Binlog事件。

  10. 将Binlog事件保存到RelayLog文件中,并更新相关binlog位点信息,根据参数设置持久化relaylog文件和master info。

  11. 更新备库读取Binlog的位点信息(Show slave status输出中的Master_Log_File和Read_Master_Log_Pos)。如果开启GTID,还要更新Retrieved_Gtid_Set。

  12. 根据参数sync_relay_log的设置决定是否持久化Relay Log文件。

  13. 根据参数sync_master_info的设置决定是否持久化master info。

DUMP线程

IO线程启动时,会跟主库建立连接,主库上会启动一个Dump线程。Dump线程主要执行下面这几个步骤。

  1. 处理备库上IO线程发送过来的读取Binlog的命令。如果备库启用了AUTO_POSITION,则命令中还包括了备库的GTID_EXECUTED信息。如果没有使用AUTO_POSITION,则命令中包括了Binlog的起始文件名和位点。

  2. 初始化Binlog读取工作。如果备库使用了AUTO_POSITION,则Dump线程需要根据发送过来的GTID_SET_ARG信息来计算Binlog的起始文件名和位点。这里可能会遇到几种情况。

  3. 如果主库没有开启GTID,就直接返回错误消息:The replication sender thread cannot start in AUTO_POSITION mode。

  4. 如果备库发送过来的GTID_SET_ARG中包含主库上不存在的GTID(相对于主库的UUID),就返回错误消息:Got fatal error 1236 from master when reading data from binary log: 'Slave has more GTIDs than the master has, using the master’s SERVER_UUID.

  5. 如果主库的GTID_PURGED不是备库发送过来的GTID_SET_ARG的子集,就说明备库上需要的一些事务已经被清理了,报错:Got fatal error 1236 from master when reading data from binary log: 'Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup.

一般在正常情况下,备库发送过来的GTID_SET_ARG应该是主库GTID_EXECUTED的子集,而主库的GTID_PURGED应该是GTID_SET_ARG的子集。

图片

如果满足了上面这几个条件,Dump线程开始扫描Binlog,定位从哪个BINLOG开始复制数据。

  • 根据Binlog Index文件中的binlog列表逆序扫描。

  • 打开Binlog文件,读取Binlog头部的Previous GTIDs事件。如果Previous GTIDs是GTID_SEG_ARG的子集,就从当前Binlog开始复制数据。否则,继续读取上一个Binlog文件。

  • 发送Binlog。如果没有使用AUTO_POSITION,那么Dump Binlog命令的参数中就包含了起始Binlog的文件和位点。如果使用AUTO_POSITION,那么前面的步骤已经定位到了起始Binlog,这时从Binlog文件的头部开始读取数据。Dump线程依次读取Binlog文件中的事件,发送给备库。如果使用了AUTO_POSITION,会在发送前先检查事件的GTID是不是包含在GTID_SET_ARG中,如果GTID_SET_ARG中已经有这个GTID了,就跳过这个事件。

当所有Binlog内容都已经发送给备库后,Dump线程开始等待新的Binlog事件生成。用户线程提交新的事务时,会通知Dump线程进行处理。

半同步(Semi Sync)

默认情况下,MySQL主备库之间的数据是异步进行的。主库提交事务时,并不需要等待Binlog复制到备库。这样做的好处是减少了备库对主库性能的影响,坏处是主库如果异常崩溃,可能最新的Binlog还没有同步到备库,如果业务切换到备库,就有可能出现数据丢失。半同步插件(semi-sync)能在一定程度上减少这种情况的出现。

开启半同步

开启半同步需要在主库和备库安装插件,并设置相关参数。

主库上安装rpl_semi_sync_source插件。

INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';

备库上安装rpl_semi_sync_replica插件。

INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';

主库上设置参数。

set global rpl_semi_sync_source_enabled=ON;

备库上设置参数。

set global rpl_semi_sync_replica_enabled=ON;

开启半同步后,主库上事务提交时,会等待Binog发送完成。如果由于各种原因,备库没有接收到Binlog,主库会一直等待,状态是“Waiting for semi-sync ACK from replica”,直到超时。超时时间通过参数rpl_semi_sync_source_timeout设置。

mysql> show processlist \G
*************************** 2. row ***************************
     Id: 11
   User: root
   Host: localhost:54932
     db: db01
Command: Query
   Time: 10
  State: Waiting for semi-sync ACK from replica
   Info: insert into ta values(1100)

超时后,半同步会退化成异步复制。可以查看参数Rpl_semi_sync_source_status,判断半同步有没有生效。

mysql> show global status like '%semi%status%';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_source_status | ON    |
+-----------------------------+-------+

如果你使用了半同步,建议把Rpl_semi_sync_source_status和半同步的等待耗时监控起来。错误日志中也有半同步状态变化的信息,也可以监控起来。

[Warning] [MY-011153] [Repl] Timeout waiting for reply of binlog (file: mysql-binlog.000010, pos: 2654), semi-sync up to file mysql-binlog.000010, position 2381.
[Note] [MY-011155] [Repl] Semi-sync replication switched OFF.
[Note] [MY-011156] [Repl] Semi-sync replication switched ON at (mysql-binlog.000010, 2927).

AFTER_SYNC和AFTER_COMMIT

半同步是在事务提交的哪个阶段等待Binlog发送呢?这和参数rpl_semi_sync_master_wait_point的设置有关。

图片

rpl_semi_sync_master_wait_point可以设置为AFTER_SYNC或AFTER_COMMIT。

  • AFTER_SYNC

在AFTER_SYNC的设置下,事务的Binlog Sync到磁盘之后,等待Binlog发送给备库。Binlog Sync之后,事务对其他线程还不可见。如果这个时候主库异常崩溃了,业务切换到备库。由于Binlog还没有同步到备库,所以备库上也看不到事务所做的修改。从业务的角度看,主备库数据是一致的。

但是原来的主库重新启动时,由于事务的Binlog已经完成了持久化,MySQL会将事务设置为已提交状态。因此相比于备库,主库多执行了一些事务。因此主库启动后,需要将这些事务的修改撤销后才能和新主库保持数据一致。

  • AFTER_COMMIT

在AFTER_COMMIT的设置下,主库在完成InnoDB Commit之后,等待Binlog同步到备库。此时,主库上其他会话已经能看到事务所做的修改了。如果这个时候主库发生异常崩溃,业务切换到备库。由于Binlog还没有同步到备库,因此备库上看不到事务所做的修改。从业务上看,备库比主库少执行了一些事务。

半同步不能保证主备数据之间完全同步,因为半同步可能会退化成异步复制。半同步在没有退化的状态下,可以提升主备数据的一致性。如果使用半同步,一般会给主库添加多个备库,只要有一个备库接收到Binlog,主库事务就可以完成提交,减少由于单个备库的问题导致主库性能下降。

总结

好了,这一讲中,我们分析了MySQL事务的二阶段提交。使用默认的异步复制,在极端情况下,主备数据可能会出现不一致。sync_binlog设置为1时,可以保障主库的数据一致性。

半同步可以部分提升主备之间的数据一致性,开启半同步会有一些性能开销,使用半同步需要做好监控。

思考题

主备库数据的一致性非常重要。如果数据不一致了,会带来很多问题,备库的复制可能会中断,业务切换到备库后,会取到错误的数据,影响业务。而且一般我们会在备库上备份,避免备份时影响主库性能,但如果备库的数据有问题,那么我们的备份也都是有问题的。

那么怎么检查主备库之间的数据是不是一致的呢?

期待你的思考,欢迎在留言区中与我交流。如果今天的课程让你有所收获,也欢迎转发给有需要的朋友。我们下节课再见。