Skip to content

Cilium 替换 kube-proxy 场景下 NodePort 端口重叠导致建连失败调查报告

本文记录一起测试环境 RKE2 集群中,Cilium 以 kube-proxy replacement 模式接管 NodePort 转发后出现的「访问 NodePort 偶发建连超时」故障的完整调查过程。故障表象极具迷惑性:客户端与目标节点在物理网络上完全连通,但 TCP 三次握手会随机卡死,且复现概率与客户端源端口取值强相关。最终定位到两个叠加的配置缺陷:一是 RKE2 的 NodePort 范围(30000-60000)与 Cilium 默认 NodePort 识别范围(30000-32767)不一致;二是容器网络命名空间默认的本地源端口范围(32768-60999)与 NodePort 段大面积重叠,当客户端源端口恰好落在 NodePort 段内时,返回的 SYN-ACK 被 Cilium eBPF 程序误判为新的 NodePort 请求,TCP 状态机被破坏。

本报告按「现象 / 复现 / 定位 / 根因 / 修复 / 预防」结构组织,排查手段(循环压测复现、perf 定位内核卡点、tcpdump 指数退避重传分析、源端口与 Service 列表比对)对各类 CNI 替换 kube-proxy 场景的网络疑难问题具有通用参考价值。

一、故障现象

测试环境出现两类报错,均指向 TCP 建连失败:

  • 客户端直接访问任一 NodePort 服务时,请求偶发卡死,最终报连接超时(Connection timed out)。不是每次必现,但命中率不低。
  • 集群内已经运行的业务 Pod 对外发起连接(如访问数据库)时,日志中间歇性出现 connection timeout 类错误,重试后可能成功。

两个现象的共同点:失败是概率性的,网络层 ping 与已建立的连接均正常,只有新建连接受影响。

二、复现与初步观察

2.1 循环访问复现

任选一个 NodePort Service,对节点地址发起循环 curl,复现率很高:

bash
for each in $(seq 1 10000); do
  curl http://192.168.10.13:35082/
done

观察结果:大部分请求正常返回,其中部分请求会卡在建连阶段(connect 阶段阻塞),直到 TCP 超时。卡住时请求没有进入 HTTP 阶段,说明问题发生在三次握手,与应用无关。

2.2 perf 定位卡点

用 perf 包住每次 curl,确认卡顿时进程阻塞在内核的哪个位置:

bash
for each in $(seq 1 10000); do
  perf record -g -o $each.perf -- curl http://192.168.10.13:35082/
done

对卡住的采样做 perf report -i <n>.perf,调用栈显示进程阻塞在 poll 系统调用上等待 connect 完成——即 SYN 发出后迟迟等不到 SYN-ACK,典型的「对端无响应」形态。

2.3 抓包分析:TCP 指数退避重传

在客户端侧抓包(tcpdump -i any -nn port 35082),卡住时的报文序列如下:

text
14:32:01.120310 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0
14:32:02.121988 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0
14:32:04.125741 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0
14:32:08.132916 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0
14:32:16.148802 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0
14:32:32.180233 IP 192.168.10.13.44359 > 192.168.10.13.35082: Flags [S], seq 2941035782, win 64240, length 0

关键特征:

  • 只有客户端发出的 SYN,没有任何 SYN-ACK 或 RST 返回。
  • SYN 重传间隔严格按 1s、2s、4s、8s、16s 指数退避,这是内核 TCP 协议栈在对端完全无响应时的标准重传行为。

这个特征指向一个违反直觉的结论:从 TCP 协议视角看,这两个 IP:PORT 之间"不连通"。但测试环境的物理网络上两点必然连通,且大部分握手能成功。因此怀疑方向转向内核协议栈之外的路径——Cilium 用 eBPF 在内核之外接管了 NodePort 流量的处理,对 TCP 报文的劫持、改写是否存在缺陷。

2.4 关键线索:本地源端口与 Service NodePort 列表重合

多抓几次卡住的样本做比对,发现一条决定性规律:当本次连接的客户端本地源端口恰好等于集群中某个 Service 的 NodePort 时,建连必定失败;源端口不在 NodePort 列表内时,建连必定成功。

用失败样本中的源端口号反查 Service 列表验证:

bash
kubectl get svc -A | grep 32769
default             risk-h5-service    NodePort    10.43.100.180   <none>    8075:32769/TCP

kubectl get svc -A | grep 44359
default             wms-service        NodePort    10.43.68.119    <none>    7004:44359/TCP

两个失败样本的源端口 32769、44359 都精确命中现有 Service 的 NodePort。至此基本锁定:问题出在 Cilium 对 NodePort 相关流量的识别与处理逻辑上,而触发条件是「本地源端口落入 NodePort 端口段」。

三、定位(一):主机网络命名空间的端口范围不一致

3.1 RKE2 与 Cilium 的 NodePort 范围配置

测试集群为 RKE2 部署,kube-apiserver 的 Service NodePort 范围配置为 30000-60000,且部署时关闭了 kube-proxy,NodePort 转发全部由 Cilium 的 kube-proxy replacement(eBPF)实现。

