主题
微服务多活部署:同城双活、异地多活与单元化流量调度
问题
某天机房掉电,整个集群全挂,老板问「不是有容灾吗?」你说「有,但没切过去」。这不是段子,是很多公司多活架构的真实状态——部署了、测试了,但真到故障时没人敢按那个切流按钮。
微服务多活不是简单的「多部署几个机房」。注册中心怎么跨机房隔离?配置中心怎么同步?流量怎么切才不会把对端打崩?数据一致性怎么兜底?这些问题答不上来,多活就是个摆设。
分析
部署形态阶梯:从单机房到异地多活
多活架构的复杂度随部署形态指数级增长,不存在一步到位的「最佳方案」,只能按业务阶段选型。
单机房:一台机器->集群,这是起点。故障时恢复时间看运气。
同城双活:两个机房在同城,物理距离 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