正常情况下, 只要主库执行更新生成的所有binlog, 都可以传到备库并被正确地执行, 备库就能达到跟主库一致的状态, 这就是最终一致性。
但是, MySQL要提供高可用能力, 只有最终一致性是不够的。
双M结构的主备切换流程图如下:
主备延迟
与数据同步有关的时间点主要包括以下三个:
1)主库A执行完成一个事务, 写入binlog, 我们把这个时刻记为T1。
2)之后传给备库B, 我们把备库B接收完这个binlog的时刻记为T2。
3)备库B执行完成这个事务, 我们把这个时刻记为T3。
所谓主备延迟, 就是同一个事务, 在备库执行完成的时间和主库执行完成的时间之间的差值, 也就是T3-T1(这个值的时间精度是秒)。
可以在备库上执行show slave status命令, 它的返回结果里面会显示seconds_behind_master, 用于表示当前备库延迟了多少秒。
seconds_behind_master的计算方法是这样的:
1)每个事务的binlog 里面都有一个时间字段, 用于记录主库上写入的时间。
2)备库取出当前正在执行的事务的时间字段的值, 计算它与当前系统时间的差值, 得到seconds_behind_master。
问:如果主备机器的系统时间设置不一致,会不会导致主备延迟的值不准?
答:不会。因为, 备库连接到主库的时候, 会通过执行SELECTUNIX_TIMESTAMP()函数来获得当前主库的系统时间。 如果这时候发现主库的系统时间与自己不一致, 备库在执行seconds_behind_master计算的时候会自动扣掉这个差值。
注:在网络正常的时候, 日志从主库传给备库所需的时间是很短的, 即T2-T1的值是非常小的。 也就是说, 网络正常情况下, 主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。
主备延迟的来源
场景一:备库机器性能略差
一般情况下,在部署主从时,都会有这么一个想法:反正备库没有请求, 所以可以用差一点儿的机器。 或者, 他们会把20个主库放在4台机器上, 而把备库集中在一台机器上。而且这种部署一般都会将备库设置为“非双一”的模式。
但实际上,备库更新过程中也会触发大量的读操作。所以,当备库主机上的多个备库都在争抢资源的时候,就可能导致主备延迟了。
当然,这种部署现在出现的比较少了。因为主备可能发生切换,备库随时可能变成主库,所以主备库选用相同规格的机器,并做对称部署,这是现在比较常见的情况。
场景二:备库压力大
问1:即便做了对称部署,还是会有可能出现延迟,为什么?
答:备库压力过大。
一般情况下,主库既然提供了写能力,那么备库可以提供一些读能力或者一些运营后台需要的分析语句,。因为不能影响正常业务, 所以只能在备库上跑。
由于主库直接影响业务, 大家使用起来会比较克制, 反而忽视了备库的压力控制。 结果就是, 备库上的查询耗费了大量的CPU资源, 影响了同步速度, 造成主备延迟。
问2:如何解决备库压力大问题?
1)一主多从。 除了备库外, 可以多接几个从库, 让这些从库来分担读的压力。(常用方式)
2)通过binlog输出到外部系统, 比如Hadoop这类系统, 让外部系统提供统计类查询的能力。
注:备库和从库其实是一个概念,在这里把能够主备切换的称为备库,以和其它从库进行区分。
场景三:大事务
问:采用一主多从,保证备库的压力不会超过主库, 还有什么情况可能导致主备延迟吗?
答:大事务。
因为主库上必须等事务执行完成才会写入binlog, 再传给备库。 所以, 如果一个主库上的语句执行10分钟, 那这个事务很可能就会导致从库延迟10分钟。
这也正是为什么不要一次性地用delete语句删除太多数据,其实这就是一个典型的大事务场景。
比如, 一些归档类的数据, 平时没有注意删除历史数据, 等到空间快满了, 业务开发人员要一次性地删掉大量历史数据。(为了减少大事务导致的主从延迟,需要控制每个事务删除的数据量,分成多次删除)
另一种典型的大事务场景, 就是大表DDL。
场景四:备库的并行复制能力
可靠性优先策略
双M结构的主备切换流程图如下:
主备切换的详细过程如下:
1)判断备库B现在的seconds_behind_master, 如果小于某个值(比如5秒,即保证主从复制在延迟较小的情况下切换) 继续下一步, 否则持续重试这一步。
2)把主库A改成只读状态, 即把readonly设置为true。
3)判断备库B的seconds_behind_master的值, 直到这个值变成0为止。
4)把备库B改成可读写状态, 也就是把readonly设置为false。
5)把业务请求切到备库B。
这个切换流程, 一般是由专门的HA系统来完成的, 我们暂时称之为可靠性优先流程。
可靠性优先主备切换流程:
可以看到, 这个切换流程中是有不可用时间的。 因为在步骤2之后, 主库A和备库B都处于readonly状态, 也就是说这时系统处于不可写状态, 直到步骤5完成后才能恢复。
在这个不可用状态中, 比较耗费时间的是步骤3, 可能需要耗费好几秒的时间。 这也是为什么需要在步骤1先做判断, 确保seconds_behind_master的值足够小。
如果一开始主备延迟就长达30分钟, 而不先做判断直接切换的话, 系统的不可用时间就会长达30分钟, 这种情况一般业务都是不可接受的。
注1: 图中的SBM, 是seconds_behind_master参数的简写。
注2:系统的不可用时间,是由这个数据可靠性优先的策略决定的。可以选择可用性优先策略,来把这个不可用时间几乎降为0。
可用性优先策略
可用性优先策略切换流程:强行把步骤4、 5调整到最开始执行, 也就是说不等主备数据同步, 直接把连接切到备库B, 并且让备库B可以读写, 那么系统几乎就没有不可用时间了。
注:这个切换流程的代价,就是可能出现数据不一致的情况。
下面来看一个可用性优先导致数据不一致的案例,假设有一个表t:
CREATE TABLE `t` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`c` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(c) values(1),(2),(3);
接下来, 业务人员要继续在表t上执行两条插入语句的命令, 依次是:
insert into t(c) values(4);
insert into t(c) values(5);
假设, 现在主库上其他的数据表有大量的更新, 导致主备延迟达到5秒。 在插入一条c=4的语句后, 发起了主备切换。
下图是可用性优先策略,且binlog_format=mixed时的切换流程和数据结果:
切换流程分析:
1)步骤2中, 主库A执行完insert语句, 插入了一行数据(4,4) , 之后开始进行主备切换。
2)步骤3中, 由于主备之间有5秒的延迟, 所以备库B还没来得及应用“插入c=4”这个中转日志,就开始接收客户端“插入 c=5”的命令。
3)步骤4中, 备库B插入了一行数据(4,5) , 并且把这个binlog发给主库A。
4)步骤5中, 备库B执行“插入c=4”这个中转日志, 插入了一行数据(5,4) 。 而直接在备库B执行的“插入c=5”这个语句, 传到主库A, 就插入了一行新数据(5,5) 。
最后的结果就是, 主库A和备库B上出现了两行不一致的数据。 可以看到, 这个数据不一致, 是由可用性优先流程导致的。
问1:如果还是使用可用性优先策略,但是设置binlog_format=row, 情况又会怎样呢?
因为row格式在记录binlog的时候, 会记录新插入的行的所有字段值, 所以最后只会有一行不一致。 而且, 两边的主备同步的应用线程会报错duplicate keyerror并停止。 也就是说, 这种情况下, 备库B的(5,4)和主库A的(5,5)这两行数据, 都不会被对方执行。
可用性优先策略,row格式binlog主备切换流程:
从上面的分析中, 可以看到一些结论:
1)使用row格式的binlog时, 数据不一致的问题更容易被发现。 而使用mixed或者statement格式的binlog时, 数据很可能悄悄地就不一致了。 如果你过了很久才发现数据不一致的问题,很可能这时的数据不一致已经不可查, 或者连带造成了更多的数据逻辑不一致。
2)由于可用性优先策略会导致数据不一致问题,因此在大多数情况下,都建议使用可靠性优先策略。即数据的可靠性要优于可用性。
问2:按照可靠性优先的思路,异常切换会是什么效果?
假设, 主库A和备库B间的主备延迟是30分钟, 这时候主库A掉电了, HA系统要切换B作为主库。 我们在主动切换的时候, 可以等到主备延迟小于5秒的时候再启动切换, 但这时候已经别无选择了。
而如果直接切换到备库B,由于中转日志还没有应用完成,这时客户端查不到之前主库A执行完的事务,会认为有“数据丢失”。
虽然随着中转日志的继续应用, 这些数据会恢复回来, 但是对于一些业务来说, 查询到“暂时丢失数据的状态”也是不能被接受的。
结论:MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。
小结:思考题
一般现在的数据库运维系统都有备库延迟监控, 其实就是在备库上执行 show slave status, 采集seconds_behind_master的值。
思考:假设, 现在你看到你维护的一个备库, 它延迟监控的图像类似下图,是一个45°斜向上的线段,你觉得可能是什么原因导致的?又如何去确认这个原因呢?
答:备库的同步在这段时间完全被堵住了。产生这种现象典型的场景主要包括两种:
1)大事务,包括:大表DDL、一个事务操作很多行。
2)从库在进行备份操作,锁住更新。
3)备库磁盘空间不足,无法写入数据。