一个 Pod 吃掉整节点带宽?K8s 限速 annotation 无效——TC/CNI 流量整形真相

场景:CI/CD Runner Pod 与生产服务混部,构建任务下载依赖占满节点出口带宽 路径:Pod 延迟确认 → 逐层追带宽(Pod→Node→CNI→tc)→ 排查命令 → annotation 无效真相 → 限速修复 + Check-list 时长:~12 分钟 以下排查基于 K8s v1.28、Calico v3.27、Linux kernel 5.15

上篇讲了 CoreDNS 自动扩缩容参数设反导致 DNS 间歇超时,这篇来看另一个更容易被忽视的节点级问题——带宽。

我盯着监控面板看了 10 分钟——CPU 25%,内存 30%,Pod 全 Running——但用户说页面打不开。一个 CI runner Pod 跑构建任务时疯狂下载依赖包,同一节点的 Web 服务 P99 从 20ms 飙到 5s。"资源不够吧?"kubectl top pod 一看 CPU 内存都正常。问题不在计算资源,在网络带宽——K8s 有 CPU/Memory 调度,但没有"带宽调度"这个概念。

场景:CI/CD Runner Pod 与生产服务混部,构建任务下载依赖占满节点出口带宽 路径:Pod 延迟确认 → 逐层追带宽(Pod→Node→CNI→tc)→ 排查命令 → annotation 无效真相 → 限速修复 + Check-list

构建任务一启动,同节点上其他 Pod 的接口响应就飙升。

# 同节点跨 Pod 延迟测试
$ kubectl exec -n production web-pod-7f8c9 -- ping -c 5 web-pod-7f8c9-peer
PING web-pod-7f8c9-peer (10.244.1.5): 56 data bytes
64 bytes from 10.244.1.5: icmp_seq=1 ttl=64 time=3420 ms
64 bytes from 10.244.1.5: icmp_seq=2 ttl=64 time=4100 ms
64 bytes from 10.244.1.5: icmp_seq=3 ttl=64 time=2890 ms
--- web-pod-7f8c9-peer ping statistics ---
3 packets transmitted, 3 received, 0% loss, 3000+ms avg

同节点 Pod 之间 ping 延迟 > 3s,正常应该 < 1ms。这不是 Pod 本身的问题——是共享网卡被打满了。

kubectl describe pod runner 看不到任何异常事件,Pod 状态 Running。因为带宽不是 K8s 调度的资源,scheduler 不感知、kubelet 不报告、Events 不记录——带宽问题在内核层面,K8s 控制面完全不可见。

同节点 Pod 延迟对比

分层:从 Pod 网卡到内核 tc,逐层追流量

K8s 为什么不调度带宽?因为带宽不像 CPU 和内存那样可以通过 cgroup 做硬隔离和配额——带宽是共享介质,同一张网卡上的所有流量走同一个队列,内核无法像 cgroup 那样按 Pod 颗粒度做资源划分。所以 K8s 干脆不碰带宽,把这个难题留给了 CNI 插件和内核 TC(Traffic Control,流量控制)子系统。

每张网卡上都有一组 qdisc(queueing discipline,排队规则),决定数据包的发送顺序和速率。

Pod 层:谁在吃带宽?

进入 Pod 网络命名空间看实际吞吐:

$ kubectl exec -n ci runner-pod-5j2k3 -- nethogs eth0
NetHogs version 0.8.5

    PID USER     PROGRAM                         SENT     RECEIVED
  12345 root     pip install                      85.3 MB  12.1 MB
  12346 root     wget -O /tmp/cache.tar.gz        120.5 MB 3.2 MB
                                                TOTAL: 205.8 MB  15.3 MB

一个 pip install + wget 就把 pod 出口带宽吃到了接近 200Mbps。如果节点的出口带宽是 1Gbps,单个 Pod 理论上能吃到 1Gbps。

Node 层:tc qdisc 看看限速了没有

退出 Pod 视角,在宿主机上看网卡队列规则:

