1、背景
前段时间大厂故障频发,不禁想起3年前,公司出现机房级别的故障,导致全站业务不可用。借此聊聊同城双活这个话题。
2、问题描述
2.1 备用机房资源利用率低
现有架构采用A/B双机房,A当做主机房承担全部流量,B机房只做冷备,平时没有流量,资源利用率低、缓存没有预热、没有经过大流量的验证,除了做数据备份,没有接管主机房的能力。
2.2 故障时间长
底层数据存储使用mysql,由于采用异步复制,即便开启增强半同步,也无法保障两个机房数据的强一致,这就导致当主数据库出现故障时,业务直接不可用,整体恢复时间依赖主数据库恢复时间。
3、同城双活建设
在这个背景之下,同城双活架构呼之欲出,通过同城双活可以解决上面说的两个问题。
- 提升备机房机器利用率,实现双机房对半流量,虽提升了备用机房资源利用率,但为了做到双活,每个机房还是需要具备承担全机房流量的能力,所以通常硬件成本还是无法降低。
- 由于具备双活的能力,当机房down掉以后,可以快速将流量切至备机房,可以做到RTO < 15分钟,大大缩短故障恢复时间。
听上去好像同城双活好处很大,但是对需要数据强一致的业务,特别是对资金敏感的业务,做到RTO<15分钟的目标,遇到最大的挑战是:当专线故障或者主机房down机数据不一致的情况下如何保障业务的连续性。
3.1 现有架构
这是一个简化的架构,相关中间件例如MQ、redis、es-job等等,由于都属于业务弱依赖或者可以保障最终一致性所以均没有画出,本文主要讨论数据库层面一致性带来的影响以及如何解决。
从现有架构中可以看到,机房A为主机房,机房B通过专线同步调用机房A,并且机房A和机房B通过增强半同步保证数据一致,另外所有的请求除了最后数据库的操作需要跨专线访问主机房,其余都在机房内封闭,保障性能。
3.2 增强半同步数据不一致
mysql5.7以后开始支持增强半同步,也就是主库会等待至少一个从库复制到relay log并且收到ack之后再进行commit,但即便如此也无法保障双机房主备数据完全一致。
- 专线故障时,从库ack超时,导致增强半同步降级为异步复制,从而使得数据不一致
- 从库数据复制完成之前主库down机,主从数据不一致(原主库会比新主库多数据)。
既然,增强半同步无法保证双机房数据的强一致,最终一致是否可行呢?答案是不可行,因为针对金融交易业务,宁可不可用也不愿为了可用性导致资损的发生。
3.3 镜像库方案
事实上CAP理论已经给我们指明的方向, 也就是AP只能选一个,所以我们是否可以换一种思路,一切都是trade off,没有完美的方案,只要能够保障大多数用户的可用性那么就是一个成功的方案。顺着这个思路,我们采用了镜像库的方案,如下图所示。
镜像库方案的核心思想:由于保存了主库全量的关键数据,当主库down机不可用时,可以通过备机房B的从库和镜像库,然后比对过滤找出数据不一致的部分所涉及的用户,最后禁用这一部分用户后,正式切换至备机房,从而保障其他用户的正常使用
镜像库的方案从思路上是可行的,但仍具有很大挑战,因为需要解决下面两个核心问题。
- 如何找出从库和镜像库之间不一致的数据?(禁写名单如何生成?)
- 镜像库的数据传输强依赖专线,当专线故障时如何保障镜像库和原主库的数据完整性?
3.3.1 如何找出从库和镜像库之间不一致的数据(禁写名单如何生成)
第一:我们先来想一下,先写镜像库还是先写主库,如果先写主库,那么可能会导致镜像库数据 <= 主库,很明显这是不可接受的,所以必须先写镜像库,虽然会造成禁写名单扩大,但可以规避资损风险。
第二:由于,镜像库数据需要跨机房传输,强依赖专线,如果保存全量数据,从成本和性能上对业务来说都是不可接受的,所以写入镜像库的数据必须小。
结论1:镜像库必须要比主库先写
结论2:镜像库只写关键数据
通常主库和从库之间数据不一致会对比主库和从库的gtid,通过对比gtid,解析binlog,就可以找出不一致的数据,但是由于镜像库先写,所以镜像库无法获取主库GTID的信息,那么如何在机房B,找到镜像库和从库不一致的数据呢?
举个例子:假设A给B,B给C,C给D按照时间顺序,分别转了一笔钱,我们来观察一下,主库、镜像库、从库之间的数据差异,如下图所示:
假设在T6时刻主库挂了,主库还没有执行C给D转账,这个时候主从之间不一致的数据为B给C转账的记录,所以理想下,禁写名单为B和C,但是由于主库挂了,所以数据比对只能在镜像库和从库之间,所以实际情况禁写名单为B,C,D,虽然比理想情况的禁写范围大,但是可以保障安全。
看到这里,你可能会有一个疑问,肉眼可以轻松得出禁写名单,但实际情况是从库和镜像库之间的GTID是没有任何关联的,也就从库T3时刻的事务是无法对应到镜像库的,那么如何生成禁写名单呢?
方案一:时间估算法
从图中可以看出,由于从库的数据只同步到T3时刻,所以事实上在镜像库上需要禁写T1时刻之后的所有数据,由于从库上T3时刻的GTID无法对应到镜像库T1时刻的GTID,时间估算法的核心思想是:适当扩大禁写范围,在镜像库上禁写(T3-10mins)之后的数据,10分钟可以通过配置灵活调整,如下图:
时间估算法有效的前提是从T1镜像库写入完成到T3从库同步完成的时间<10分钟,不然仍可能存在禁写名单不完整。
方案二:唯一id关联
由于无法得知从库最新的GTID对应的镜像库的记录,所以无法精准生成禁写名单,我们先来梳理下数据写入顺序,如下图所示
既然从库数据和镜像库数据无法关联,如果写镜像库的时候冗余一个唯一的id,同步到从库带上唯一ID,这样从库和镜像库数据就关联上了,基于这个思路,能否解决问题呢?如下图所示:
上图把整个数据写入按照时间顺序分为4个阶段,我们试着分析一下整个禁写名单的生成过程。
阶段1: 生成唯一ID分别为1,2,3,4,5,随后将5次请求的数据所涉及的账号、id、操作时间记录到镜像库。
阶段2:将5次请求涉及的业务数据正常写入至主库。
阶段3:将5次请求的数据所涉及的账号、id、操作时间记录到主库。
阶段4:将请求数据同步到从库,分别同步得到数据C,B,D。
禁写名单生成流程如下:
- 获取镜像库数据如下
用户 | id | 时间 |
---|---|---|
A | 1 | time1 |
B | 2 | time2 |
C | 3 | time3 |
D | 4 | time4 |
E | 5 | time5 |
- 获取从库数据如下:
用户 | id | 时间 |
---|---|---|
C | 3 | time3 |
B | 2 | time2 |
D | 4 | time4 |
对比结果,由于用户A,E的数据未同步至从库,所以最终禁写名单位A和E
结论:时间估算法方案相对简单,也有更好的性能,但在极端情况下可能存在禁写名单误差。通过ID关联,虽然能减少误差,但由于主库冗余了数据性能相对较差,整体复杂度较高。
3.3.2 当专线故障时如何保障镜像库和原主库的数据完整性?
实现容灾的前提是保障镜像库数据的完整性,当专线故障的情况下,由于写镜像库强依赖专线,导致当专线故障时,镜像库数据不完整最终导致容灾失败,如下所示:
其实要解决专线故障问题,实现的思路也比较简单,一种是提升专线的稳定性,另一种是多镜像库方案,先来看如何提升专线稳定性。
既然一条专线稳定性不够,那就多条,总之一定要将请求写入镜像库,但多条专线虽然提升了专线的稳定性,那如果镜像库挂了又该怎么办呢?一样的思路,可用性的本质是冗余,那一个镜像库不够,那就两个呗
那问题又来了,写到两个镜像库以后如何保障镜像库的数据一致性呢?事实上两个镜像库不需要数据一致,只要两个镜像库其中一个写入成功就代表成功了,最终生成禁写名单时通过镜像库B∪镜像库C获得完整的镜像库数据,从而生成最终的禁写名单。
镜像库方案整体实现复杂度较高,除了上述问题以外,我们还需要解决下面这些问题,这里就不继续展开了
- 如何判断当前是否可以安全的执行机房切换?标准是什么?
- 禁写名单生成后,应用服务器如何不停机、统一、安全的接收规则?
3.4 总结
上面介绍了镜像库方案虽然能够实现机房双活,但整体复杂度较高,涉及应用层的改动成本也不小,从镜像库方案还可以有更多的思考的延伸。
镜像库方案的本质是通过禁写名单保障大部分用户的可用性,那么采用分库、单元化等用户物理隔离的手段是否也能解决呢?
我始终认为数据层面的事情,就应该在数据层面解决,所以最优雅的方式是数据层面解决数据一致性问题,例如OceanBase或TiDB,他们都是实现了Paxos、raft等共识算法的分布式数据库,在副本数不多,同城数据要求强一致的场景下也许是一个更好的思路。
技术服务于业务,不同的技术和架构是为了更好的拟合业务的发展。
最后,集思广益,希望与我沟通讨论构建同城双活、异地多活架构中遇到的最大的挑战和难题,感谢大家的观看。