近期在协助某业务线进行 GameDay(故障演练)验证 SLO 时,遭遇了一场从“局部演练”演变为“全量生产事故”的离谱故障。原本旨在验证 Redis 访问延迟时业务自动降级到 MySQL 的逻辑,结果不仅引发了 MySQL InnoDB 锁死、全链路雪崩,更要命的是,操作员在恐慌中强删 Chaos Mesh 的 CRD,直接破坏了 Operator 的状态机,导致故障注入变为永久性残留。
最终结论先行:
业务代码在设计降级链路时,缺乏最基础的并发熔断机制(Circuit Breaker),导致 Redis 延迟后瞬时海量请求全部砸向 MySQL,直接打满线程池;而运维在紧急中止演练时,盲目使用 kubectl patch 强制清空 finalizers,导致 Kubernetes API 对象被立刻删除,Chaos Controller 彻底丢失清理上下文,底层 Pod Network Namespace 中的 Linux tc (Traffic Control) 规则永久残留,演练变成了真死机。
故障现场:失控的爆炸半径
排查过程中,监控面板的雪崩来得非常快:
-
Redis 注入延迟生效:通过 Chaos Mesh 下发
NetworkChaos,向特定 Pod 注入 2000ms 延迟。Redis 客户端 P99 监控如期飙升。 -
MySQL 瞬间瘫痪:不到 30 秒,MySQL 的
Threads_running指标从常规的 50 飙升到 8000+,数据库直接报ERROR 1040 (HY000): Too many connections。 -
API Server 阻塞与强删惨剧: 发现业务雪崩后,演练人员试图紧急停止注入:
bash kubectl delete networkchaos redis-delay-test -n production命令卡死没有任何响应。原因是瞬时的连接风暴导致 Node 负载飙高,Chaos Webhook 和 Controller Manager 处理超时。 此时,演练人员犯下了不可原谅的致命错误——动用 K8s 的“核武器”强删:bash # 绝对禁止在未清理底层资源时这样滥用! kubectl patch networkchaos redis-delay-test -n production -p '{"metadata":{"finalizers":[]}}' --type=merge执行后,CRD 瞬间消失。控制台显示“已删除”。 但业务完全没有恢复。 重启 MySQL 后,业务 Pod 依然处于 2000ms 的网络延迟中,哪怕 Chaos 对象在 etcd 里已经荡然无存。
核心原理剖析:为什么强删 Finalizer 是自寻死路?
这次故障暴露了两个维度的技术盲区:架构防御性的缺失,以及对 K8s Operator 模式底层状态机的无知。
1. 降级链路的无防备反噬
在没有限流控制的前提下,做高并发的“缓存降级到 DB”无异于自杀。
当 Redis 出现 2s 延迟时,原先 10ms 返回的请求全部堆积在内存中。由于没有配置 Hystrix、Sentinel 或 Resilience4j 的最大并发拦截(Max Concurrent Calls),Tomcat/Go 协程池全量承接流量并转向 MySQL 发起查询。
结果就是,原本能被 Redis 拦截的 5000 QPS 读请求,全部变成了 MySQL 的慢查询,直接耗尽 max_connections,引发 CPU 和 IO 的双重饱和。永远不要测试没有熔断保护的降级链路,那不叫高可用,叫 DDoS 放大器。
2. Chaos Mesh 与 tc qdisc 的幽灵规则
在 K8s 生态中,finalizer 的存在是为了保证异步垃圾回收。Chaos Mesh 的运行机制如下:
-
创建
NetworkChaos。 -
Chaos Controller 监听到事件,给对象打上
chaos-mesh/records的 finalizer。 -
下发指令给对应 Node 上的 Chaos Daemon。
-
Chaos Daemon 通过
nsenter进入目标 Pod 的 Network Namespace,执行 Linux tc 命令:bash tc qdisc add dev eth0 root netem delay 2000ms - 正常删除流程:用户执行
kubectl delete,API Server 仅设置deletionTimestamp。Controller 监听到删除信号,通知 Daemon 执行tc qdisc del dev eth0 root清理规则,最后由 Controller 移除 finalizer,对象才从 etcd 中真正消失。
当操作员使用 patch 强行清空 finalizer 时,API Server 立即从 etcd 抹除了该对象。Chaos Controller 直接丢失了该对象的上下文(再也查不到目标 Pod 的标签选择器和状态记录)。
此时,底层的情况是:K8s 认为天下太平,但 Pod 的网卡上依然挂着 netem delay 规则。
我们登入故障机器,找到一个受害 Pod 的 PID,直接验证:
# 获取 Pod 主进程 PID
PID=$(crictl inspectp -o go-template --template='{{.info.pid}}' <pod-id>)
# 进入网络命名空间查看 tc 规则
nsenter -t $PID -n tc qdisc show dev eth0
# 输出结果赫然写着:
qdisc netem 8001: root refcnt 2 delay 2.0s
只要这个 Pod 不被销毁,这个 2s 的延迟规则将永远伴随它。
止血与彻底修复
现场止血方案: 既然 K8s 侧状态已经丢失,唯一快速恢复的方法是滚动重启所有受影响的 Pod,让 CNI 重新分配 Veth pair 和网络命名空间。
kubectl rollout restart deploy/payment-service -n production
长效整改方案(防御性编程与架构):
- 强制引入并发隔离:在业务代码的降级逻辑侧,必须加装 Bulkhead(舱壁隔离)或熔断器。
java // 伪代码示例:严格限制降级到 DB 的并发线程数 @Bulkhead(name = "redisFallbackDb", maxConcurrentCalls = 20) public User getUserFallback() { ... } -
演练紧急停止开关 (Halt/Abort):在 Chaos Mesh 中,不要依赖
kubectl delete,而是应该配置annotations快速挂起,或者配置与 Prometheus 联动的监控熔断。 -
限制爆炸半径:严格限制
NetworkChaos的mode和selector,绝对禁止在全命名空间进行未经externalTargets过滤的网络阻断(否则极易把 Pod 与 CoreDNS 和 API Server 的通信一并干掉)。
同类问题速查排查清单
-
资源强删后遗症排查:发现 K8s 资源卡在 Terminating 时,严禁无脑 patch finalizer。必须先看 Controller 的 Logs 确认阻塞原因。若是网络类故障遗留,需通过
nsenter -n进容器检查tc或iptables。 -
Chaos Mesh 失控急救:若 Controller 已瘫痪,可通过重启 Chaos Daemon (DaemonSet) 或手工执行清理脚本恢复底层网络:
tc qdisc del dev eth0 root。 -
降级链路容量核算:压测或演练降级链路前,核对下游组件(如 MySQL/RPC)的连接池上限。降级 QPS 预估值绝对不能超过下游的最大吞吐量,必须设置 Rate Limiter。
-
验证 eBPF/tc 规则残留:执行
tc filter show dev和egress tc qdisc show dev。任何未知的netem或bpf挂载点都可能是故障演练工具的残留。