Cilium 侧通过 Helm 部署:

bash
helm3 upgrade --install cilium -f values.yaml -f custom-values.yaml ./ -n kube-system \
  --set image.repository=harbor.example.com/cilium/cilium \
  --set image.useDigest=false \
  --set nodePort.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.backend.image.repository=harbor.example.com/cilium/hubble-ui-backend \
  --set hubble.ui.standalone.enabled=true \
  --set hubble.ui.backend.image.tag=v0.13.2 \
  --set hubble.ui.backend.image.useDigest=false \
  --set hubble.ui.frontend.image.tag=v0.13.2 \
  --set hubble.ui.frontend.image.useDigest=false \
  --set hubble.ui.frontend.image.repository=harbor.example.com/cilium/hubble-ui

问题就出在这里:Helm 部署时没有指定 nodePort.range,Cilium 使用默认值 30000-32767。Cilium 官方文档(kube-proxy free 章节)明确要求:Cilium 的 NodePort 范围必须与 kube-apiserver 配置的 --service-node-port-range 保持一致。当前集群实际是 30000-60000,两边不一致,32768 以上的 NodePort 流量无法被 Cilium 正确识别。

3.2 修复与验证

第一步,更新 Cilium 部署参数,把 NodePort 范围对齐到 30000-60000:

bash
helm3 upgrade --install cilium -f values.yaml -f custom-values.yaml ./ -n kube-system \
  --set kubeProxyReplacement=true \
  --set nodePort.enabled=true \
  --set nodePort.range="30000\,60000" \
  ...(其余参数同前)

或写入 values.yaml:

yaml
kubeProxyReplacement: true
nodePort:
  enabled: true
  range: "30000,60000"

第二步,在每个节点上把 NodePort 段加入内核预留端口,防止主机上的本地连接把 NodePort 段用作源端口:

bash
# /etc/sysctl.d/99-nodeport.conf
net.ipv4.ip_local_reserved_ports = 30000-60000
bash
sysctl --system

完成后继续执行 2.1 节的循环 curl 压测,直接访问 NodePort 的超时错误消失。

四、定位(二):容器网络命名空间的源端口重叠

4.1 业务 Pod 仍报连接超时

主机侧修复后不久,开发反馈业务 Pod 内仍有间歇性建连失败,典型报错是数据库连接超时。沿用之前的思路检查容器网络命名空间的端口参数,发现:

bash
# 在 Pod 内或通过 nsenter 进入容器 netns 查看
sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999

sysctl net.ipv4.ip_local_reserved_ports
net.ipv4.ip_local_reserved_ports = 35357

容器的本地源端口范围是 32768-60999,与 NodePort 段 30000-60000 存在 32768-60000 的大面积重叠。容器作为客户端发起连接时,内核从 32768-60999 中随机挑源端口,一旦挑中 30000-60000 段内且已被分配为 NodePort 的端口,就触发与主机侧相同的建连失败。同时注意到 ip_local_reserved_ports 是 per-netns 变量,主机上设置的预留端口不会传递到容器命名空间。

4.2 容器默认源端口范围的内核来源

为什么容器内是 32768-60999?查阅 RHEL 9.2 对应内核源码(linux-5.14.0-362.24.1.el9_3/net/ipv4/af_inet.c),这是内核在初始化每个网络命名空间时硬编码的初始值:

c
static __net_init int inet_init_net(struct net *net)
{
    ...
    net->ipv4.ip_local_ports.range[0] = 32768;
    net->ipv4.ip_local_ports.range[1] = 60999;
    ...
}

即每个新创建的 netns(包括每个 Pod)的 ip_local_port_range 都初始化为 32768-60999。在社区默认参数下(NodePort 30000-32767),两个区间在 32768 处恰好首尾相接、互不重叠,所以默认部署几乎永远不会暴露这个问题;一旦集群把 NodePort 范围扩大到 32768 以上(本例 30000-60000),重叠区就出现,建连失败的机会随之产生。

4.3 修复:initContainer 调整容器内核参数

参考 RHEL 9 上对 TCP 时间戳等 per-netns 参数的处理惯例,通过 privileged initContainer 在业务容器启动前调整 Pod 网络命名空间的源端口范围,将其整体移出 NodePort 段:

bash
kubectl patch deploy <deploy-name> --patch '{"spec":{"template":{"spec":{"initContainers":[{"command":["sysctl","-w","net.ipv4.ip_local_port_range=10000 30000"],"image":"harbor.example.com/library/busybox","imagePullPolicy":"IfNotPresent","name":"sysctl","terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","securityContext":{"runAsUser":0,"privileged":true}}]}}}}'

initContainer 与业务容器共享网络命名空间,在 init 阶段执行的 sysctl -w 对后续业务容器生效。patch 触发滚动更新后,业务侧的数据库连接超时等报错消失,问题最终解决。

声明式写法(推荐固化到 Deployment YAML):

yaml
spec:
  template:
    spec:
      initContainers:
        - name: sysctl
          image: harbor.example.com/library/busybox
          imagePullPolicy: IfNotPresent
          command: ["sysctl", "-w", "net.ipv4.ip_local_port_range=10000 30000"]
          securityContext:
            runAsUser: 0
            privileged: true

