1. 三地五中心的真实画像:为什么是五个而不是三个
三地五中心、LDC(逻辑数据中心)、单元化、容灾切换,这四个词单独拎出来都能讲一天,但真正把它们串成一条线的人不多。我在交易系统基础设施这条线上摸爬滚打了好几年,参与过两轮异地多活改造,踩过的坑足够写一本小册子。这篇就把我理解的这套体系完整摊开讲一遍——它解决的是什么问题、架构为什么这么长、LDC 和单元化到底是什么关系、容灾切换的标准方案该怎么设计。不管你是刚接手多活项目的同学,还是已经在写切换脚本的老兵,都应该能从里面找到能直接用的东西。
先说清楚这套东西的适用场景:日订单量上百万、可用性指标卡在 99.99% 以上、监管或业务明确要求"单机房整体失效不影响对外服务"的系统。如果你还在单机房跑、日请求量几万,那这套架构对你是过度设计,看完当知识储备就好,别硬上,硬上的代价是运维复杂度直接翻三倍。
1.1 从同城双活到异地多活的演进逻辑
大部分团队的容灾建设是有清晰台阶的。第一级是冷备,主机房跑业务,备机房只放数据库副本,真出事靠人工拉起来,RTO 按天算。第二级是同城双活,两个机房在同一个城市,光纤直连,RTT 一般 1ms 以内,做数据库同步复制完全没问题,RPO 可以做到 0,但这两个机房共享同一套城市级基础设施——电力、骨干网、城市级故障面前它们是同一个篮子里。
第三级就是异地多活,机房分散到不同城市,机房之间的 RTT 跳到 20~40ms,同步复制开始变得昂贵,于是有了单元化和 LDC 这套东西。你要理解一点:异地多活不是为了"更快",而是为了"出事了还能活"。同城双活解决的是机房级故障,异地多活解决的是城市级故障。这个目标差异决定了后面所有设计的取向。
1.2 三地五中心的物理布局与成本账
"三地五中心"字面意思是三个城市、五个数据中心。我见过的典型形态是这样的:主城市放两个中心,构成同城双活的主 LDC,承载绝大部分在线流量;第二城市放两个中心,作为异地单元承接一部分真实流量(注意,是真实流量,不是空转的冷备);第三城市放一个中心,体量最小,通常承担仲裁、全局配置、数据备份或者离线分析这类职责。
为什么第三个城市只放一个中心?因为它的定位是"仲裁者"和"最后一道数据防线",不需要承载大规模在线流量,两个中心反而浪费预算。这里有个真实的成本账:一个中等规模的数据中心,机柜、带宽、电力、运维人力加在一起,年成本量级在千万级。五个中心意味着五份,所以这套架构基本只出现在头部体量的业务里。中小团队如果要做,务实的做法是"两地三中心"起步,先把单元化和切换流程跑通,再考虑扩第三个城市。
还有一个容易被忽略的点:五个中心不代表五份对等的算力。实际配比可能是 4:4:1:0.5:0.5 这种。第三城市的那个中心,往往算力很小,但它的存在让"多数派仲裁"成为可能——分布式一致性协议需要奇数个投票节点,三个城市天然满足这个条件。
注意:三地五中心的地理选择有硬约束,城市之间的距离不能太近(否则一次区域性灾害全覆盖),也不能太远(RTT 超过 50ms 后,跨单元同步的延迟会拖垮用户体验)。行业里常见的距离区间是 800~1500 公里。
1.3 名词先对齐:LDC、单元、Cell、GZone 到底指什么
我见过太多人把这些词混着用,沟通时鸡同鸭讲。用我的理解给你捋一遍。
LDC(Logical Data Center,逻辑数据中心),本质是给物理机房做了一层逻辑抽象。一个 LDC 可以对应一个物理机房,也可以对应一组机房。它的价值在于:上层业务只认 LDC 这个名字,不关心底层机房怎么摆,扩容、迁移、替换物理设备时上层无感。你可以把 LDC 理解成"给机房起的业务别名"。
单元(Unit / Cell / Set),是流量和数据的最小闭环单位。一个单元里包含完整的计算、存储、缓存、消息能力,能独立处理一部分用户的完整请求。单元是横向切分的,每个单元处理一部分用户;LDC 是纵向打包的,一个 LDC 里可以跑多个单元。
GZone(Global Zone,全局单元),承载那些没法按用户切分的东西:全局唯一 ID 生成器、全局配置、跨单元的清结算、全局风控规则。GZone 通常只部署在一个地方,或者用主备方式部署,它的可用性要求比普通单元更高。
RZone(Route Zone,路由单元),无状态的路由层,负责算一个用户应该落到哪个单元。用户登录、下单、查询,第一步都要过 RZone 拿路由结果。
CZone(Center Zone,中心单元),是过渡期的产物,承载那些还没完成单元化改造的存量业务和存量数据。单元化不是一天做完的,CZone 就是给还没搬家的业务留的临时住所,但它也是切换时最大的风险点——后面会细说。
2. 单元化架构拆解:把爆炸半径关进笼子
单元化这个词被说得太玄乎了,其实它的目标非常朴素:让一次故障的影响范围局限在一个单元内,而不是全站。假设你有 20 个单元,一个单元挂了,理论上只损失 5% 的流量,而不是 100%。这就是"爆炸半径"的概念。但要做到这一点,代价是架构复杂度上升,因为你必须保证单元之间不互相依赖——这句话说起来简单,做起来是全套中间件的改造。
2.1 单元化必须守住的三条铁律
第一条,单元封闭。一个用户的读写请求,从入口到数据库,全程必须落在同一个单元内,不允许出现"入口在 A 单元、数据库在 B 单元"的情况。一旦出现跨单元调用,故障隔离就失效了,A 单元挂掉会把 B 单元一起拖死。
第二条,流量收敛。用户的流量要能被稳定地导向它所属的单元。这意味着路由结果必须持久化,不能每次请求重新算——重新算意味着算错了用户就被分到不同单元,数据就乱了。常见做法是把路由结果写进用户会话或者独立的映射表,路由层只做查表。
第三条,同单元内闭环。单元内的服务、缓存、消息、数据库必须自成体系。特别是缓存和消息队列,很多团队的缓存是全局共享的,一个单元写脏了所有单元跟着遭殃。正确做法是缓存集群按单元划分,消息 Topic 按单元隔离,跨单元的消息走专门的同步通道。
这三条听起来是常识,但我在实际项目里见过的违反案例一大堆:有团队为了省事把全局配置放在一个共享的 Redis 里,结果那个 Redis 挂了全站不可用;有团队的分布式事务协调器是全局单点,切换时第一个挂。
2.2 单元划分维度的选择与权衡
单元按什么维度切,是整个设计里最关键的决定。行业里主流有三种:
| 划分维度 | 适用场景 | 优势 | 代价 |
|---|---|---|---|
| 用户 ID | C 端业务、交易、社交 | 天然正交,扩容平滑 | 需要全链路透传用户 ID |
| 商户/租户 ID | B 端 SaaS、电商平台 | 数据隔离天然清晰 | 大商户会形成热点单元 |
| 地理位置 | 本地生活、区域业务 | 就近接入延迟低 | 用户迁移时会跨单元 |
我参与的项目选的是用户 ID 取模,这是最通用的方案。具体做法是用用户 ID 的后几位做哈希,映射到固定数量的槽位,槽位再进一步映射到单元。为什么不直接把用户 ID 映射到单元?因为单元数量会变,直接映射的话扩容时几乎全部用户都要迁移。用固定槽位做中间层,扩容时只需要迁移部分槽位,迁移成本可控。
这里有个我踩过的坑:取模的基数一定要选得足够大。我们第一版用了 64 个槽位,后来业务量涨了要扩到 128 个单元,槽位不够用了,只能重做映射,迁移了整整两个月。后来改成 1024 个槽位,扩容时只需要把槽位重新分配,用户几乎无感。槽位数量建议是预期最大单元数的 4~8 倍。
2.3 路由分层:RZone、GZone、CZone 的职责划分
路由不是一层能搞定的,实际生产环境里是分层路由。我画不出图(这里也不用图),用文字给你讲清楚链路。
用户请求进来,第一站是接入层,接入层做两件事:识别用户身份、拿到用户的路由标识(比如槽位号)。第二站是 RZone,RZone 部署在多个地方,无状态,它拿着路由标识去查映射表,返回目标单元。第三站才真正落到目标单元的业务服务上。
GZone 的介入点是那些"不属于任何单一用户"的请求:比如查询全局排行榜、发起跨单元转账。这类请求先在 RZone 判断类型,如果是全局类请求就打到 GZone,由 GZone 内部再去调用各个单元的服务。GZone 是全局单点,所以它的容量要预留得足够,切换时它也是重点保护对象。
CZone 的介入点是那些"还没完成单元化"的业务。请求经过路由时,如果发现目标业务还在 CZone,就直接落到 CZone。理想情况下 CZone 的业务量应该随着改造推进逐渐归零,但现实里总有那么几个顽固的存量系统赖着不走。我的建议是给 CZone 的业务设一个明确的退出时间表,每季度review一次,不然它会变成永久的技术债。
2.4 数据分片与跨单元一致性的处理办法
数据是单元化里最难的部分。按用户切分的数据,天然可以分到各单元,这部分好办。难的是那些跨用户的数据关系:比如 A 用户给 B 用户转账,A 在单元 1、B 在单元 2,这笔账怎么记?
行业里主流有三种处理方式。第一种是异步补偿,先在 A 单元扣钱、记一条待处理流水,然后异步通知 B 单元加钱,失败就重试。这种方式最终一致,实现简单,但用户体验上会有一小段"钱扣了对方没收到"的窗口。第二种是TCC 事务,Try 阶段两边都预占资源,Confirm 阶段一起提交,Cancel 阶段一起回滚。一致性更强,但代码侵入大,每个业务都要写三套逻辑。第三种是中心化处理,所有跨单元交易都路由到 GZone,在 GZone 里用一个统一的账户体系处理。这种方式最简单直接,但 GZone 会成为瓶颈。
我们最终选的是异步补偿为主、GZone 兜底为辅的组合。绝大多数跨单元交易走异步补偿,只有金额超过阈值或者风控敏感的交易才路由到 GZone。这个阈值是可以动态调整的,切换期间会把阈值调低,让更多交易走 GZone,牺牲一点性能换稳定性。
还有一个细节:单元内的数据库主从切换。每个单元内一般是"一主两从"的配置,主库写、从库读。从库的读取延迟在正常情况是毫秒级,但在压力大时会拉长,导致"刚下单查不到订单"的诡异现象。我们后来给关键业务加了"写后读主"的逻辑,写完之后的第一次查询强制走主库,第二次开始才走从库。这个改动让客诉少了八成。
3. LDC 落地实操:从机房到逻辑数据中心的抽象
单元化讲的是横向切分,LDC 讲的是纵向打包。这两件事必须一起做,否则单元切好了但没地方放。LDC 落地的核心工作量不在业务代码,而在元数据管理——你要有一套统一的、可靠的、所有组件都认的数据,来描述"哪些单元在哪个 LDC 里、每个单元的状态是什么"。这套元数据是切换时的决策依据,它错了,切换就会切错。
3.1 LDC 的元数据模型怎么建
我们的元数据模型长这样,字段不多但每个都很关键:
LDC 表:ldc_id, ldc_name, region, status, capacity_weight 单元表:unit_id, unit_name, ldc_id, slot_range, status, version 路由表:user_hash, slot_id, unit_id, effective_time看懂这四个字段的含义,你就理解了整个体系。status表示 LDC 或单元的当前状态(在线、只读、下线、维护中),slot_range表示这个单元负责哪些槽位,version是版本号用于灰度发布。
元数据本身的存储是个讲究活。它不能存在业务数据库里,因为业务数据库可能挂了;也不能存在单个 Redis 里,那是单点。我们的做法是存到一个独立的、跨三地同步的配置中心,每个 LDC 都有一份完整副本,读取走本地副本,写入走主副本然后异步同步。这样即使某个 LDC 的网络断了,它也能读到元数据,只是读到的是稍旧版本。
注意:元数据的版本管理要做"单调递增"。切换过程中会频繁修改元数据,如果版本号回退了,各个组件可能读到不同的值,直接切错。我们的做法是每次修改版本号 +1,所有组件都拒绝比自己记录的版本更旧的元数据。
3.2 数据库单元化的改造路径
数据库改造是整个项目里最耗时的部分,我估计占了总工期的 60%。改造分三步走。
第一步是分库分表,在主库里把按用户切分的数据拆开,物理上还是一个库,逻辑上已经分片。这一步的收益是让数据模型先对齐单元切分逻辑,为后续的物理拆分铺路。
第二步是单元内独立部署,把每个单元的数据拆到独立的数据库实例上,这些实例部署在各自的 LDC 里。这一步完成后,单元内的读写就完全闭环了。这步的风险在于数据迁移,我们用了双写+校验+切读+切写的四阶段方案,每个阶段之间观察至少一周。
第三步是跨单元只读通道,有些业务确实需要查别的单元的数据,比如客服系统要查所有用户的订单。我们给这类场景开了一条只读通道:每个单元的从库把数据同步到一个专门的查询集群,只读请求打到查询集群。这样既满足了查询需求,又不破坏单元封闭性。
第三步的坑在于同步延迟。查询集群的数据总是比单元内的数据旧一点,客服查到的订单状态可能不是最新的。我们的处理方式是在查询结果上标注"数据可能存在延迟",并给客服系统做了强制刷新按钮,按需从单元内主库拉最新数据。
3.3 中间件改造清单与优先级
中间件改造是最容易被低估的部分,因为它涉及的东西太多了。我列一个清单,按优先级排序,你可以照着自己的系统对一遍:
| 中间件 | 改造要点 | 优先级 | 改造周期(参考) |
|---|---|---|---|
| 服务注册发现 | 服务实例按单元打标,支持按单元路由 | P0 | 2~4 周 |
| RPC 框架 | 支持单元内优先调用,跨单元需显式声明 | P0 | 3~6 周 |
| 消息队列 | Topic 按单元隔离,跨单元走同步通道 | P0 | 2~4 周 |
| 分布式缓存 | 集群按单元划分,禁止全局共享写 | P0 | 2~3 周 |
| 配置中心 | 支持按单元、按 LDC 的差异化配置 | P1 | 3~5 周 |
| 分布式事务 | 支持单元内事务,跨单元降级为最终一致 | P1 | 4~8 周 |
| 全局 ID 生成 | ID 中嵌入单元位,保证单元内可解析 | P1 | 2~3 周 |
| 定时任务 | 任务按单元隔离,防止多单元重复执行 | P1 | 2~4 周 |
这个表是我根据实际项目经验估的,不同团队差异会很大。重点说两个容易翻车的:
消息队列。很多团队用的是所有单元共享一套消息集群,觉得这样省资源。但共享意味着一个单元发送消息失败会阻塞其他单元,而且消息的顺序性在跨单元时基本没法保证。正确做法是每个单元一套独立的消息集群,跨单元的消息通过一个专门的桥接服务同步。桥接服务要做幂等,因为消息可能会重复投递。
定时任务。这个是小坑但很致命。单元化之后,如果定时任务没做隔离,每个单元都会执行一遍,导致数据重复处理。我们的处理方式是在任务调度器里加上单元判断:只有主单元或者指定的任务单元才执行全量任务,其他单元只执行本单元内的任务。这个改造看着简单,但我们上线时漏了两个任务,跑了一晚上才被发现。
3.4 单元化上线的灰度节奏
单元化改造不能一把梭,必须灰度。我们用的节奏是"先读后写、先旁路后主链路"。
第一周,旁路校验。新老逻辑同时跑,新逻辑的结果只记录不生效,比对两者的差异。这一步能发现 80% 的逻辑问题。
第二周,读流量切换。把读请求切到单元化后的链路上,写还走老的。读流量可以随时切回,风险可控。
第三周开始,写流量按比例灰度。从 1% 到 10% 到 50% 到 100%,每个档位观察 3~5 天。观察指标包括:错误率、P99 延迟、跨单元调用比例、数据库连接数。
最后是全量切换,完成后再观察两周,确认没有异常才把老链路下线。
有个经验值得分享:灰度比例不要按百分比拍脑袋定,要按业务重要性分层。低风险的查询类业务可以直接 50% 起步,涉及资金的核心交易最好从 1% 起,而且要挑非高峰时段。我们有一次在高并发时段切了 10% 的支付流量,结果单元内的数据库连接池没配够,直接打满了,回滚花了十分钟。
4. 容灾切换标准设计方案:把切换当成一次发布
前面三章讲的是"平时怎么建",这一章讲"出事时怎么切"。容灾切换的标准设计方案是我最想展开的部分,因为它最容易被做成"纸上预案"——文档写得漂漂亮亮,真出事时没人敢按那个按钮。核心问题是:切换的本质是一次有计划的高风险变更,它应该像发布一样有流程、有灰度、有回滚。
4.1 RTO/RPO 指标怎么定,附计算过程
RTO(恢复时间目标)和 RPO(恢复点目标)是所有容灾设计的目标函数,但很多人定指标时是拍脑袋的。我给你一个可以推演的计算方法。
先说RTO,它是从故障发生到业务恢复的总时间,拆开来看:
RTO = 故障检测时间 + 决策时间 + 流量切换时间 + 数据追平时间 + 业务验证时间代入一组我们的实测值:故障检测 30 秒(监控告警的收敛时间)、决策 60 秒(值班长判断并上报)、流量切换 120 秒(DNS 和网关配置生效)、数据追平 60 秒、业务验证 60 秒,合计330 秒,约 5.5 分钟。这组数字看起来很漂亮,但它是理想值,实际演练里第一次达标率不到三成,问题几乎都出在"决策"和"验证"这两个环节。
再说RPO,它衡量的是最多丢多少数据。同步复制场景下 RPO = 0,但代价是写延迟增加一个 RTT。异步复制场景下:
RPO = 数据同步链路的总延迟 = 网络 RTT + 批量提交窗口 + 应用侧缓冲假设机房 A 到机房 B 的 RTT 是 20ms,批量提交窗口设了 50ms,应用侧缓冲 10ms,那么 RPO 大约是 80ms。按峰值 5 万 TPS、单条记录 1KB 估算,80ms 对应的数据量是 4000 条、约 4MB。这就是"最坏情况下会丢多少"的量化答案。
注意:RTO/RPO 是要写进 SLA 对外承诺的,所以宁可定得保守。我见过团队对外承诺 RTO 5 分钟,实际演练 20 分钟都切不完,真出事时会非常被动。
4.2 切换场景分类与触发条件
不是所有故障都需要切,切换场景要分类,不同场景触发条件不同。
| 场景 | 触发条件 | 切换范围 | 典型 RTO |
|---|---|---|---|
| 单实例故障 | 单台机器/单个容器异常 | 无感,自动摘除 | 秒级 |
| 单元级故障 | 整个单元的中间件或数据库不可用 | 单元内流量迁移 | 1~3 分钟 |
| 机房级故障 | LDC 整体失联或机房级电力/网络故障 | LDC 间切换 | 5~15 分钟 |
| 城市级故障 | 区域性灾害导致同城多机房受影响 | 跨地域切换 | 15~60 分钟 |
| 计划性切换 | 演练、割接、机房门禁升级 | 按计划执行 | 可控 |
关键区别在于是不是要人工决策。单实例故障和单元级故障可以全自动处理,脚本发现异常直接摘流量。机房级和城市级故障必须有人工确认,因为误判的代价太大——你切走了,发现其实是监控本身挂了,白切一场还引入了不必要的风险。
我们给机房级切换定了三条硬性触发条件,满足任意两条才启动:一是同城两个 LDC 的心跳同时中断超过 60 秒;二是核心业务错误率超过 50% 且持续 2 分钟;三是外部多渠道确认(值班、监控、机房侧)一致指向同一故障。三条同时满足的要求太严,两条是实践出来的平衡点。
4.3 切换执行的标准流程分阶段拆解
标准切换流程分五个阶段,我按实际执行顺序展开。
阶段一:确认与冻结。值班长确认故障,按下"冻结开关"——停止所有非必要的变更、发布、扩容操作。这一步太重要了,切换过程中如果有其他变更在跑,排查问题时你根本分不清是谁导致的。
阶段二:数据状态检查。在切流之前,先检查目标 LDC 的数据是否完整。这一步不是可选项,必须做。检查内容包括:主从复制延迟、跨单元同步延迟、最近的校验和比对结果。如果目标侧数据落后太多,先追平再切换。
阶段三:流量切换。这是最核心的一步。顺序是:先切非核心流量(查询、展示),再切核心流量(交易、支付)。每次切 10%,观察 1~2 分钟再继续。切流的过程中要盯紧三个指标:目标侧的错误率、目标侧的容量水位、源侧是否还在接收流量(防止双写)。
阶段四:数据追平与校验。流量切过去之后,源侧可能还残留一部分未同步的数据,要做一次性的追平。追平之后跑校验任务,比对源和目标的关键数据(订单数、金额汇总)是否一致。
阶段五:观察与回切准备。切换完成后进入观察期,至少 30 分钟。这期间不改任何配置,只监控。同时准备好回切方案,万一新 LDC 顶不住,要能快速切回去。
4.4 数据校验与追平机制
数据校验是切换里最容易被糊弄的环节。很多人觉得"同步延迟小于 1 秒就没问题",但延迟是平均值,真正危险的是那些卡在链路里的个别数据。
我们的做法是三层校验。第一层是行数校验,比对每个表的记录数,差异超过阈值就告警。第二层是抽样内容校验,随机抽 1% 的记录做字段级比对,校验和一致才算通过。第三层是关键业务校验,对资金、库存这类敏感数据做全量比对,慢但必须做。
追平机制上,我们准备了一个补偿队列。切换期间所有因为链路中断没同步成功的写操作,都会被记录到补偿队列里,切换完成后由补偿任务逐条重放。重放必须幂等,因为有些操作可能已经成功了只是没收到确认。
注意:补偿队列本身也是要持久化的,不能放内存。我们第一次演练时把补偿队列放在内存里,切换过程中进程重启了一次,队列全丢了,追平数据花了两个小时人工对账。
4.5 回切与演练机制
切换不是单向的,切过去之后还要能切回来。回切比正切更难,因为切过去的这段时间里,新 LDC 已经产生了大量新数据,回切时要先把这些数据同步回原 LDC。我们要求回切必须满足两个条件:一是原 LDC 已经确认完全恢复并且稳定运行超过 30 分钟;二是双向同步链路已经打通并且延迟正常。
演练是容灾体系的生命线。我给的建议是季度全流程演练 + 月度单场景演练相结合。全流程演练要真的切生产流量,可以让内部系统先切、然后再切一部分外部低风险流量。月度演练则聚焦单个环节,比如只演"数据追平"或者只演"路由切换"。
演练之后必须复盘,复盘的产出要落到两个地方:一是更新切换手册,把发现的新问题写进去;二是转换成自动化脚本,能自动化的绝不留在人工步骤里。我们做了两年演练,切换手册从最初的 40 页变成 12 页,因为大部分步骤都脚本化了。
5. 实战踩坑记录与排查速查表
讲了这么多设计,最后落到实操。这部分是我最想分享的,因为设计文档到处都是,但真正踩过的坑很少有人系统写下来。下面这些都是我在真实项目里遇到过的,有的差点酿成事故,有的只是虚惊一场,但都值得记住。
5.1 演练里最容易翻车的几个点
第一个坑:连接池没预热。切换之后流量打到新 LDC,新 LDC 的服务实例是冷启动状态,数据库连接池是空的,一波流量进来要建几百个连接,直接把数据库的连接数打满。我们的解决办法是切换前先跑一轮预热脚本,模拟真实流量把连接池和缓存都填满。
第二个坑:缓存雪崩。新 LDC 的缓存是空的,所有请求都穿透到数据库。切换期间数据库本来就压力大,再叠加穿透流量,直接崩。解决办法是提前做缓存预热,把热点数据刷到新 LDC 的缓存里,并且给数据库加一层限流。
第三个坑:DNS 缓存。切换时改了 DNS 解析,但客户端的 DNS 缓存还没过期,一部分流量还在往旧 LDC 打。我们把 DNS 的 TTL 在切换前就调到了 60 秒,切换前 10 分钟开始生效,这样切换时客户端基本都能拿到新解析。
第四个坑:定时任务重复执行。这个前面提过,但值得再强调。切换之后两个 LDC 可能同时跑定时任务,导致数据被处理两次。解决办法是在任务调度里加分布式锁,锁的粒度按任务 ID。
第五个坑:日志和监控错乱。切换后,新 LDC 的日志采集配置没跟着改,日志全丢了,出问题完全没法排查。这个坑不致命但很折磨人,切换清单里一定要包含监控和日志的配置检查项。
5.2 常见问题排查速查表
| 现象 | 可能原因 | 排查方向 | 处理建议 |
|---|---|---|---|
| 切换后大量跨单元调用 | 路由结果未持久化或缓存未同步 | 检查 RZone 的映射表、客户端路由缓存 | 强制刷新路由缓存,补齐映射 |
| 新 LDC 数据库连接打满 | 连接池冷启动 + 流量突增 | 看连接数曲线、慢查询 | 预热连接池、临时调大 max_connections |
| 消息重复消费 | 补偿队列重放未幂等 | 查消息消费日志、去重表 | 消费端补幂等逻辑,去重表加唯一索引 |
| 主键冲突 | 全局 ID 生成器未按单元隔离 | 检查 ID 生成规则 | ID 中嵌入单元位,冲突数据人工修正 |
| 缓存与数据库不一致 | 双写顺序问题或缓存未失效 | 比对缓存和 DB 的关键字段 | 切换期间以 DB 为准,定向刷新缓存 |
| 切换后错误率高但监控正常 | 监控采集配置未同步 | 检查新 LDC 的采集 Agent | 补齐采集配置,重启 Agent |
| 回切时数据丢失 | 回切前未做双向同步 | 检查同步链路状态 | 回切前必须完成数据校验和追平 |
| 切换脚本执行卡住 | 依赖的服务未就绪 | 看脚本执行日志的卡点 | 脚本加超时和依赖检查 |
这张表我建议打印出来贴在值班工位上。真出事的时候人是慌的,照着表一项一项过,比凭记忆靠谱得多。
5.3 几条我自己踩出来的经验
第一条,切换手册要写"怎么判断"而不是"怎么做"。大部分手册写的是操作步骤,但真正难的是判断要不要切、切到哪一步该停。我给手册加了一章"决策树",用问答的形式引导值班人员做判断,效果好很多。
第二条,演练要演"失败"。常规演练都是顺利切换,但现实中切换经常失败,要训练团队在失败时怎么回滚、怎么止损。我们专门做了一次"切换中途回滚"的演练,暴露了回滚脚本根本没测过的问题。
第三条,不要迷信自动化。自动化能解决 90% 的重复劳动,但最后 10% 的判断必须留给人。我们曾经尝试做全自动的机房级切换,后来发现误判的代价太大,改成了"自动检测 + 人工确认 + 自动执行"的模式。
第四条,把每次演练的数据存下来。RTO 是多少、在哪一步耗时最长、哪些指标异常过,这些数据积累下来才能持续优化。我们做了两年的数据记录,发现耗时最长的永远是"决策"环节,后来通过优化值班流程把这个环节从 3 分钟压到了 40 秒。
第五条,跨团队协作的准备工作比技术准备更重要。三地五中心涉及的团队少说十几个:业务、DBA、中间件、网络、机房、监控。切换时如果某个团队的联系人找不到,整个流程就卡住了。我们维护了一份切换联系人清单,每季度更新一次,演练时必须按真实流程联系一遍。
5.4 未来还可以继续深挖的方向
这套体系统起来之后,还有几个方向值得继续做。一是故障自愈,现在很多场景还是要人工决策,能不能通过更精细的健康度模型让系统自动识别并处理更多类型的故障。二是多活向多云延伸,当基础设施不局限于自建机房时,LDC 的抽象层要能同时管住不同的云环境,元数据模型和网络方案都要重新考虑。三是切换的常态化,把切换从"重大事件"变成"日常操作",比如每天非高峰时段随机切一部分流量到备用 LDC,让切换能力一直保持在线。
我自己在实际操作中的体会是,这套架构最难的地方从来不是技术,而是让人愿意相信它。业务方要相信单元化不会丢数据,运维要相信切换脚本不会误操作,管理层要相信投入这么多钱是有价值的。这种信任只能靠一次次的演练和真实的故障处理慢慢积累起来,没有捷径。每次切换成功之后的那种踏实感,是这套体系给我最大的回报。