Skip to content

微服务多活部署:同城双活、异地多活与单元化流量调度

问题

某天机房掉电,整个集群全挂,老板问「不是有容灾吗?」你说「有,但没切过去」。这不是段子,是很多公司多活架构的真实状态——部署了、测试了,但真到故障时没人敢按那个切流按钮。

微服务多活不是简单的「多部署几个机房」。注册中心怎么跨机房隔离?配置中心怎么同步?流量怎么切才不会把对端打崩?数据一致性怎么兜底?这些问题答不上来,多活就是个摆设。

分析

部署形态阶梯:从单机房到异地多活

多活架构的复杂度随部署形态指数级增长,不存在一步到位的「最佳方案」,只能按业务阶段选型。

单机房:一台机器->集群,这是起点。故障时恢复时间看运气。

同城双活:两个机房在同城,物理距离 10-50km,光纤延迟 1-3ms。共享存储(如 SAN、分布式存储)或独立存储但 DB 主从同步。两个机房都接流量,但写操作只能回主库。本质是「读多活 + 写主备」,并非真正的双写。

两地三中心:一个城市两个机房(同城双活)+ 异地一个灾备机房。异地机房平时不接流量,只接收数据同步。故障时手动切过去。RPO 取决于同步延迟,RTO 取决于人的响应速度。

异地多活:多个城市都有独立写能力的机房,每个机房自包含服务 + 缓存 + DB。这是真正的多活,但也是坑最多的架构——数据冲突、同步延迟、跨机房调用链路。

微服务视角 vs 中间件视角

纯微服务视角看多活,容易忽略中间件层的配置差异。多活不是部署一套代码就完事的,需要以下组件协同配合:

注册中心多机房隔离:Nacos 和 Consul 都支持 multi-datacenter 模式。核心思路是:服务在本地注册中心注册,但允许跨机房查询。Nacos 的 nacos.naming.distro-task-interval-ms 控制跨机房数据同步间隔,默认 1000ms,对延迟敏感的场景要调小。

配置中心跨机房同步:配置变更需要广播到所有机房。Nacos 通过 Distro 协议做数据同步,Apollo 的 Config Service 需要每个机房独立部署 + Meta Server 做集群发现。配置不同步的后果很严重——改个限流阈值,一个机房改了另一个没改,流量调度出问题。

网关层流量调度:入口网关按用户特征(uid hash、地理位置)路由到目标机房,同时做跨机房降级。比如 A 机房挂了,网关直接路由到 B 机房,但需要评估 B 机房的容量是否撑得住。

单元化:多活的终极形态

单元化是解决「写多活」问题的主流方案,核心思想是:按用户维度分片,每个分片的所有操作在一个单元内闭环

流量调度时序(单元化路由)

用户请求


入口网关(SLB / Global LB)

  ├── uid hash % 3 = 0 ──> 单元 A(上海机房)
  │                        ├── 网关层 → 服务层 → 缓存层 → DB 层
  │                        └── 所有写操作在单元内完成,不跨单元

  ├── uid hash % 3 = 1 ──> 单元 B(上海机房)
  │                        └── 同上,完全闭环

  └── uid hash % 3 = 2 ──> 单元 C(杭州机房)
                           └── 同上,完全闭环

跨单元数据同步:通过 binlog CDC(Debezium → Kafka)异步同步到全局 IDC

单元内闭环是单元化的核心约束。一个用户的所有请求(读、写、缓存、DB)都在同一个单元完成,不跨单元调用。如果业务架构做不到这一点——比如订单在单元 A 但支付回调发到单元 B——那单元化就是空中楼阁。

跨单元收敛:即使做不到 100% 闭环,也要尽量收敛。支付回调、消息推送这类写操作,可以通过路由层识别目标单元,把请求转发过去。不收敛的后果是跨机房 RPC 延迟 + 分布式事务失控。

数据同步链路与一致性兜底

多活架构落地最难的部分不是流量调度,是数据。

binlog CDC 同步:Debezium 监听 MySQL binlog,写到 Kafka,下游消费写入同步库。这里有个常见的坑:回环写入。A 机房的 binlog 同步到 B 机房,B 机房的 binlog 又同步回 A 机房,导致数据无限循环。解决方案是给 binlog 打上源机房标记,消费端过滤掉本机房产生的变更。