五、根因分析

故障的触发链条归结为 Cilium eBPF 对 TCP SYN 报文的识别缺陷,叠加两处端口段重叠:

  1. Cilium 的 kube-proxy replacement 用 eBPF 程序在内核原生 TCP 协议栈之外接管 NodePort 流量的 DNAT 与转发。eBPF 钩子挂载在网卡驱动层,报文到达时尚未进入 conntrack,没有方向性信息——eBPF 程序无法直接区分"这是一个发往 NodePort 的新 SYN 请求"还是"这是一条已有连接的 SYN-ACK 回程报文"。
  2. 为弥补方向性缺失,Cilium 用端口段做启发式判断:报文涉及的端口落在配置的 NodePort 范围内,就按 NodePort 流量处理。
  3. 于是形成缺陷场景:当客户端 socket 的本地源端口恰好落在 NodePort 段内(且该端口已分配给某个 Service)时,对端返回的 SYN-ACK(其源端口是服务端口、目的端口是客户端本地源端口)被 eBPF 程序依据端口段规则误判为新的 NodePort SYN 请求,被劫持改写。两端点的 TCP 状态机被破坏,三次握手永远无法完成,应用层表现为 connect 超时。
  4. 端口段重叠由两处配置错误共同放大:
    • Cilium nodePort.range 未与 RKE2 的 30000-60000 对齐(默认 30000-32767),导致 32768 以上的 NodePort 流量识别异常;
    • 主机/容器 netns 的 ip_local_port_range(32768-60999)与扩大后的 NodePort 段(30000-60000)大面积重叠,使"源端口落入 NodePort 段"成为高概率事件。

这也解释了故障的概率性特征:每次建连的源端口由内核在 32768-60999 中随机选取,只有选中重叠区且已被占用的端口才失败,表现为随机偶发。

六、修复方案汇总

三种手段配合使用,缺一不可:

层面措施说明
Cilium 配置nodePort.range: "30000,60000" 与 kube-apiserver 的 --service-node-port-range 严格对齐保证 eBPF 按正确的端口段识别 NodePort 流量
主机内核参数每个节点 net.ipv4.ip_local_reserved_ports = 30000-60000把 NodePort 段从主机本地源端口中整段预留,杜绝主机上源端口与 NodePort 撞车
容器内核参数privileged initContainer 执行 sysctl -w net.ipv4.ip_local_port_range="10000 30000"把 Pod 内源端口范围整体移出 NodePort 段;注意该参数是 per-netns,主机设置不会传入容器

七、经验教训与预防 checklist

CNI 以 kube-proxy replacement 模式接管 Service 转发时,端口段规划必须在部署阶段一次做对。上线前 checklist:

  • [ ] 确认 kube-apiserver 的 --service-node-port-range,并让 Cilium 的 nodePort.range 与之完全一致(Cilium 官方文档明确要求,默认值 30000-32767 只在集群也用默认范围时才安全)。
  • [ ] 全节点设置 net.ipv4.ip_local_reserved_ports 覆盖整个 NodePort 段,写入 /etc/sysctl.d/ 持久化,并纳入新节点入网初始化脚本。
  • [ ] 评估容器侧源端口范围与 NodePort 段是否重叠:NodePort 上界只要超过 32767,就与容器默认 ip_local_port_range(32768-60999)重叠,必须通过 initContainer 调整,或把 NodePort 范围收缩到 30000-32767 以内。
  • [ ] 自定义 NodePort 范围(如 30000-60000)属于非默认配置,写入集群部署文档,防止后续扩容节点或升级 CNI 时遗漏。
  • [ ] 出现"网络连通但建连概率性超时"时,优先比对失败连接的源端口与 Service NodePort 列表,这是此类问题最快的判别方法。

八、故障速查表

故障现象可能原因排查与处理
访问 NodePort 概率性 connect 超时,抓包见 SYN 以 1/2/4/8/16s 指数退避重传、无任何回包客户端源端口落入 NodePort 段,SYN-ACK 被 Cilium eBPF 误判劫持用失败连接的源端口 kubectl get svc -A | grep <port> 比对;命中即按第六章三件套修复
32768 以上的 NodePort 完全不通,30000-32767 正常Cilium nodePort.range 为默认 30000-32767,未与集群实际范围对齐--set nodePort.range="<min>\,<max>" 更新 Helm 部署并重启 Cilium agent
主机侧修复后,Pod 内访问外部服务(如数据库)仍间歇超时容器 netns 的 ip_local_port_range(默认 32768-60999)与 NodePort 段重叠为 Deployment 注入 privileged initContainer 调整 ip_local_port_range
主机设置了 ip_local_reserved_ports,容器内源端口仍落在 NodePort 段ip_local_reserved_ports 是 per-netns 参数,不传递到容器命名空间容器侧必须单独用 initContainer 调整
升级/重装 Cilium 后故障复发Helm 参数未固化在 values.yaml,升级时丢失nodePort.range 等关键参数写入 values.yaml 纳管,禁止只靠命令行 --set