$ tc -s qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 892143875 bytes 1234567 pkt (dropped 0, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0

默认 qdisc 是 pfifo_fast——先入先出,不做任何限速。节点上有多少 Pod 共用 eth0,谁抢到带宽就是谁的。CI/CD Pod 发得多,生产 Pod 的包就被挤到队列后面。

tc -s qdisc 查看队列规则

CNI 层:bandwidth annotation 实现了吗?

K8s 提供了两个带宽 annotation:kubernetes.io/egress-bandwidthkubernetes.io/ingress-bandwidth。在 Pod YAML 加上:

metadata:
  annotations:
    kubernetes.io/egress-bandwidth: "10M"
    kubernetes.io/ingress-bandwidth: "10M"

但是——这两行 annotation 不是 K8s 核心功能。它们只是一个"建议",需要 CNI 插件来实现。kubelet 读到 annotation 后会通过 CNI 接口传递给 CNI 插件。CNI 插件(如 Calico)的 bandwidth 组件会创建 htb qdisc 并挂到 Pod 的 veth 上。

验证 CNI 是否实现了 bandwidth:

# 在宿主机上看 Pod 的 veth pair
$ ip link show | grep veth
veth123abc@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast ...

$ tc class show dev veth123abc
# 没有输出 —— CNI 没注入限速规则!

如果你的 CNI 没实现 bandwidth plugin(Flannel、kube-router 默认不支持,Calico 默认也不开),加 annotation 等于没加——内核 tc 上根本不生成限速规则。

带宽排查分层架构图

内核层:htb qdisc 才是限速的关键

如果 CNI 实现了 bandwidth,它会在 Pod 的 veth 上创建一个 htb(Hierarchy Token Bucket)qdisc:

$ tc class show dev veth123abc
class htb 1:1 root rate 10Mbit ceil 10Mbit burst 15Kb cburst 15Kb

htb 是一个层级令牌桶——它允许创建多个 class,每个 class 有自己的 rate(保证带宽)和 ceil(最大带宽)。CNI bandwidth 插件会在 Pod 的 veth 根上挂一个 htb,限制这端流量不超过 annotation 设定的值。

当 Pod 发送数据时,内核 TC 子系统会在 htb qdisc 检查令牌桶——令牌不够就丢包或延迟。这才是有带宽限制的效果。

路径:下次先跑这套排查命令

诊断带宽问题不要每次都从 kubectl 开始——先到节点上跑 tc。以下三步,每一步对应一个明确的诊断结论。

Step 1 — 先看有限速吗?

在节点上直接看网卡队列规则。这条命令告诉你"节点层面有没有人在管带宽":

$ tc -s qdisc show dev eth0

# 输出 A:默认队列 → 无限速,所有 Pod 共享带宽
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 ...
 Sent 892 MB 1234567 pkt (dropped 0, overlimits 0)

# 输出 B:htb 队列 → CNI bandwidth 已生效
qdisc htb 1: root refcnt 2 r2q 10 default 0 direct_packets_stat 0
 Sent 567 MB 10000 pkt (dropped 0, overlimits 456)

pfifo_fast 意味着内核在做先入先出,不做任何限速——谁发得多谁占优。htb 证明已经有人(CNI)注入了限速规则,这时应该看具体限速值够不够。

Step 2 — 谁在吃带宽?

节点上跑 nethogs,10 秒内锁定肇事进程:

$ sudo nethogs eth0
NetHogs version 0.8.5

    PID USER     PROGRAM                         SENT     RECEIVED
  12345 root     pip install                      85.3 MB  12.1 MB
  12346 root     wget -O /tmp/cache.tar.gz        120.5 MB 3.2 MB

如果 PID 是个构建工具(pip/wget/curl),说明是 CI/CD 类任务在抢带宽。如果 PID 是业务进程,检查是不是数据同步逻辑出了问题——不同原因对应不同解决方案(应用层限速 vs 节点层隔离)。

Step 3 — Pod veth 上有限速吗?

这是最关键的诊断:加了 annotation 到底有没有生效?验证方法——找到 Pod 对应的 veth pair,看它的 tc class:

$ POD_IFINDEX=$(kubectl exec runner-pod-5j2k3 -- cat /sys/class/net/eth0/ifindex)
1234

$ ip link | grep "if1234"
veth1234@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast

$ tc class show dev veth1234
# 空输出 → CNI 没注入规则,annotation 无效

# 如果 CNI bandwidth 生效,会看到:
class htb 1:1 root rate 10Mbit ceil 10Mbit burst 15Kb cburst 15Kb

这里有一个常见的误解:有人会用 kubectl describe pod 查看 annotations 字段,看到 kubernetes.io/egress-bandwidth: 10M 就认为限速生效了。不对——annotation 只代表 kubelet 收到了这个标记,不代表 CNI 把它翻译成了 tc 规则。真正有效的证明是 tc class show 输出了 htb class。

三步诊断对照表

现象 Step 1 eth0 qdisc Step 3 veth class 结论
带宽打满 pfifo_fast 或 htb CNI 没开启 bandwidth → 修 CNI 配置
带宽打满 pfifo_fast htb 有速率值 CNI 配了但限速值太高 → 调小 rate
带宽打满 htb htb 节点带宽不够 → 隔离/扩容
带宽正常 任意 任意 不是带宽问题 → 排查别的方向

三步走完,你不仅能定位根因,还能直接得出结论是修 CNI 配置还是调参数——不需要猜。

定位:加了 annotation ≠ 限速生效

大多数人的做法:在 Pod YAML 加上 kubernetes.io/egress-bandwidth: "10M",认为带宽就限住了。发现没效果,怀疑 K8s 版本问题、kubelet 配置没开、或者干脆 reboot 节点。

正确的排查思路:先确认 CNI 是否实现了 bandwidth plugin。

检查三件事:

检查项 方法 通过标准
CNI 插件支持 cat /etc/cni/net.d/*.conf 看配置文件中是否引用了 bandwidth 插件
Calico 的配置 calicoctl get felixconfig -o yaml bandwidth 功能已开启
运行时验证 创建带 annotation 的 Pod 后,宿主机上 tc class show dev vethXXX 出现 htb class

如果你的 CNI 不支持 bandwidth(Flannel、Cilium 默认,或 Calico 没开),加 annotation 就是白加——内核 tc 上根本不生成规则。

以 Calico 为例,启用 bandwidth 插件需要在 FelixConfiguration 中开启:

apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  bandwidthRateLimiting: true

这条配置让 Calico 在创建 Pod veth pair 时,挂上一个 htb qdisc 并应用 annotation 指定的速率限制。

区别的本质:annotation 是"告诉 kubelet 我想要限速"——但真正执行限速的是 CNI 的 bandwidth 组件 + 内核 tc 的 htb qdisc。中间链路断了,annotation 就是一行注释。

错误 vs 正确排查对比

标点:三种限速方案 + Check-list

方案 A:CNI bandwidth plugin(推荐)

前提是你的 CNI 支持 bandwidth 插件。Calico 开启后,所有 Pod 的 annotation 自动生效——最干净,不需要碰节点配置。

方案 B:节点级 tc 脚本

CNI 不支持时,直接在宿主机的 bridge 上挂 tc 规则。用 cgroup classid 匹配(与 CNI bandwidth 同样的方式),Pod 重建后 classid 不变——比 IP 匹配更鲁棒:

# 找到 Pod 的 cgroup classid
POD_CLASSID=$(cat /proc/$(kubectl exec <pod> -- cat /proc/1/status | grep PPid | awk '{print $2}')/cgroup | grep net_cls | cut -d: -f3)
# 输出如 0x100001

# 创建 htb qdisc + filter 按 classid 匹配
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 1Gbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 100Mbit ceil 100Mbit
tc filter add dev eth0 protocol ip parent 1:0 prio 1 cgroup matchall flowid 1:10

方案 B 的缺点:脚本要管理 Pod 生命周期,重连后自动重建。

方案 C:应用层限速

在代码层用 token bucket 算法限速(如 Go 的 rate.Limiter、Java 的 RateLimiter)。最灵活但最侵入——改代码、发版、排查,周期太长。

Check-list

  • [ ] nethogs / iftop 确认哪条链路带宽被打满
  • [ ] tc -s qdisc show dev <iface> 确认当前有无限速规则
  • [ ] cat /etc/cni/net.d/*.conf 检查 CNI 是否引用了 bandwidth 插件
  • [ ] Calico 环境:calicoctl get felixconfig -o yaml | grep bandwidthRateLimiting
  • [ ] 创建带 annotation 的 Pod 后,宿主机 tc class show dev vethXXX 验证生效
  • [ ] 验证限速后:iperf3 -c <target-ip> 确认速率在限制范围内

K8s 不是没有告诉你带宽被谁吃了——tc 已经记了每张网卡的队列情况,只是你没去查

下篇我们聊 Cilium/eBPF CNI 模式下的网络排障特有工具链——当你用 Cilium 时,tciptables 都看不到真实流量,得用 cilium monitorbpftrace 代替。


附:完整命令清单

# 带宽排查
nethogs eth0                             # 节点上按进程看带宽
iftop -i eth0                            # 节点上按连接看带宽
tc -s qdisc show dev eth0                # 看队列规则(pfifo_fast=无限速,htb=有限速)
tc class show dev eth0                   # 看限速规则的具体速率值
tc class show dev vethXXX                # 看 Pod veth 上的限速规则

# Pod 网络诊断
kubectl exec <pod> -- nethogs eth0       # Pod 内看带宽占用
kubectl exec <pod> -- ss -i              # Pod 内看 socket 延迟
kubectl exec <pod> -- ping <peer>        # 跨 Pod 延迟

# CNI 配置检查
cat /etc/cni/net.d/*.conf                # CNI netconf 插件链
calicoctl get felixconfig -o yaml | grep bandwidth  # Calico bandwidth 开关

# 临时注入带宽限制
tc qdisc add dev vethXXX root tbf rate 10mbit burst 32kbit latency 50ms

# 节点实时监控
sar -n DEV 1 5                           # 网卡吞吐量
watch -n 1 'tc -s qdisc show dev eth0'   # 实时看队列统计