缓存同步:Redis 跨机房同步有几种方案:Redis Cluster 跨机房(官方不支持,不推荐)、自建同步工具(如阿里云 Redis 的全球分布式模式)、或者业务层容忍缓存不一致——读 DB 兜底,缓存只做加速。对一致性要求高的数据,建议直接走 DB,不要依赖缓存同步。

一致性兜底:假设某条数据在同步过程中出现了冲突(比如两个机房同时修改了同一条记录的用户名),需要一个确定性策略来决定谁赢。常见做法:

  • 按机房优先级(上海 > 杭州,上海机房的数据覆盖杭州机房的)
  • 按时间戳(最后写入者胜出,但要求各机房时钟同步)
  • 业务自定义(保留某个字段来自 A 机房,另一个字段来自 B 机房)

没有银弹,选一个方案并在业务层面接受相应价。

故障切换决策:RPO/RTO 与切流按钮

人肉切流最大的问题是不敢切。平时没有演练,真出故障时,运维团队花 30 分钟开会确认「是不是真的挂了」,然后花 30 分钟讨论「切了会不会更糟」,最后 2 小时过去,业务已经崩完了。

RPO(Recovery Point Objective):能容忍丢失多少数据。同城双活 RPO ≈ 0(同步复制),异地多活 RPO 取决于同步延迟,几秒到几分钟。

RTO(Recovery Time Objective):多久恢复。同城双活秒级,异地多活分钟级。

切流按钮:生产环境至少要准备:

  • 手动切流:操作面板,确认 + 二次确认,有回退按钮
  • 自动切流:基于健康检查 + 异常检测(如 5xx 比例突增、P99 延迟飙升),但自动切流需要灰度策略——先切 1% 流量验证,确认没问题再全量切
  • 切流演练:每季度至少一次,模拟机房故障,记录切换时间,迭代切流 SOP

K8s 跨集群调度:多活场景下,K8s 本身不跨集群做流量调度。Service 和 Ingress 只在集群内生效。跨集群的流量路由需要依赖外部负载均衡器(如 Global SLB、DNS 智能解析)或 Service Mesh 的多集群方案(Istio multi-cluster)。

yaml
# 多活 K8s 集群的 Service 配置示例(每个机房独立集群)
# 集群 A(上海)
apiVersion: v1
kind: Service
metadata:
  name: user-service
  annotations:
    # 标记该 Service 属于哪个机房,供上游负载均衡器识别
    multi-region.zone: shanghai
spec:
  selector:
    app: user-service
  ports:
    - port: 8080
---
# 集群 B(杭州)
# 同名的 Service,标注不同 zone

全局负载均衡器(如阿里云 Global Traffic Manager、AWS Route 53)根据 DNS 解析或 Anycast 就近路由,再将流量转发到对应集群的 Ingress Gateway。

与 02 篇注册中心跨机房坑的关系

之前 02 篇(服务发现与注册中心选型)提到过 Nacos/Consul 跨机房心跳的坑。这里展开说多活场景下的配置清单:

  • Nacos 跨机房nacos.naming.distro-task-interval-ms 调小到 200-500ms;nacos.naming.clientbeat.check-interval 保持独立心跳,不要跨机房上报
  • Consul 多数据中心:WAN gossip pool 自动发现,但跨数据中心查询是同步的(RPC 等待),延迟敏感场景建议用本地缓存 + 异步刷新
  • 注册中心多活:别把注册中心跨机房部署成一个大集群——网络分区后整个注册中心不可用。每个机房独立部署注册中心集群,通过 Distro 或 WAN gossip 做数据同步

总结

多活架构的落地公式:单元化保证写闭环 + 数据同步保证一致性 + 可演练的切流机制保证故障响应。不要一开始就追求「全量异地多活」,从同城双活起步,跑通故障切换流程,再逐步扩展到多地。生产环境里,金丝雀多活比全量多活更安全——先让 20% 用户走双活验证,稳定了再全量切。

参考

  • 微服务模块 02 篇:服务发现与注册中心选型(跨机房心跳坑)
  • 微服务模块 33 篇:治理实践(优雅停机与探针)
  • 分布式系统模块 34 篇:异地多活架构(数据同步与共识)
  • 《Designing Data-Intensive Applications》Chapter 9: Consistency and Consensus
  • Uber 单元化架构实践:Project Mecha

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1