标签: tc流量控制

  • 深入混沌工程内核:从 TC/eBPF 故障注入到 SLO 自动化验证实战

    混沌工程绝不是毫无章法的“拔网线”。本文直接拆解基于 Chaos Mesh (v2.6.2) 的底层故障注入原理(Linux tc 与 eBPF 机制),并给出一套将故障注入与 Prometheus SLO 报警集成的自动化 GameDay 验证闭环方案。记住:没有可观测性度量和自动恢复兜底的故障注入,纯粹是在搞破坏。

    为什么你的故障注入总是不及预期?深入 TC 与 eBPF 机制

    很多研发拿着现成的 YAML 一把梭,看到 Pod 报错就以为混沌实验成功了。但在真实的排查场景中,如果不清楚底层到底“烂”在哪个系统调用或网络栈层级,你根本无法验证微服务的超时重试和熔断机制是否真正生效。

    1. 网络延迟注入:Netem 与 Namespace 的戏法

    当你下发一个针对某个 Pod 的网络延迟(NetworkChaos)时,控制面并不会去修改交换机配置。底层的 chaos-daemon 会通过 Kubelet 拿到目标容器的 PID,然后利用 nsenter 钻进该容器的网络命名空间(Network Namespace),利用 Linux 内核自带的 Traffic Control (tc) 和 netem 模块进行流量整形。

    某次验证超时熔断时,发现注入 200ms 延迟后应用依然秒回。直接登录 Node,钻入目标 Pod 命名空间查看真实流控规则:

    # 获取目标 Pod 容器的主进程 PID
    PID=$(crictl inspect <container_id> | jq .info.pid)
    
    # 进入容器的网络命名空间查看 tc 规则
    nsenter -t $PID -n tc -s qdisc show dev eth0
    

    正常被注入延迟的网卡,你能看到类似如下的输出:

    qdisc netem 1: root refcnt 2 limit 1000 delay 200.0ms  10.0ms 25%
     Sent 10234 bytes 81 pkt (dropped 0, overlimits 0 requeues 0)
     backlog 0b 0p requeues 0
    

    如果输出是 qdisc pfifo_fast 0:,说明 tc 规则根本没打上。通常是因为 CNI 插件(如 Cilium 的某些 BPF 模式)绕过了宿主机的 veth pair,或者内核没有加载 sch_netem 模块(modprobe sch_netem 可解)。

    2. 磁盘 IO 故障:eBPF 对 VFS 的精准拦截

    早期的 IO 故障注入靠在容器里跑 dd 把磁盘带宽打满,这种做法极度粗暴,且容易引发宿主机的 IO 风暴,波及同节点其他核心 Pod(典型的爆炸半径失控)。

    现代混沌工程(如 Chaos Mesh 的 IOChaos)在内核态使用 eBPF 实现精准注入。要求宿主机内核至少在 4.17+(推荐 5.4+ 以获得稳定的 BPF 特性)。其原理是将一段 BPF 字节码挂载到内核的 VFS(虚拟文件系统)层面上,例如通过 kprobe 拦截 vfs_readvfs_write 函数。

    当目标进程发起读写请求时,BPF 程序会被触发,强制在内核态 bpf_ktime_get_ns() 循环等待(制造延迟),或者直接修改系统调用返回值,返回 -EIO (Input/output error)(制造读写失败)。这种方式只针对特定 PID 和特定目录生效,彻底切断了对宿主机全局的干扰。

    SLO 验证闭环:用数据说话,拒绝肉眼盯盘

    GameDay(故障演练日)的核心不是制造恐慌,而是验证系统的容错边界是否符合 SLO(服务级别目标)。我们通常以 Error Budget(错误预算)消耗率为核心判定标准。

    在演练前,必须确保 Prometheus 中有定义严谨的 SLO 监控指标。例如,核心交易链路的 P99 延迟 SLO 定义为 200ms。

    # 记录规则:计算订单服务 P99 延迟
    record: job:request_latency:p99
    expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service="order-svc"}[1m])) by (le))
    

    在 GameDay 流程中,自动化脚本的逻辑应该是:

    1. 持续轮询拉取当前 P99 延迟,确认 Baseline 正常(如 50ms)。

    2. 下发 NetworkChaos,注入 150ms 延迟。

    3. 观测 P99 延迟指标是否在 1 分钟内攀升至 200ms 左右。

    4. 核心断言:断言上游 API Gateway 的 5xx 错误率是否上升。如果上游配置了合理的 100ms 超时和重试熔断,上游应用应触发熔断策略,而不会被下游彻底拖死导致线程池耗尽(防止级联雪崩)。

    GameDay 实战剧本:千万别忘了防御性恢复

    这里给出一个验证数据库主备切换的真实网络隔离注入配置片段。注意其中的 durationmode 参数,这是防御性编程在混沌工程中的体现。

    apiVersion: chaos-mesh.org/v1alpha1
    kind: NetworkChaos
    metadata:
      name: db-partition-gameday
      namespace: sre-chaos
    spec:
      action: partition
      mode: fixed
      value: "1" # 仅影响 1 个目标 Pod(爆炸半径控制)
      selector:
        namespaces:
          - production
        labelSelectors:
          "app": "mysql-cluster"
          "role": "master"
      direction: both
      target:
        selector:
          namespaces:
            - production
          labelSelectors:
            "app": "order-service"
      # 极其重要:强制 60 秒后自动恢复。严禁在没有自动恢复时间的配置下执行演练!
      duration: "60s" 
    

    排查心法:演练过程中如果发现系统挂了且无法自愈,第一反应是直接删除 Chaos 资源(kubectl delete networkchaos db-partition-gameday -n sre-chaos)。如果 chaos-controller-manager 组件本身在这个时候假死卡住了,立刻在宿主机执行兜底恢复脚本: find /proc -maxdepth 1 -regex '/proc/[0-9]+' -exec nsenter -t {} -n tc qdisc del dev eth0 root 2>/dev/null \; (强制清理节点上所有的 tc 限制,简单粗暴但救命)。

    常见问题

    Q1: 生产环境做混沌实验,如果控制面(Controller)挂了,故障一直存在怎么兜底? 控制面宕机会导致 duration 到期后无法自动清理。成熟的落地方案必须在 Node 层面部署一层“看门狗(Watchdog)”。可以写一个 DaemonSet,每 10 秒去 APIServer 检查特定 Chaos 对象是否存在,如果 APIServer 超时无响应,或者 Chaos 对象已被标记删除但底层规则还在,DaemonSet 直接在本地执行 tc qdisc delbpf-loader unload 强制清理底层规则,确保业务绝对存活。

    Q2: 使用 PodChaos 注入了 CPU 满载(Stress)故障,为什么进容器敲 top 命令看到的 CPU 使用率并没有飙升? 这是容器隔离性带来的经典视图问题。top 命令读取的是 /proc/stat,默认情况下容器内挂载的是宿主机的 /proc 系统(除非你使用了 lxcfs 这类用户态文件系统)。因此 top 看到的是整个宿主机的 CPU 状态。要确认容器是否被压满,应该在宿主机查看目标容器对应的 cgroup 统计指标:cat /sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod.slice/cpuacct.usage_percpu

    Q3: 注入 IO 故障后,为什么 Node 节点内核直接发生 Panic 重启了? eBPF 的能力虽然强大,但拦截诸如 vfs_read/write 属于非常底层的内核操作。在特定的 Linux 内核版本(尤其是一些云厂商魔改的 4.19.x 分支)中,bpf 钩子与系统现有的某些内核模块(如特定的存储驱动)会产生竞态条件。遇到内核 Panic,首先通过 kdump 捕获 vmcore,用 crash 工具查看堆栈调用树(Backtrace),通常能看到 bpf_prog_XXX 导致了空指针解引用。解决办法是:升级内核至稳定版(如 5.4.x),或改用相对高层的应用级注入方案。