URL
date
AI summary
slug
status
tags
summary
type
前一篇文章聊了MySQL并行复制的原理和演进,这篇文章聊聊MySQL主从复制的两种方式。
异步复制
这是MySQL最早支持的复制方式。异步复制,顾名思义,指的是复制过程是异步的,主库在写完binlog之后就会给从库发binlog event,但是只管发而不会同步等待对方接收成功,发完之后主库线程就接着提交事务了。整个过程如下图所示:
这种方式存在一个问题:假设在主库事务提交之后,从库写入relay log之前,主库发生宕机,主从切换,则会造成数据丢失(用户已经成功提交的事务丢失了,没有保证ACID里的D)。
半同步复制
为了解决异步复制存在的问题,MySQL 5.5引入了半同步复制。这种复制方式要求主库在发送binlog event给从库之后,需要加入一个同步等待的环节,但是并不是等所有从库返回,只要当任意一个从库接收到binlog、写入relay log并响应ack之后(所以叫半同步),主库才能接着进行事务提交。整个过程如下图所示:
这里支持等待的节点有两处:
after_commit
,在主库事务commit之后等待
after_sync
,在主库事务binlog落盘之后,commit之前等待
上图展示的是
after_sync
这种方式,也是更为主流的半同步方式,也被称为增强式半同步或者无损半同步。after_commit
第一个版本引入的半同步复制只支持
after_commit
,这种模式保证了只要事务成功响应给客户端,至少有一个从库把该事务相关的binlog event写入了relay log。这样即使主库宕机,从库依然有完整的数据,不会造成数据丢失。只要告诉客户端事务提交成功,则数据一定不会丢失的持久性。注意:上图中主库dump线程发送binlog event给从库的时间点有2个,结合源码来看,应该一个在Flush之后,一个在Sync之后。这是事务Commit三阶段(Flush-Sync-Commit)中的前两个,不太了解的同学可以看看这篇文章——MySQL的二阶段提交和组提交。
而具体在哪个时间点发送和参数
sync_binlog
的配置有关,如果sync_binlog<>1
,则在Flush之后发送,此时binlog只是write,并不保证落盘。如果sync_binlog=1
,则在Sync之后,也就是binlog落盘后。而主库等待从库Ack不受此参数影响,只和
after_commit
和after_sync
相关。幻读问题
由于
after_commit
等待从库Ack的时间点是在Engine Commit之后,虽然事务线程还没返回,但是其他线程已经能查询到事务影响的数据了。假设此时binlog event还没成功写入relay log,主库发生宕机,主从切换,就会产生幻读。下图展示了这个场景:- User1插入一条数据,事务已经提交,等待从库ack,但从库并未写入relay log
- 由于User1的事务已经提交,User2上来查询,已经能插到刚刚User1插入的数据了
- Master宕机,切换到Slave,User2再进行同样的查询,发现刚刚User1插入的数据不见了
after_sync
2013年,MySQL 5.7.2版本引入了无损半同步复制(Loss-less Semi-Synchronous Replication on MySQL 5.7.2)。解决方案就是调整了一下Engine Commit和等待Ack的顺序:先等待从库ack,确保从库已经把binlog event写入relay log之后再进行Engine Commit。同时这样可以堆积事务,利于Commit阶段的组提交,能提升性能。流程如下图所示:
其他优化
等待从库ack的数量可配
随后的MySQL 5.7.3版本假定只有一个Slave也可能是不可靠的,所以引入了对于多Slave的ack保证,增加了一个可配置的参数
rpl_semi_sync_master_wait_slave_count
,用来表示Master需要等待多少个Slave的ack才能继续。拆分dump线程和ack接收线程
而MySQL 5.7.4版本把Master上的dump和ack拆分成2个线程,消除了dump线程的延迟。之前是一个dump线程做2件事情,接收到ack之后才能继续发送下一个binlog给slave。
优化后的半同步复制流程大致如下:
关于更详细的半同步复制的发展,你可以看这个网站
半同步相关配置
mysql>show variables like '%semi%'; +-------------------------------------------+-----------------+ | Variable_name | Value | +-------------------------------------------+-----------------+ | rpl_semi_sync_master_enabled | ON | | rpl_semi_sync_master_timeout | 1000 | | rpl_semi_sync_master_trace_level | 1 | | rpl_semi_sync_master_wait_for_slave_count | 1 | | rpl_semi_sync_master_wait_no_slave | OFF | | rpl_semi_sync_master_wait_point | AFTER_SYNC | | rpl_semi_sync_slave_enabled | OFF | | rpl_semi_sync_slave_trace_level | 1 | +-------------------------------------------+-----------------+
其中主要关注的几个变量:
- rpl_semi_sync_master_enabled:主库是否开启半同步复制
- rpl_semi_sync_master_timeout:半同步复制的超时时间
- rpl_semi_sync_master_wait_for_slave_count:等待ack的数量
- rpl_semi_sync_master_wait_no_slave:如果发现开启半同步的从库的数量(状态值Rpl_semi_sync_master_clients)小于rpl_semi_sync_master_wait_for_slave_count,是立即降级到异步复制还是等到超时再降级
- rpl_semi_sync_master_wait_point:等待ack的时间点(after_commit/after_sync)
- rpl_semi_sync_slave_enabled:从库是否开启半同步复制
半同步监控
mysql>show status like '%semi%' +--------------------------------------------+-----------------+ | Variable_name | Value | +--------------------------------------------+-----------------+ | Rpl_semi_sync_master_clients | 1 | | Rpl_semi_sync_master_net_avg_wait_time | 0 | | Rpl_semi_sync_master_net_wait_time | 0 | | Rpl_semi_sync_master_net_waits | 185458531 | | Rpl_semi_sync_master_no_times | 3 | | Rpl_semi_sync_master_no_tx | 2419 | | Rpl_semi_sync_master_status | ON | | Rpl_semi_sync_master_timefunc_failures | 0 | | Rpl_semi_sync_master_tx_avg_wait_time | 1986 | | Rpl_semi_sync_master_tx_wait_time | 298549272827 | | Rpl_semi_sync_master_tx_waits | 150297538 | | Rpl_semi_sync_master_wait_pos_backtraverse | 0 | | Rpl_semi_sync_master_wait_sessions | 0 | | Rpl_semi_sync_master_yes_tx | 151419221 | | Rpl_semi_sync_slave_status | OFF | +--------------------------------------------+-----------------+
- Rpl_semi_sync_master_clients:有多少个开启半同步复制的从库
- Rpl_semi_sync_master_net_avg_wait_time:主库等待从库回复的平均时间,以微秒为单位。此变量始终为0,不推荐使用,并且将在以后的版本中删除
- Rpl_semi_sync_master_net_wait_time:主库等待从库回复的总时间,以微秒为单位。此变量始终为0,不推荐使用,并且将在以后的版本中删除
- Rpl_semi_sync_master_net_waits:主库等待从库回复的总次数。
- Rpl_semi_sync_master_no_times:主库关闭半同步复制的次数
- Rpl_semi_sync_master_no_tx:从库未成功确认的事务数(也就是未通过半同步复制的事务数)
- Rpl_semi_sync_master_status:主库上半同步复制是否正常运行,ON表示半同步复制正常运行,OFF则表示降级为异步复制
- Rpl_semi_sync_master_timefunc_failures:调用gettimeofday等时间函数时主库失败的次数。
- Rpl_semi_sync_master_tx_avg_wait_time:主库等待一个事务的平均时间,以微秒为单位。
- Rpl_semi_sync_master_tx_wait_time:主库等待事务的总时间,以微秒为单位。
- Rpl_semi_sync_master_tx_waits:主库等待事务的总次数。
- Rpl_semi_sync_master_wait_pos_backtraverse:主库等待事件的二进制日志次数低于之前等待事件的总次数。当事务等待回复的顺序与其二进制日志事件的写入顺序不同时,就会发生这种情况。
- Rpl_semi_sync_master_wait_sessions:当前正在等待从库回复的会话数。
- Rpl_semi_sync_master_yes_tx:从库成功确认的事务数(也就是通过半同步复制的事务数)
- Rpl_semi_sync_slave_status:从库上半同步复制是否正常运行,ON表示从库正通过半同步复制且Slave I/O正在运行;为OFF时,反之。
疑问解答和相关思考
从库ack之后是不是一定不会丢?
不一定。relay log也是文件,也要写磁盘,所以也会有刷盘的性能问题。设计上也通过了一个配置
sync_relay_log
来控制刷盘的策略。如果配置为1,那么每次接收到主库的binlog event都会保证刷盘,但是性能最差。我观察了一下我们的生产库,配置的是10000。其实这个值如果不配置为1的话,主库和从库只有一个挂了其实是没有影响的。- 假设从库挂了,虽然有一些已经ack了的事务丢了,但是从库重启之后,又会去主库重新拉取
- 假设主库挂了,其实本身就对从库已经接收到的事务没有影响
只有从库挂了之后,在从库重启之后再到主库重新拉取binlog之前,主库也挂了,从库被提升成主库这种场景下,没刷盘的relay log那部分事务才会丢失。
canal也会ack?
canal的提交记录里有一个关于半同步ack的更新。因为canal-instance模拟的就是mysql主从复制的协议,对于主库来说它就是一个从库,但是它需要或者应该响应半同步ack吗?
我觉得是不需要。因为本身半同步复制的目的是为了保证主库事务提交后,从库的relay log里一定也保存有对应的事务。从而在主库宕机时,从库可以被安全的提升成主库且不会丢失数据。而一般这种实例级别的复制都会直接使用mysql原生的主从复制,而不太会使用canal。使用canal都是做一些偏业务上的数据异构或者是数据变更监听。如果canal去响应ack,反而会影响半同步的效果:canal抢先ack了,真正的备库还没ack,主库的事务就提交了,反而导致备库数据可能不全。
并且canal在半同步ack的实现上,感觉也存在一些问题。首先,在连接主库的时候并没有告知是否自身开启了半同步。(这个是通过从库是否设置了变量
rpl_semi_sync_slave
来判断(ReplSemiSyncMaster::is_semi_sync_slave())
)。不清楚这种从库如果给主库响应ack,主库是否会接受?但是感觉就非常山寨。好在这个功能不是默认开启的,需要配置启动参数启用,个人感觉实际也没有人在使用。我们可以通过下面的SQL验证一下canal-instance并没有被当做半同步从库:
mysql>SHOW SLAVE HOSTS; +---------------------+----------------+----------------+---------------------+--------------------------------------+ | Server_id | Host | Port | Master_id | Slave_UUID | +---------------------+----------------+----------------+---------------------+--------------------------------------+ | 453710748 | 10.11.19.156 | 43010 | 916966854 | 3218525e-87d3-11ef-9e24-0c42a1e7f60a | | 705368988 | 10.11.19.156 | 42990 | 916966854 | 31d18d22-87d3-11ef-9e24-0c42a1e7f60a | | 1980437471 | 10.11.19.223 | 58944 | 916966854 | 31735ecd-87d3-11ef-9e24-0c42a1e7f60a | | 1705837295 | | 3069 | 916966854 | 486c8ecf-6374-11ef-bc71-0c42a1f0492e | | 1158353820 | 10.11.19.156 | 43196 | 916966854 | 3b2d5104-87d3-11ef-9e24-0c42a1e7f60a | +---------------------+----------------+----------------+---------------------+--------------------------------------+ mysql>show status like 'Rpl_semi_sync_master_clients'; +------------------------------+-----------------+ | Variable_name | Value | +------------------------------+-----------------+ | Rpl_semi_sync_master_clients | 1 | +------------------------------+-----------------+
10.11.19.*
的ip都是canal-instance,总共5个从库,但是半同步从库只有一个。从库会先看到更新?
由于半同步复制(after_sync)是先写入从库的relay log之后再提交事务,但是从库的sql线程也在回放relay log里的事务,假设主库由于某些原因卡顿了一下,事务可能先在从库中可见。确实也有小伙伴在实际生产环境中碰到了类似的问题,详细可见MySQL事务还没提交,Canal就能读到消息了?。
主从切换后数据不一致?
半同步复制虽然解决了主从切换时数据丢失的问题,但是却带来了另一个问题:因为主库的binlog会先落盘,如果在从库写入relay log之前,主库宕机,切换到从库,此时从库并没有接收到对应的binlog event,旧主重启之后进行数据恢复,由于redo和binlog都存在,所以对应事务会被提交,那么和新主的数据不一致。
这个时候应该就要人为介入处理了,手动把旧Master上的多余事务回滚掉?因为对于用户来说,这个写入是没有成功的,并且旧Master宕机之前也没有在存储引擎提交。但是具体怎么回滚呢?
- 一个思路是不是可以直接放弃旧主,直接从新主再重做一个从库,这样数据一定是一致的。
- 另外可以借助一些工具,找出对应的gtid进行闪回?
半同步降低可用性?
半同步复制这种方式一个明显的弊端就是会造成可用性降低(依赖从库),且性能降低(多了等待从库响应的时间)。如果从库不可用或者网络不可达,主库会等待一个配置的超时时间,如果超时了,那么后续会自动降级为异步复制。当半同步从节点重新达到
rpl_semi_sync_master_wait_for_slave_count
,主库才会自动转换为半同步方式的复制。这也算是对于可用性的弥补,可用性并不强依赖从库。新的半同步插件
MySQL 8.0.26开始,提供了新的主从复制插件(semisync_source.so/semisync_replica.so),也使用了新的系统变量(rpl_semi_sync_source_wait_for_replica_count/rpl_semi_sync_source_wait_no_replica等),但是原理应该是一样的。
参考
- 作者:黑微狗
- 链接:https://blog.hwgzhu.com/article/mysql-replication-async-with-semi
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章