MySQL 数据库内建的复制功能是构建基于 MySQL 的大规模、高性能应用的基础。复制就是让一个 MySQL主库 (Master) 将数据通过日志的方式经网络传送到另一台或多台 MySQL 从库(Slave),然后在从库上重放该日志,以达到和主库数据同步的目的。
MySQL 的复制模式分为异步复制、全同步复制及半同步复制三种。下面将针对不同复制模式下的数据一致性问题进行详细分析。
异步复制(Asynchronous replication)
主库在执行完客户端提交的事务后会立即提交并返回,不关心从库是否已经接收到日志并处理。如果此时主库上已经提交的事务因为某些原因未传送到从库,同时主库发生宕机,且在此时从库提升为主库,就会导致新主库数据缺失,从而造成主从数据不一致的情况发生。该复制模式下必然存在此问题。
主库将事务写入到 Binlog 文件中,并通知 dump 线程发送这些新的 Binlog,然后主库就会继续处理提交操作,所以此时无法保证这些 Binlog 已经成功传到任何一个从库节点上。
全同步复制(Fully synchronous replication)
主库执行完一个事务,且所有从库都执行了该事务才返回给客户端。因为需要等待所有从库才能返回,所以事务的时间会被拉长,从而性能必然会受到较大影响。
全同步是在 MySQL NDB Cluster 上采用的复制方式。NDB 是分布式存储引擎,无共享架构,严格来说 NDB 节点不是复制,而是 2PC,确保事务提交的强一致。该集群在国内使用极少,且存在较多问题,不建议采用。全复制会严重影响主库的事务提交性能,对网络要求非常严格,不适合同城、异地的架构场景。
半同步复制(Semisynchronous replication)
介于全同步复制与异步复制之间。主库需要等待至少一个从库节点收到日志事件,并刷新 Binlog 到 Relay Log 文件中的 ACK 确认消息后,才能提交并返回,主库一般不需要等待所有从库的 ACK。注意 :该 ACK 是确保日志传送到从库并写入到中继日志,而不是从库已经完成重放、将数据写入完成。详见图 1。
image
图1 半同步复制模式
相对于异步复制,半同步复制提高了数据的安全性,同时也带来了一定程度的延迟,这个延迟最少是一个 TCP/IP 往返的时间。所以,半同步复制最好在低延时的网络中使用。
一般在实际生产中,我们把同城或同机房,网络延迟小的(2ms 内)设置为半同步,对于异地延迟在 10ms 以上的,采用异步复制的方式。
一般将
rpl_semi_sync_master_wait_for_slave_count 设置为1,在数据一致性的情况下,可以最大程度保证主库事务处理的性能。
MySQL 官方称该复制模式为无损复制,从复制机制上看,的确会保证数据不丢失。但真实情况是这样吗?下面我们从半同步的两种复制形式进行分析。
1. 普通半同步 AFTER_COMMIT。处理流程如下(见图 2)。首先要理解第三步引擎层提交的含义。第三步完成后,由于主库还未收到从库返回的确认信息,当前会话一直无法完成并返回给客户端,但主库的其他会话已经可以看到执行的结果了。如果按照事务隔离级别来理解,就相当于对主库的读取出现了脏读。
image
图2 普通半同步处理流程
例如 :你给小王转账 100 元,在步骤三完成后,小王会查询到自己的余额多了 100 元。但如果此时发生异常 :主库宕机,日志尚未发送到从库,且发生了主从切换,从库提升为主库。那么因为之前的转账 100 元日志未发送到从库,小王再次查询余额的时候,会发现之前已“到账”的 100 元又不见了!
此时就会出现两种不一致 :(1)数据层不一致。主库宕机重启后,依然有之前转账 100 元的记录,由于从库未接收到日志,且提升为主库后无法再同步这部分数据,而原主库重启后数据库会跳过 ACK验证,引擎层会将事务再次提交,就会比新主库多一条记录。(2)应用层不一致。前后看到的结果不一致。
最致命的是应用层不一致,数据层不一致我们可以通过数据补偿或应用重做的方法解决,但应用层不一致就是逻辑错误,是不能容忍的!
2. 增强半同步 AFTER_SYNC。MySQL 从 5.7 版本开始支持增强半同步并设为默认值,处理流程如下(见图 3)。
image
图3 增强半同步处理流程
增强半同步是接收从库返回 ACK 信息后再做引擎层提交,解决了普通半同步的脏读、应用层不一致的问题。
例如 :你给小王转账 100 元,在步骤五完成后,小王会查询到自己的余额多了 100 元。但如果此时发生异常 :主库宕机,并且发生了主从切换,从库提升为主库。由于之前的转账 100 元信息已经发送到从库,那么小王依然可以查看到之前转账的 100 元!
如果在步骤五之前发生异常,由于主库引擎层未提交,那么其他会话也是无法查看到最新的记录。
综上,就不会出现之前的应用层数据不一致问题,前后看到的结果是一样的。由此可见,增强半同步才是真正的无损复制,更有效地保证了数据的一致性,确切地说是避免了应用层的不一致。
那么,增强半同步是不是一定就可以保证数据层和应用层的数据一致性呢?答案是否定的,下面将具体分析。
(1)第一种情况,日志丢失,主从切换。如果出现主库日志未同步到从库或从库接收后未写入 Relay Log,且此时发生主从切换就会导致从库应用日志丢失,出现数据不一致的情况。所以出现主从数据不一致要具备 2 个条件 :主库日志未同步到从库 ;同时发生主从切换。
例如 :应用客户端向主库发起一条插入请求,然后开始提交,如果此时网络中断,日志未发送到从库,也就无法接受从库的 ACK 信息,于是提交无法进行,处于 hang 状态。这时如果主库宕机,且同时发生主从切换,那么应用客户端会出现报错,记录未插入成功,新主库同样也没有这条记录。当应用客户端连接新主库后重新发起请求,可再次插入这条记录,应用逻辑没有被破坏,数据是一致的。
但此时会出现数据层的不一致。当原主库再次启动后,会跳过 ACK 验证,对PendingBinlog 进行引擎层面的提交,所以启动后原主库就会存在这条记录,而新主库并没有,从而造成数据层的不一致。
当应用连接新主库再次执行这条记录时,新主库会把这条记录发送给原主库,就会报错(如果是主键)或出现重复数据。
解决方法 :一是重新初始化原主库数据,然后再建立和新主库的复制关系。二是手工处理,反向解析原主库日志,删除多余的数据 ;新主库跳过不需要同步的原主库 GTID 事务号 ;新主库追平原主库GTID。三是自动处理,现在有些成熟的管理平台已经具备了自动处理多余数据并追平 GTID 的功能,操作方法和手工处理的思路是一样的。
(2)第二种情况,日志未丢失,主从未切换。主库因为无法接收到从库的ACK 信息而无法提交并返回,此时主库宕机,如果主库重启动后未切换,主库可以正常启动,那么这条记录同样会被提交。而应用层的反应是异常,该条记录并未成功,那么会出现应用层的不一致。
解决方法 :一是让应用确认该记录已经正常插入,无需重复执行 ;二是数据库删除该记录,应用重新执行。
主库等待 ACK 返回的默认时间是60s,超过 60s 会降级成异步并提交事务。有时因为网络问题从库一直没有响应,为了确保主库可用性,我们会牺牲部分一致性要求,这是符合生产场景的。对一致性要求极高的环境,会将 ACK 返回时间设置为无穷大,这样虽然保证了一致性,但是会影响部分可用性。
(3)第三种情况,一主多从,日志丢失。有一种极端情况,即使是增强半同步,也会出现数据层和应用层的数据不一致,见图 4。
image
图4 一主多从出现数据不一致的情况
在一主多从的情况下,将应答从库数量设置为 1,同城数据中心 A 的从库首先接收到日志并返回 ACK 给主库,主库就可以正常提交。如果数据中心 A 的主库和从库均发生宕机,而这此时日志尚未发送给数据库中心 B、异地数据中心的从库,并且发生了主从切换,那么这两个数据中心从库就会在数据层和应用层均出现和原主库数据不一致的现象。
解决方法 :一是增加同城数据中心A 从库数量(尽量放置在不同机房单元或区域),并增加应答从库数量。这样会增加一些成本,而且不能解决数据中心整体故障导致的数据不一致问题。二是增加应答从库数量。比如一主三从的模式下,将应答从库数量设为 2,这样能确保同城 B中心的一个从库返回 ACK 后才提交,保证了同城 B 中心的数据一致性。但在同城数据中心间网络不稳定的情况下,对系统性能会有较大影响。对于要求 RPO 严格为零的关键系统,且两中心间网络安全可靠的前提下,可以采取这种方法,其他系统不必要采用。三是将同城双中心扩展为同城三中心或多中心,同时增加应答从库数量。这是最佳方式,但成本过高,具备此条件的数据中心可以采用。
总 结
究竟该如何最大程度地避免数据不一致的发生呢?总结如下 :一是采用较高版本的 MySQL 数据库产品,至少在 5.7及以上版本,启用增强半同步。二是回退掉 PendingBinlog,采用双 1 参数,ROW格式,开启 GTID。三是使用第三方插件如 MHA、keepalived 设置主库切换策略,发生宕机后不马上切换,尝试 N 次重启后再切换,另外从库提升为主库后,尝试从原主库拉取日志补齐(系统正常,MySQL 数据库无法启动的情况)。四是建立自动切换工具,启用数据校验和补齐机制。五是条件允许的情况下,增加数据中心数量、从库数量,增加应答从库数量。
最后,附上我行某信息系统的一个实际案例(见图 5),供读者参考。数据库版本为MySQL 5.7,主要采用了以下措施:一是采用了一主三从模式,同城双数据中心,其中一个主库和一个从库在同城 A机房,另两个从库在同城 B 机房。二是采用了增强半同步,由于系统 RPO 接近但可不为零,故应答从库数量设置为 1。三是完备的切换策略,主库尝试 N 次(N分钟)启动后再切换,基于自定义权重和最新 GTID 进行选主。四是数据校验和补齐机制功能,切换后,备选主库校验发现还有未同步的 Binlog,会尝试从原主库获取最新的 Binlog 日志并应用。
image