故障频发,同城双活如何构建?

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等共识算法的分布式数据库,在副本数不多,同城数据要求强一致的场景下也许是一个更好的思路。

技术服务于业务,不同的技术和架构是为了更好的拟合业务的发展。

最后,集思广益,希望与我沟通讨论构建同城双活、异地多活架构中遇到的最大的挑战和难题,感谢大家的观看。

相关推荐
红尘散仙27 分钟前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记2 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
_codemonster2 小时前
30分钟快速搭建 Spring Cloud Alibaba 微服务实战(一)
微服务·架构·毕业设计·课程设计
会编程的土豆2 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
Cosolar2 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
喵个咪2 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6163 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364573 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao3 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
qcx234 小时前
【系统学AI】09 Multi-Agent架构(2026版):从学术理论到工业级实践
java·人工智能·架构·multi-agent·claude agent