Chaos Engineering 实战:一次 Zone 故障演练暴露出 3 个隐藏的单点,我用这套预案 15 分钟恢复
Chaos Engineering 实战:一次 Zone 故障演练暴露出 3 个隐藏的单点,我用这套预案 15 分钟恢复
说实话,我们之前一直觉得自己挺高可用的。
多可用区部署、数据库主从、K8s 副本数 3+、告警链路到企业微信。纸面上一看,该有的都有。直到上个月我们第一次认真跑了一次 Zone 级故障演练,才发现“理论上能扛”和“真断了还能扛”之间,差了不止三条街。
演练场景很简单:把 us-east-1b 这个可用区里的所有业务 Pod 全部干掉,模拟一次 zone failure。结果?核心链路在 3 分钟内开始异常,客服群里陆续有人反馈“页面点不动”。更离谱的是,我们花了 15 分钟才完全恢复——而且不是因为业务复杂,是因为三个隐藏的单点没被发现。
这篇文章就把这次演练的过程、暴露的问题、以及我现在沉淀的预案和 checklist 完整记下来。不是纯理论,所有命令和配置都可以直接拿去跑。
背景:为什么要做 Zone 级演练
我们是一套典型的云原生架构:K8s 托管集群、RDS 多可用区、ElastiCache 主从、对象存储。业务流量走 Nginx Ingress → 业务服务 → 消息队列 → 结算系统。看似每个组件都有冗余,但我们之前从没验证过“一个可用区整体挂掉”这种场景。
导火索是隔壁团队的一次真实事故:他们某个可用区因为运营商链路问题整体失联,结果一个看似无关的“配置中心”实例刚好全在那个区,导致全链路服务发现异常。那次事故让我意识到,高可用不能只靠架构图,要靠演练图。
于是我们定了一个目标:每季度做一次 Zone 级故障演练,目标不是“证明系统没问题”,而是“主动找出问题”。
演练设计:怎么模拟一个可用区挂掉
我们没有直接物理断网,而是用了 Chaos Mesh 的ZoneChaos配合节点标签来做模拟。核心思路是:把某个可用区标签下的 Pod 全部删除,并且让调度器在一段时间内禁止往这个区调度新 Pod。
先用标签把节点按可用区打好标:
kubectl label nodes--alltopology.kubernetes.io/zone=\--overwritekubectl label nodes$(kubectl get nodes-lzone=1b-oname)\topology.kubernetes.io/zone=us-east-1b--overwrite然后写一个 Chaos Mesh 实验:
apiVersion:chaos-mesh.org/v1alpha1kind:PodChaosmetadata:name:zone-1b-failurenamespace:chaos-testingspec:action:pod-killmode:allduration:"10m"selector:namespaces:-"production"nodeSelectors:topology.kubernetes.io/zone:"us-east-1b"labelSelectors:app:"order-service"scheduler:cron:"@every 30s"这个实验会每 30 秒把 us-east-1b 里 production 命名空间下、带app=order-service标签的 Pod 杀一遍。持续 10 分钟。足够暴露问题,也不会真的把集群搞崩。
提醒:演练前一定要在非生产环境先跑三遍。我们第一次是在 staging 跑的,结果把 staging 的 ZooKeeper 选主搞出了脑裂,花了一个小时才恢复。血的教训。
演练中暴露的 3 个隐藏单点
单点一:配置中心只在一个可用区有副本
我们用的是自建的 Nacos 做配置中心。部署文档上写的是三节点,但仔细一看,三个 Pod 全在 us-east-1b。原因很简单:当初部署的时候,集群只有 1b 有足够的资源,后来扩了 1a 和 1c,但 Nacos 的 StatefulSet 没加反亲和性约束,副本一直原地不动。
演练开始后,1b 被干掉,Nacos 三个节点全挂。业务服务启动新 Pod 时读不到配置,直接 CrashLoopBackOff。老 Pod 还能靠本地缓存撑一会儿,但任何需要重启或扩容的场景都会炸。
修复方案是给 StatefulSet 加上强反亲和:
affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-nacostopologyKey:topology.kubernetes.io/zone然后滚动迁移,强制把副本打散到三个可用区。
单点二:第三方 webhook 的 endpoint 写死了单一可用区入口
我们的支付回调依赖一个第三方服务商。他们的回调地址是区域化的,但我们在文档里抄了一个 us-east-1b 的入口,写死在代码里。1b 一挂,webhook 发不过来,支付状态一直卡在“处理中”。
这个问题特别隐蔽,因为日常完全没问题。只有整个可用区失联时才会暴露。
修复方案是把这个 endpoint 改成多可用区域名 + 健康检查:
importrandomfromurllib.parseimporturlparse WEBHOOK_ENDPOINTS=["https://pay.us-east-1a.example.com/webhook","https://pay.us-east-1b.example.com/webhook","https://pay.us-east-1c.example.com/webhook",]defget_webhook_endpoint():# 实际生产环境建议用 DNS 权重 + 健康探测,而不是随机returnrandom.choice(WEBHOOK_ENDPOINTS)更稳妥的做法是接对方的全局域名,让 DNS 和任播来处理可用区切换。但这件事必须去供应商侧确认,不能自己脑补。
单点三:有状态会话缓存没有跨区复制
用户登录态存在 ElastiCache Redis 里。Redis 是主从结构,主节点在 1b,从节点在 1a。我们以为主从自动切换就够了,但演练时切换花了 40 多秒,期间大量用户被登出,客服工单直接刷屏。
更坑的是,有些服务在 Redis 不可用时没有降级逻辑,直接 500。等于说 Redis 主从切换虽然发生了,但业务损伤已经造成。
修复分两步:
- 把 Redis 主从改成 Cluster Mode,让客户端能自动感知故障转移;
- 给所有读缓存的地方加降级:缓存读不到就走数据库,宁可慢不能挂。
defget_user_session(user_id):try:returnredis.get(f"session:{user_id}")exceptredis.ConnectionError:# 降级:直接读数据库,同时打标监控metrics.counter("cache_fallback_db").inc()returndb.query_session(user_id)15 分钟恢复的时间线
演练当天,我们的 on-call 同学按下面这个时间线恢复了业务:
- T+0:00:Chaos 实验注入,1b 业务 Pod 开始被批量删除。
- T+1:30:监控大盘出现订单成功率下降,告警触发。
- T+2:00:on-call 确认是演练导致,未触发回滚,继续观察。
- T+3:30:客服开始反馈登录掉线、支付卡住。确认 Redis 主从切换进行中。
- T+5:00:Redis 完成自动切换,但第三方 webhook 还在丢消息,支付状态不一致。
- T+7:00:临时切换 webhook 到备用 endpoint,支付回调恢复。
- T+10:00:Nacos 迁移完成,新 Pod 可以正常启动。
- T+15:00:全链路指标回到基线,演练结束。
坦白讲,这 15 分钟里至少有 8 分钟是在定位“到底是哪个单点”。如果问题事先都知道,恢复能压缩到 5 分钟以内。所以混沌工程最大的价值不是训练恢复速度,而是把未知风险变成已知风险。
我沉淀下来的演练 checklist
现在我们每次演练前都会过一遍这个 checklist,演练后补一条复盘记录。
演练前:
- 确认演练范围:是 zone、node、还是单 Pod?
- 确认停止条件:成功率降到多少立即终止?
- 通知客服和运营,避免“误报警”变成真事故。
- 在 staging 跑通至少一次完整实验。
- 备份数据库和关键配置,确保可以回滚。
演练中:
- 指定一个指挥员,其他人只记录不操作。
- 每 30 秒记录一次核心指标:订单成功率、P99 延迟、错误率、登录态异常数。
- 发现超时或失败立即暂停实验,先止血再复盘。
演练后:
- 输出《单点清单》和《修复计划》。
- 把修复项排进下一个 sprint,责任人到天。
- 更新 runbook,把新发现的问题写成标准化恢复步骤。
- 把有修复的项再跑一次演练,验证是否真的解决了。
写在最后
混沌工程不是“没事找事”。在真实事故里,那 15 分钟可能就是几十万甚至几百万的损失。演练把这 15 分钟“预支”到一个可控的周末,让我们能笑着把问题挖出来。
如果你也在做云原生架构,我建议:先别急着上更复杂的容灾方案,先跑一次 Zone 级故障演练。你可能会发现,你最担心的那个组件其实没问题,真正脆弱的,是那些你从来没想过会挂的东西。
现在我们的演练已经从“季度一次”改成“月度一次”。不是因为我喜欢折腾,而是因为每次都能找到新的坑。高可用这件事,本来就是没有尽头的。
参考资料:
- Chaos Mesh 官方文档:https://chaos-mesh.org/
- AWS Well-Architected 可靠性支柱:https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html
- Google SRE Book:Chaos Engineering 章节
