排查过程中,我们遭遇了一次由于混沌工程(Chaos Engineering)实施不当引发的生产级雪崩。原本只是一次常规的 GameDay 演练,目的是向支付网关注入 100% 网络丢包以验证断路器(Circuit Breaker)的降级 SLO,最终却导致该节点被标记为 NotReady,数百个核心 Pod 被强行驱逐,跨可用区的网络流量剧烈抖动。最终结论很明确:在对配置了 hostNetwork: true 的目标 Pod 注入网络故障时,未做端口或目标 IP 级别的精细过滤,直接在物理网卡上挂载了全局的 tc netem 丢包规则,阻断了 Kubelet 与 API Server 的心跳,导致控制平面触发了无差别的脑裂驱逐(Eviction)。
混沌工程的核心原则是“控制爆炸半径”,而不是在没有消防措施的情况下往机房里扔燃烧弹。这种因为对底层网络命名空间(Network Namespace)和 K8S 节点状态机理解不透彻造成的事故,属于典型的低级失误。
案发现场:从演练降级到节点失联
某次核心链路故障演练期间,监控大盘显示支付网关的 99 线延迟如预期般飙升,断路器指标(hystrix_circuit_breaker_open)也正常触发。但演练进行到第 60 秒时,事态失去控制:
-
Prometheus 疯狂涌出
KubeNodeNotReady告警。 -
该节点上的数十个与演练完全无关的微服务(包括本地缓存 Redis 和日志采集 DaemonSet)连接数全部归零。
-
K8S 控制平面开始调度新 Pod 到其他节点,引发瞬间的镜像拉取风暴和 CPU 满载(Load Average 瞬间飙升到 80+)。
登录目标节点,系统自身其实毫无压力,CPU、内存、磁盘 IO 全在低位。但查看 Kubelet 日志,满屏都是触目惊心的超时报错:
E0914 10:23:45.123456 1234 kubelet_node_status.go:456] Error updating node status, will retry: error getting node "node-pay-01": Get "https://10.0.0.1:6443/api/v1/nodes/node-pay-01": dial tcp 10.0.0.1:6443: i/o timeout
E0914 10:23:55.124567 1234 kubelet_node_status.go:456] Error updating node status, will retry: error getting node "node-pay-01": Get "https://10.0.0.1:6443/api/v1/nodes/node-pay-01": dial tcp 10.0.0.1:6443: i/o timeout
Kubelet 彻底哑火了。
底层机制:tc netem 与 hostNetwork 的致命交叉
为什么对一个“支付网关 Pod”注入网络故障,会干掉整个宿主机的网络?
排查混沌注入工具(此处使用的是基于 eBPF/tc 封装的底层执行器)的注入记录,并直接在问题节点执行 tc qdisc show 查看流量控制规则:
$ tc qdisc show dev eth0
qdisc netem 8001: root refcnt 9 loss 100%
破案了。工具在物理网卡 eth0 的 root 队列规则(qdisc)上,直接挂载了 netem loss 100%。
在 Kubernetes 环境中,绝大多数 Pod 运行在独立的 Network Namespace 中(由 CNI 分配独立 IP 和 veth pair),对这些 Pod 注入网络故障,tc 规则只会作用于 Pod 对应的 veth 接口或内部的 eth0 上。
然而,由于支付网关需要极高的网络吞吐,运维团队为该 Deployment 配置了 hostNetwork: true。这意味着该 Pod 共享宿主机的 Root Network Namespace。
当混沌引擎按照常规逻辑查找该 Pod 的网络接口并实施注入时,它拿到的是宿主机的 eth0。更要命的是,注入配置里没有指定任何 Filter(过滤器),直接进行了全局 100% 丢包。
来看看 K8S 节点状态机的默认时间线:
-
Kubelet 每 10 秒向 API Server 发送一次心跳(
node-status-update-frequency)。 -
当 API Server 连续 40 秒(
node-monitor-grace-period)收不到心跳,kube-controller-manager就会将 Node 标记为NotReady。 -
如果节点持续
NotReady达到 300 秒(Pod 默认的tolerationSeconds),控制平面将触发驱逐逻辑。由于演练中某些关键有状态 Pod 的容忍度被调整为了 60 秒,驱逐几乎在节点失联后立刻发生。
拨乱反正:爆炸半径的精细管控
混沌工程不是盲目的破坏。既然我们要对 hostNetwork 下的特定进程注入故障,就必须在内核网络栈层面实现精确制导。
1. 废弃无差别的 root qdisc
切忌直接在 root 节点挂载 netem,必须结合 htb (Hierarchical Token Bucket) 或 prio 建立层级队列,并使用 u32 或 cgroup 过滤器匹配特定流量。
如果要针对支付网关(监听在 8080 端口,目标下行调用 3306 数据库)进行丢包注入,正确的底层 tc 规则构建路径应该是:
# 1. 在 eth0 根节点创建 prio 队列(分为 3 个频段,默认走 band 2)
tc qdisc add dev eth0 root handle 1: prio
# 2. 在 band 1 (handle 1:1) 上挂载 netem 丢包规则
tc qdisc add dev eth0 parent 1:1 handle 10: netem loss 100%
# 3. 使用 u32 过滤器,仅将发往 3306 端口的流量导向丢包队列 (classid 1:1)
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip dport 3306 0xffff flowid 1:1
# 4. (关键) 显式放行 Kubelet 与 API Server 的通信端口 6443,导向正常队列 (classid 1:2)
tc filter add dev eth0 protocol ip parent 1:0 prio 0 u32 \
match ip dport 6443 0xffff flowid 1:2
2. 使用支持白名单的现代混沌工具
如果在 Kubernetes 中使用 Chaos Mesh 或 Litmus,必须在 YAML 中显式配置目标及绕过规则。现代混沌工具提供了网络级别的高级选择器。例如 Chaos Mesh 的 NetworkChaos:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: payment-loss-injection
spec:
action: loss
mode: one
selector:
labelSelectors:
app: payment-gateway
loss:
loss: '100'
# 必须限制方向,并在外部控制器中规避控制面端口
direction: to
target:
selector:
labelSelectors:
app: mysql-cluster
如果是针对 hostNetwork,必须开启特定的 BPF 注入模式或在系统级预设 K8S 控制平面的网段白名单(CIDR exclude)。
避坑总结
高可用系统的韧性验证,基础是敬畏系统。在生产环境进行故障注入,必须具备“一键终止(Abort Button)”的能力,但如果网络故障直接干掉了节点级别的通信,你的“一键终止”指令连下发到 Chaos Daemon 的通道都会被切断,这就演变成了不可控的灾难。做混沌实验前,先问自己三个问题:目标域到底在哪个 Namespace?底层注入手段会不会产生越权逃逸?如果控制通道断了,实验能不能自动回滚?
同类问题速查排查清单
-
宿主机网络逃逸排查:检查目标业务 Pod 是否配置了
hostNetwork: true。如果是,常规的网络故障注入(如直接作用于网卡的tc规则)将污染整个宿主机网络环境。 -
Kubelet 心跳阻断确认:当节点突然 NotReady,优先检查 API Server 审计日志与问题节点本地 Kubelet 日志,确认
i/o timeout端口是否为 6443,判断是否为网络层拦截导致。 -
残留 tc 规则清理:如果注入工具异常退出导致规则未清理,手动执行
tc qdisc del dev恢复网络通信,切忌盲目重启节点。root -
混沌控制通道隔离:混沌代理(Chaos Daemon)所在的端口和控制平面(API Server 6443, etcd 2379, kubelet 10250)必须在注入系统的全局白名单(Whitelist/Bypass)中,确保任何情况下的实验可中止性。