1. 跨云多活到底解决了什么问题
1.1 从一个真实的故障说起
作为某内容社区平台的SRE负责人,我这些年最怕听到的不是业务告警,而是“某家云厂商的控制台又出问题了”。这句话一出现,往往意味着所有坐落在该云上的业务都要跟着遭殃。2022年那一次影响面极广的云厂商故障,让我印象极其深刻。当时我们的大量核心接口出现超时,报警群瞬间刷屏,客服侧也开始收到大量用户反馈,社区刷不出内容、评论发不出去。我们在故障群里试着操作控制台,结果连按钮都点不动,更别提查看机器状态、调整负载均衡策略了。
那个时刻给我最直观的冲击是:单云依赖就是一台巨型“单点”。表面上你的服务可能分布在多个可用区,甚至在不同城市的机房,但只要控制面还是同一套,只要计费、配额、权限体系还挂在同一个平台上,那么云平台一抖,你再怎么说“高可用”都是空话。也正是从那次故障之后,我们内部下了一个决心:不能再把鸡蛋放在同一个篮子里,跨云多活这件事必须提上日程。
值得说明的是,跨云多活并不是一个全新的概念,做容灾的团队早就在讨论异地多活、单元化架构。但“云原生时代”给这个问题加了一层新变量:我们的基础设施已经高度容器化、服务化,应用跑在Kubernetes集群之上,依赖各种云托管组件。这种背景下做跨云多活,考验的不只是网络和数据的“怎么切”,还有编排、发布、配置、监控、流量治理这一整套体系如何在多云之间保持一致。这篇文章不聊概念,只聊方案取舍、落地过程和踩过的坑,希望对正在做或者准备做跨云多活的SRE和架构师有帮助。
1.2 为什么单云多可用区不够用
很多团队第一反应是:我们已经在单云里做了多可用区部署,机房级别故障都能扛住,为什么还要跨云?这个问题我内部也回答过很多次。多可用区确实能解决一部分物理设施故障,比如某栋机楼断电、某条光纤被挖断。但云厂商级别的故障通常不只是“机房坏了”,而是整个控制面、公共依赖、甚至某个区域的网络出口同时出问题。
举一个实际现象:云厂商一个大故障发生时,往往外网入口、对象存储、消息队列、数据库托管服务等同时出现异常,而且控制台登录不上、API也返回错误。此时就算你的应用做成了多可用区部署也无济于事,因为域名解析、证书、密钥管理、镜像仓库、监控告警这些依赖全部和云厂商绑定在一起。更隐蔽的一个问题出在“配额”和“账号”层面。云厂商把某个区域的资源配额锁死或风控误伤,也会导致你根本没办法快速扩容。
所以多可用区是“机房故障”这一层的保险,跨云多活才是“云厂商故障”这一层的保险。如果你所在的业务体量大,核心链路对可用性要求达到99.99%以上,那么多可用区大概率是不够用的。我们不能假定云厂商永远不犯错,反而要以云厂商会犯错的假设来设计系统,这是跨云多活的第一性原则。
1.3 主备切换和多活之间的取舍
在讨论跨云时,最常被拿来和多活对比的方案是跨云主备。做法也很直白:主业务跑在A云,B云只部署一套最小化环境,平时不接流量或只接少量影子流量,等A云故障时再把流量切过去。这个方案看起来很美好,成本也低,但是有两个致命问题。
一个是切换时间。传统主备架构的切换不是一键完成的,需要修改DNS解析、拉起备用环境的实例、处理积压的数据同步延迟,再等边缘节点缓存过期。整套流程跑下来,半小时甚至一小时都是正常的。而核心业务故障超过五分钟,用户体验就已经很难接受了,半小时以上的故障足以让用户流失一大截。
另一个问题是验证不充分。备用的那套环境长期不承载真实流量,配置漂移、依赖缺失、数据过期这些隐患不会被发现。真正灾难发生时,你才发现备用环境根本启动不起来,或者启动起来也连不上主库。这是主备架构最常见也最尴尬的翻车现场。
多活方案则完全不同,两个云上的系统都真实承载流量,业务逻辑和数据同步每天都处于运行状态。切换不再是“启动一套死系统”,而是“调流量比例”这种日常操作,每次发布和扩容都在验证这套系统的可用性。代价也很清楚:成本约等于翻倍,架构复杂度更高,需要专门的团队持续运营。但从可用性角度来看,多活是唯一能让人睡得着觉的方案。
2. 整体设计思路与架构拆解
2.1 流量接入层的多活设计
跨云多活的第一个环节是流量入口。用户请求先进入哪个云,这个决策必须牢牢掌握在自己手里,而不是让某个单一厂商替你决定。我们在做接入层设计的时候,核心思路是:DNS解析、HTTPDNS、全局负载均衡、四层负载、七层负载,每一层都要具备跨云调度能力。
最简单也最基础的一层是DNS。通过在多个云厂商分别配置权威DNS和HTTPDNS,解析结果可以按地域、按权重指向A云或B云的入口IP。这里要特别注意TTL的取值。常规域名解析的TTL可以设到300秒甚至更长,但容灾场景下TTL设太久会导致切流量后用户长时间指向故障机房。我们最终把核心域名的解析TTL降到了10到30秒,同时用HTTPDNS在端侧绕过运营商的LocalDNS缓存,这样切换的生效时间能控制在分钟左右。
再往下一层,是入口机和负载均衡层面的问题。每个云都有各自的负载均衡产品,但它们的健康检查机制、延迟探测路径和IP资源都独立。实际落地时,我们更推荐在每个云入口之上再架一层自建的流量调度网关,用一个统一入口做多云流量比例的控制。这个网关可以是一个无状态的接入集群,通过消息或配置中心接收切换指令,再按照权重把流量转发到对应云的后端服务。这样一来,无论底层是哪家云的负载均衡,上层切换配置的语义都是一致的。
接入层的切换一定要常态化演练。我们最初做全量切换演练时,总会发现边缘节点缓存、运营商DNS劫持、HTTPDNS失效等意外情况。只有平时不断调整,才能在真实灾难发生时不慌不忙。
2.2 应用层无状态化是前提
跨云多活对应用层有个很硬的要求:服务必须无状态。如果你的服务里存了本地会话、本地缓存、或者把临时文件写到了宿主机磁盘,那这些状态就会把服务焊死在某一个云上,跨云切流时只能眼睁睁看着这一部分流量失败。
我们做了几件非常关键的事。第一,把Session从本地内存迁到集中式的Redis集群,并且使用跨云多活架构,让A云和B云的应用实例都能访问到同一份会话数据。第二,把所有上传文件统一迁移到支持多云访问的对象存储,并且用自定义域名走CDN回源,避免文件读取和写入绑定某一朵云。第三,应用日志采集以后直接发送到消息队列,再由独立的采集器写入统一存储,不在本地保留长期日志。
做完这几点以后,应用层才能真正看成“一批无状态的进程”。哪个云负载高了,流量调度过去就可以了;某个云挂了,所有实例被杀掉,剩下云上的实例继续对外服务,不会因为状态遗留导致业务行为异常。这块工作说起来简单,真正落地时牵扯的改造特别多,尤其是老业务、内部系统、定时任务这些历史包袱,要花大量精力去梳理和迁移。
无状态化完成之后,还有一个容易忽略的治理点:服务间调用。跨云多活要求任何一个云内的服务实例都可以调用另一个云内的服务实例,所以注册中心、配置中心也必须做成多活。我们最终的做法是在两个云各部署一套注册中心集群,通过跨云专线做注册数据的双向同步,同时服务发现时优先调用本云实例,本云实例异常时再跨云调用,这样既保证了性能,又保证了容灾能力。
2.3 数据层的多活难题与解法
数据层是所有跨云多活方案里最复杂、也最决定成败的部分。想象一下,用户在A云写入一条评论,评论的主数据库在A云;如果B云也有用户写入,那么两边可能同时出现互相冲突的数据。业界对这块有不同的解法,我们最终走的是“单元化 + 双向同步 + 冲突仲裁”的路子。
单元化的意思是按用户维度对数据做切分。我们把整个用户池按照某个维度(比如用户ID哈希、租户ID)划分成若干个逻辑单元,每一个单元的数据主副本固定落在某一个云上。比如ID哈希取模后偶数落在A云,奇数落在B云。用户请求入口会先做一次路由判断,命中哪个单元就把请求转发到对应云。数据只在所属单元对应的云上写入,再通过跨云同步把数据复制到另一个云。这样设计的好处是写写入冲突概率大大降低,两个云之间的数据做到近乎实时同步。
同步链路我们以MySQL为例,主要用binlog订阅的方式,把主集群的变更实时转发到对端云的从集群。消息队列和缓存数据的同步也类似,需要做序列化、幂等和延迟兜底。这里最难的不是实时同步本身,而是故障切换时如何保证“正在写入的数据不丢”。我们采用了一个比较土但很有效的方案:切换前先切入口流量,让某一云不再接收新写入,等待同步追平,再切数据库主导权。整个过程配合监控和自动化脚本来完成,人工只负责确认状态和下达指令。
冲突处理是另一个绕不开的坎。现实中总会出现网络分区导致的脑裂,两边同时写同一条数据。我们的策略是引入全局唯一的版本号生成服务,每条数据写入时带着版本号和来源标识,同步时按版本号新旧决定保留哪一份。仲裁规则必须在切换预案里提前定义好,否则两边各执一词,恢复数据时谁都说服不了谁。这一块没有银弹,只能根据自己的业务属性定规则,比如订单场景以支付时间为准,评论场景以创建时间为准,养成数据的核心目标是“不要产生脏数据”。
3. 核心环节的实操要点
3.1 多集群资源和服务的编排
跨云多活落地时,第一步通常是搭建两套Kubernetes集群,分别跑在A云和B云。这里第一个实操要点是集群版本和插件版本必须保持一致,否则同一套Deployment在两边产生的行为可能存在细微差别。我们内部规定所有集群的K8s版本升级必须在两边同一时间完成,避免出现“A云是1.26,B云是1.28”这种互相兼容性不确定的状态。
第二点是资源编排要用GitOps方式统一管理。我们把所有应用清单、命名空间、配置项都放到同一个Git仓库,通过自动化流水线推送到两套集群。任何人对配置的修改都必须走Merge Request,经过评审后由Pipeline自动同步。这一点极大避免了配置漂移,也保证了“两朵云上的部署产物完全一致”这个多活的核心前提。我们最开始尝试过手动在两套集群分别apply配置,效果很差,总会有某边漏掉一个环境变量或者忘记更新镜像版本,后来才彻底切到GitOps模式。
第三点是灰度发布和版本一致性。跨云多活并不意味着两边必须时刻运行同一个版本,但我们要求发布时先在一个云发布,验证稳定后再发布另一个云。这个顺序很重要,因为如果两朵云同时发布新版本并且出了问题,你根本无法判断是代码问题还是基础设施差异导致的。我们把跨云发布节奏定义为“先A后B,间隔观察十分钟”,既保证了操作安全性,也能在出现问题时快速定位是某朵云的差异还是业务代码本身的bug。
资源编排还有一个容易踩坑的地方:配额和限额。K8s集群的节点池、GPU资源、存储类、LoadBalancer数量,在不同云的配额限制是完全不同的。如果某个云资源配额过低,切换流量时扩容可能失败。我们专门做了容量巡检脚本,每天拉取两朵云的已用配额和剩余配额,低于阈值就告警,防止灾难发生时才发现扩容不上去。
3.2 数据同步与冲突处理
数据同步链路的设计直接决定了跨云多活的RPO(恢复点目标)。我们内部的目标是正常情况下主备云数据延迟不超过三秒,切换时RPO控制在十秒以内。为了达到这个目标,有几个实操细节必须注意。
MySQL同步链路我们用的是binlog实时订阅加回放。在源端开启GTID模式,追踪每个事务的位点,通过跨云专线推送到目标端。这里第一个坑是专线质量。如果两个云之间的专线抖动大,同步位点很容易堆积,延迟一高,切换时就会丢数据。我们做了断线补偿和位点校准机制,同时利用多个专线隧道做冗余,避免单链路故障导致同步中断。
另一个坑是DDL同步。很多团队只同步DML数据,忽略了表结构变更。一旦源端执行了一条ALTER TABLE,目标端没有同步执行,后续所有变更都会失败。我们最终选择直接同步所有DDL操作,并且在执行前进行兼容性检查和备份。每一次表结构变更都要求做成独立的迁移脚本,由自动化平台统一在两端执行,不允许任何人手动连接数据库改表。
对于缓存和消息队列这类中间件,多活同步的思路不一样。缓存我们采用双写策略,写操作同时写两个云的Redis集群,读取时优先读本云,本云未命中再跨云读取并回填。消息队列则需要消费侧具备幂等能力,因为跨云同步和重复投递可能导致同一条消息被消费多次。所有消费者必须通过唯一消息ID做去重,确保业务结果一致。
冲突处理这块,我们的经验是提前定义好每类业务的“主版本规则”。比如用户资料修改,以最后更新时间为准;订单状态流转,以状态机当前状态为准。每种规则都要有兜底记录,出现无法自动仲裁的冲突时,进入死信队列由人工介入处理,绝不能让冲突数据静默覆盖。
3.3 切换与容灾演练怎么做
切换是多活系统最核心的动作,也是最容易出事的环节。我们内部把切换动作拆成三层:流量切换、数据切换、依赖切换。流量切换负责把用户请求按权重从A云转到B云;数据切换负责把主导写权限从A云数据库切换到B云数据库;依赖切换负责将消息队列、对象存储、配置中心等云托管服务的读写路径切换到目标云。
演练最重要的是形成标准化流程脚本,而不是依赖人脑判断。我们每季度做一次全量切换演练,每两周做一次半自动小流量切换。演练脚本会明确每一步的触发条件、预期结果、失败回滚方案。比如流量切换时,先调10%流量到B云,观察错误率和延迟五分钟后,再逐步递增到50%、100%。每调一次,系统自动把监控面板推到指定群,负责人看图确认没问题后继续下一步。
红蓝对抗和混沌工程也很必要。我们会在演练过程中随机注入故障,比如拔掉某朵云的出口带宽、切断专线、人为设置数据库同步延迟。这样做的好处是能让切换预案在真实故障形态下被检验,避免“演习很顺利、实战全拉胯”。
演练记录要统计两个关键指标:切换耗时和人工干预次数。理想状态下切换耗时应随演练次数下降,人工干预次数也应逐渐趋近于零。如果某一次演练耗时反而变长,一定说明流程或自动化存在退化,需要立刻复盘。要多想一步:切换脚本本身也是代码,也会有bug,所以切换脚本必须版本化管理,并且经过测试才能进入生产环境。
4. 真实故障切换中的问题排查实录
4.1 脑裂问题的现场
跨云多活中我最不想遇到但又一定会遇到的问题就是脑裂。所谓脑裂,指的是两个云之间的网络链路或同步链路中断,但两边的系统都还活着,都认为自己是“主”。这时候如果两边都在接收写入,数据就会分叉,后面怎么合并都很痛苦。
我们遇到过一次典型的脑裂场景:某次跨云专线故障导致A云和B云之间的数据同步中断了大约十分钟。同步中断期间,两边都按照路由规则接收了自己单元对应的写入。由于我们做了单元化,大部分用户的数据主副本仍然只在一个云上,所以整体影响没有扩大。但问题出现在那些命中到故障单元的请求,明明主副本在A云,由于网络不通,B云侧的重试机制把请求本地化处理了。这部分数据变成了脏数据,后面同步恢复后产生了大量冲突。
排查脑裂问题时,第一件事不是急着恢复同步,而是先做“故障隔离”。把两边入口的写流量同时停掉,或者至少把非主单元的写请求全部拦截,避免新的分叉产生。然后检查同步断点,确定A云和B云各自多写了哪些数据,逐条评估冲突情况。修复过程没法省略,需要在业务侧找到对应单据,以预定义规则仲裁保留副本。脑裂之后最忌讳的就是直接恢复同步,让两边数据自动覆盖,那等于把冲突错误隐藏到业务底层。
事后反思,我们增加了仲裁节点。两朵云之外单独部署一套轻量级的仲裁服务,周期性检测两边的心跳和同步状态。一旦发现两个云无法互相感知对方存活,仲裁节点会主动下线其中一边的写权限。虽然不能完全避免脑裂,但能把脑裂窗口从十分钟缩短到几秒。
4.2 数据回切的坑
很多人以为跨云切换最危险的是切过去的那一刻,其实回切才是最考验耐心的。曾经有一次我们完成了从A云到B云的切换,业务恢复了正常,大家都很开心。但评估后觉得B云容量不如A云,决定半天之内切回A云。结果回切时发现问题远比想象中复杂:B云在故障切换期间累积了大量新的写入数据,而这些数据还没有完全同步回A云。
回切的标准动作必须和“正向切换”一样严谨。首先再次切入口流量到目标云,然后等旧云的数据和新云完成追平,并且连续观察一段时间没有延迟,才能把数据库主写权限转移回去。我们当时就是因为太着急,在数据还有几万条没追平时就开始了回切,结果部分用户的数据出现了短暂的“漂移”——在A云能看到数据,在B云却看不到。
回切还有一个容易被忽略的问题:幂等冲突。B云在切换期间写入了一些新的评论,这些评论的ID范围和A云原本的ID范围可能重叠。同步回A云时,A云数据库里已经存在相同主键的记录,导致写入失败。我们后来把所有业务表的主键生成方式统一改成雪花ID或者独立发号器,从源头避免跨云回切时的主键冲突。如果你们还在用数据库自增主键做跨云同步,回切时一定会踩坑,趁早改掉。
回切时机的判断我们总结了一个简单标准:延迟时间连续十分钟小于三秒,且两个方向的同步位点之差稳定收敛。同时业务低峰期操作,预留两小时的回切窗口,一旦中途出现不可预期的问题,立刻暂停回切而不是强行推进。
4.3 演练比故障更费心的几个环节
做跨云多活几年下来,我的一个体会是演练比真实故障更费心。因为真实故障是不知不觉发生的,你只需要按照预案执行;演练则是主动在一套健康的系统上制造破坏,稍有不慎就可能引发真故障。
演练中最容易出问题的是流量比例的调节。跨云调流量时,我们依靠网关按百分比转发,这个百分比是按连接数统计的。小流量阶段比如1%,量很小,错误率指标波动可能被噪声淹没。我们就把健康判断阈值设得更保守,不看单一指标,而是同时观察错误率、TP99延迟、依赖组件耗时和客户端上报数据四个维度。任何一项出现异常,都先暂停增量调整,而不是继续加大流量。
第二个坑是演练时间选择。虽然内部演练通常在凌晨低峰期进行,但跨云多活涉及的服务和依赖非常多,总有一些跨境、异地或第三方依赖在凌晨反而有定时任务高峰。我们所有正式演练前必须查看全局任务清单,确认没有和大流量任务、数据备份任务、财务对账任务重叠。曾经就因为没看日历,演练时正好撞上每月一次的会员结算批量任务,结果切流量后批量任务全部超时,差点造成资损。
演练之后的“清理”也很关键。每次演练会产生大量的切换记录、临时日志、测试数据,如果不清干净,留着这些脏东西会干扰下一次演练的基线数据。我们规定演练结束后必须跑一遍数据比对脚本,把测试产生的多余数据删除,并且重新校准两朵云的容量水位。
5. SRE视角的经验沉淀
5.1 多活系统最容易被忽视的成本
做跨云多活,最简单的算法就是所有资源乘以二。计算引擎要双倍、存储要双倍、带宽要双倍,人力更是长期的持续投入。我见过很多团队一开始雄心勃勃,做到一半发现成本太高,然后就开始偷工减料:数据同步链路只建一条不建冗余,灾备云的环境不保持和主云一致,或者减少演练频次。这些做法短期看起来省钱,实际都是为下一次故障埋雷。
跨云多活真正贵的不是机器资源,而是运营成本。你需要一套覆盖跨云的监控体系,需要专门的工程师维护集群一致性,需要定期做演练和复盘,还需要处理多云之间的安全加固和合规审计。我们内部有个原则:如果一个季度内没有发生过任何跨云切换或者演练,那么这套多活系统的可靠性一定在下降。因为系统在持续变化,配置在漂移,代码在变更,只有不断用真实的动作去激活它,它才算活着。
如果预算有限,我建议采用分层多活的策略。核心链路(登录、读内容、发评论、订单)做完整的多活;非核心链路(数据分析、后台任务、日志处理)保持单云部署,故障时允许短暂降级。这样能在成本和可用性之间取得一个比较务实的平衡。
5.2 让故障切换从“艺术”变成“肌肉记忆”
跨云多活体系跑起来以后,我最大的感受是:切换不应该依赖某个“大神”,而应该变成一套任何人照着执行都能成功的流水线。SRE团队的目标是把故障切换从一门依赖经验的艺术,变成全自动化的常规操作。
我们最终沉淀了一套完整的故障响应手册,每个角色、每个动作、每个判断标准都写清楚。值班SRE接到告警后,先对照手册确认影响范围,再自动触发告警聚合和定界脚本,然后按流程执行切换。所有切换动作尽量通过自动化平台完成,人工只需要确认和下单,系统再自动执行。只有自动化无法覆盖的异常场景,才需要人工介入。
自动化程度越高的系统,越需要持续验证自动化本身正确。我们每个月都会做一次核心链路的故障注入测试,验证监控告警是否正常触发、脚本是否能够正确执行、回滚是否干净利落。如果某个步骤被跳过或者脚本执行异常,就说明系统在退化,需要立刻修复。
跨云多活不是一次性交付的项目,而是需要长期运营的基础设施能力。它不会让业务飞黄腾达,但能在最坏的时刻,让平台平静地扛过去。用户不会因为多活架构点赞,但故障时他们不会大面积流失,这就是SRE做这件事的全部意义。
最后分享一个小技巧:在所有切换脚本和容灾工具里,一定要给每个按钮加二次确认和可回滚路径。哪怕你对自己的自动化再有信心,也要保留一个“人肉退路”。我见过太多自动化工具在紧急关头因为一个小参数没传对,导致整个切换失败。真正可靠的系统,永远留有被人接手的机会。