MySQL高可用实践
上QQ阅读APP看书,第一时间看更新

2.3 数据一致性

半同步复制最主要的目标是保证主从数据强一致性。要想搞清楚这其中的原理和具体实现,需要研读MySQL源代码。MySQL semi-sync半同步以插件方式引入,源代码在plugin/semisync目录下。阿里有一篇很好的文章(http://mysql.taobao.org/monthly/2017/04/01/),从官方MySQL 5.7源代码的层面分析了半同步复制的数据一致性问题。

在半同步方式中,主库在等待从库ACK时,如果超时则会退化为异步复制,这就可能导致数据丢失。主库等待提交确认的超时时间,由rpl_semi_sync_master_timeout参数控制,默认值为10000毫秒。在下面的分析中,假设rpl_semi_sync_master_timeout足够大,不会退化为异步方式。这里通过三个参数rpl_semi_sync_master_wait_point、sync_binlog、sync_relay_log的配置来对半同步复制进行数据一致性的分析。

2.3.1 rpl_semi_sync_master_wait_point配置

(1)源代码剖析

(2)设置为WAIT_AFTER_COMMIT

rpl_semi_sync_master_wait_point为WAIT_AFTER_COMMIT时,commitTrx的调用在引擎层提交之后(由ordered_commit函数中的process_after_commit_stage_queue调用),如图2-6所示。即在等待从库ACK时,虽然没有返回当前客户端,但事务已经提交,其他客户端会读取到已提交的事务。如果从库还没有读到该事务在二进制日志中的事件,同时主库发生了崩溃,然后切换到从库。那么之前读到的事务就不见了,出现了幻读,如图2-7所示。

图2-6 WAIT_AFTER_COMMIT处理流程

图2-7 WAIT_AFTER_COMMIT导致幻读

除了幻读,这种场景还有一个问题是,如果客户端会重新尝试把该事务提交到新的主库上,当宕机的主库重新启动后,以从库的身份重新加入到该主从结构中,那么此时就会发现,该事务在从库中被提交了两次,一次是之前作为主库的时候,一次是被新主库同步过来的,结果依然是主从数据不一致。

(3)设置为WAIT_AFTER_SYNC

MySQL针对上述问题,在5.7.2引入了Loss-less Semi-Synchronous。在调用binlog sync之后,引擎层提交之前等待从库的ACK。这样只有在确认从库收到事务的二进制日志事件后,事务才会提交。在提交之前等待从库ACK,同时可以堆积事务,趋向group commit,有利于提升性能,如图2-8所示。

图2-8 WAIT_AFTER_SYNC处理流程

其实在图2-8的流程中依然存在着导致主从数据不一致,使主从同步失败的情形。进一步的说明可见下面章节对sync_binlog配置的分析。

2.3.2 sync_binlog配置

(1)源代码剖析

(2)设置分析

当sync_binlog设置为0时,binlog sync磁盘由操作系统负责。当不为0时,其数值为定期刷磁盘的binlog commit group次数。当sync_binlog值大于1时,sync binlog操作可能并没有使binlog落盘。如果没有落盘,事务在提交前,主库掉电,然后恢复,那么这个时候该事务被回滚。但是,从库上可能已经收到了该事务的二进制日志事件并且执行了,这个时候就会出现从库事务比主库多的情况,主从同步失败。因此,如果要保持主从一致,则需要设置sync_binlog为1。

WAIT_AFTER_SYNC和WAIT_AFTER_COMMIT两图中Send Events的位置(图2-6和图2-8),也可能导致主从数据不一致,出现同步失败的情况。实际在rpl_semi_sync_master_wait_point分析的图中是sync binlog大于1的情况。根据上面源代码,流程如图2-9所示。主库依次执行flush binlog、update binlog position、sync binlog。如果主库在update binlog position后,且sync binlog前掉电,主库再次启动后原事务就会被回滚,但可能出现从库已经获取到事件,这也会导致从库数据比主库多的情况,结果主从同步失败。

图2-9 sync_binlog大于1时的处理流程

由于上面的原因,sync_binlog设置为1时,MySQL会在sync后更新二进制日志坐标。流程如图2-10所示。这时,对于每一个事务都需要sync binlog,同时sync binlog和网络发送二进制日志事件会是一个串行的过程,性能会下降。

图2-10 sync_binlog等于1时的处理流程

2.3.3 sync_relay_log配置

(1)源代码剖析

(2)设置分析

在从库的I/O线程中get_sync_period获得的是sync_relay_log的值。与sync_binlog对sync控制一样,当sync_relay_log不是1时,semisync返回给主库的position可能没有刷新到磁盘。开启GTID时,在保证前面两个设置正确的情况下,sync_relay_log不是1的时候,仅发生主库或从库的一次崩溃并不会造成数据丢失或者主从同步失败的情况。如果发生从库没有sync relay log,主库事务提交,客户端观察到事务提交,然后从库崩溃,这样从库端就会丢失掉已经回复主库ACK的事务,如图2-11中所示的GTID:xx:1-40。

图2-11 sync_relay_log不为1时从库崩溃

但当从库再次启动时,会从主库同步丢失事务的二进制日志事件。如果没有来得及这样做主库就崩溃了,此时用户访问从库就会发现数据丢失,如图2-12所示。

图2-12 主库在从库同步前崩溃导致数据丢失

通过上面这个例子可知,MySQL半同步复制如果要保证任意时刻发生一台机器宕机都不丢失数据,则需要同时设置sync_relay_log为1。对relay log的sync操作是在queue_event中,每个事件都要sync,所以sync_relay_log设置为1时,事务响应时间会受到影响,对于涉及数据比较多的事务延迟会增加很多。

这么多分析后我们不难发现,当前原生的MySQL主从复制要同时满足数据一致性、高可用和高性能,依然是力有不逮。