近期排查了一起极其典型的“异地多活”架构翻车事故。某业务在做全链路压测与流量切流演练时,双中心网关集群在 10 秒内接连雪崩,P99 延迟从 15ms 直接飙升至网关超时上限(10s),最终导致两个可用区同时瘫痪。
结论先行:这不是什么深奥的底层 Bug,而是一个极其低级的架构设计缺陷。控制面在下发“单元化路由规则(UID -> AZ)”时存在跨城同步延迟。在这短短 5 秒的数据不一致窗口期内,AZ-A 认为请求该去 AZ-B,而 AZ-B 还在使用旧规则认为请求该回 AZ-A。网关层完全没有做防环处理(Loop Detection),导致请求在两地专线间无限次 Ping-Pong 转发,瞬间打爆了 Envoy 的连接池和跨城专线带宽。
伪多活架构的遮羞布,就这样被区区 5 秒的延迟扯得粉碎。
故障现场:从 P99 飙升到全局 502
排查过程中,监控面板的异动非常诡异:
-
外部流量未突增:入口 QPS 正常,没有遭受 DDoS 攻击。
-
专线带宽被打满:两地机房之间的 10G 专线监控显示,出入带宽在几秒内呈直线上升至 100%。
-
网关层资源枯竭:Envoy 节点的 CPU Load Average 飙升至 80+,
envoy_cluster_upstream_rq_pending_overflow指标疯狂报错。 -
后端业务毫无波澜:底层的微服务和 DB 监控一片祥和,甚至 QPS 还下降了——因为流量全死在网关了。
直接拉取 Envoy 的 Access Log,发现令人窒息的现象,同一个 x-request-id 在同一秒内出现了数百次日志打印:
{"time": "...", "x-request-id": "a7b2c9-...", "upstream_cluster": "outbound|80||gateway-az-b", "response_code": "504"}
{"time": "...", "x-request-id": "a7b2c9-...", "upstream_cluster": "outbound|80||gateway-az-b", "response_code": "504"}
而在 AZ-B 的网关日志里,同样的 Request ID 正在被疯狂转发回 AZ-A。
根因拆解:分布式的“阿喀琉斯之踵”
该业务号称实现了“异地双活”,其实质是经典的单元化架构(Cell-based Architecture)。路由规则按用户 UID 取模或查表分配:UID_Range_1 在 AZ-A 闭环,UID_Range_2 在 AZ-B 闭环。如果用户访问错了机房,入口网关会负责将其 Proxy 到正确的机房。
演练时,运维执行了 UID 搬迁操作:将某一批 UID 从 AZ-A 迁移至 AZ-B。
正常的迁移状态机应该是:禁止写入 -> 数据同步 -> 变更路由规则 -> 开放写入。
问题出在路由规则下发环节。全局控制面(Global Control Plane)将新的路由表通过 xDS 下发给两地的 Envoy 集群。 由于跨城网络抖动和底层配置中心的同步机制,AZ-A 的网关瞬间收到了新规则,而 AZ-B 的网关存在约 5 秒的同步延迟。
这 5 秒内,逻辑变成了这样:
-
用户流量进入 AZ-A。
-
AZ-A 网关查最新路由表:“该 UID 已迁至 AZ-B”,于是将请求通过专线转发给 AZ-B 的网关。
-
请求抵达 AZ-B。AZ-B 网关查旧路由表:“该 UID 属于 AZ-A”,于是将请求再转发回 AZ-A 的网关。
-
AZ-A 再次收到请求,再次转发给 AZ-B……
一次简单的 HTTP 请求,在没有 Max-Forwards 限制的情况下,变成了跨城专线上的死循环。几千个这样的请求,瞬间裂变成数百万次的内部 RPC 调用,直接击穿 Envoy 的 max_connections 和 max_pending_requests 限制,导致网关假死,进而引发全量业务 502。
为什么犯错不可原谅?
真正的多活,不仅是画在 PPT 上的两套对等集群,而是骨子里对分布式系统“弱一致性”的敬畏。 CAP 定理早就告诉我们,跨越 WAN 网络的节点,绝对不可能实现原子的状态变更。只要存在时间差,就一定会出现路由视角的不一致。
在架构设计时,不假设“配置下发绝对同时生效”,而是假设“一定会出现路由环路并进行兜底拦截”,这叫防御性编程。花了几百万拉跨城专线,却连一个最基础的 Hop Limit 都不加,这种架构翻车纯属人祸。
止血与防御性修复
当时在现场的紧急止血操作非常粗暴:直接切断了 AZ-A 到 AZ-B 的专线路由转发(牺牲跨城纠错能力,强行阻断环路),网关雪崩立刻停止。随后紧急排查控制面同步组件并修复延迟。
彻底的修复方案(防环机制落地):
1. 网关层强制拦截:引入 Max-Forwards 机制
无论使用 Nginx 还是 Envoy,在进行跨机房流量 Proxy 时,必须注入并校验自定义 Header(如 X-Multi-Active-Hop)。在 Envoy 中,可以通过原生机制或极简的 Lua Filter 实现:
# Envoy Lua Filter 防环片段
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
local hop_count = tonumber(request_handle:headers():get("x-multi-active-hop") or "0")
if hop_count >= 2 then
request_handle:respond({[":status"] = "508"}, "Loop Detected in Multi-Active Routing")
return
end
request_handle:headers():replace("x-multi-active-hop", tostring(hop_count + 1))
end
2. 路由变更状态机:平滑过渡
不要做“一刀切”的路由变更。UID 迁移的路由切换必须存在中间态(Transit State)。
当 UID 正在迁移时,路由状态设为 MIGRATING,此时新旧机房的网关对该 UID 的请求应统一 Hold 住(挂起等待)或降级处理,直到两端均确认收到最新配置(ACK)后,再将状态切为 COMMITTED 放行。
3. 隔离爆炸半径 为跨城 Proxy 流量配置独立的 Cluster 和 Connection Pool,绝对不能与处理本地域内流量的线程池混用。这样即使专线打满或跨城目标假死,本地域内的“正确流量”依然不受影响。
同类问题速查(排查清单)
-
跨城/跨 AZ 路由环路检测:检查所有跨域转发是否携带并校验了
X-Forwarded-For、Max-Forwards或自定义跳数 Header,超过阈值(通常为1或2)必须立即丢弃并返回 508 (Loop Detected)。 -
配置中心弱一致性容灾:检查下发控制面(etcd / Consul / 自研 xDS 服务)在脑裂或跨城延迟 > 10s 的情况下,Data Plane 是否能优雅降级,还是会触发雪崩逻辑。
-
隔离与限流(Bulkhead):检查网关对于“跨城纠错流量”是否配置了独立的连接池(Connection Pool)和并发数限制(Circuit Breaker),防止小比例的纠错流量耗尽全局 Worker 资源。
-
UID 状态机原子性:在单元化架构中,检查 UID 归属地切换是否有明确的“过渡态”,严防因配置生效时间差导致的“两地都不认”或“两地互相抛”的脏读问题。