Cilium 实战:用 eBPF 数据面替代 kube-proxy 踩坑记

Cilium 是基于 eBPF 的云原生网络方案,能用 eBPF 数据面彻底替代 kube-proxy,带来更低的网络延迟与更强的可观测性。但在把生产集群从传统的 iptables 模式切换到 Cilium 时,我们连续踩了 DNS 解析失败、ClusterIP 不通、Hubble 观测缺失三个坑。本文完整复盘排查过程与回滚方案,帮你在落地 eBPF 数据面前少走弯路。

一、为什么要用 eBPF 数据面替代 kube-proxy

传统 Kubernetes 集群依赖 kube-proxy 在每节点用 iptables(或 IPVS)规则实现 Service 转发。当 Service 数量上千时,iptables 规则链线性增长,新建连接和规则同步都会变慢。Cilium 则把转发逻辑下沉到 eBPF 程序,直接在内核态完成 Service 负载均衡、网络策略与可观测采集,跳过 iptables 这条慢路径。

我们最初决定迁移,并不是因为”追新”,而是被真实的延迟指标逼的。内部压测显示,当集群 Service 数量从 200 涨到 2000 时,iptables 模式下新建连接的 P99 延迟从 3ms 一路涨到 18ms,而 Cilium eBPF 模式基本稳定在 4ms 上下不波动。对支付、交易、实时推荐这类延迟敏感的服务来说,这十几毫秒的差距会直接体现在业务成功率和用户体感上。更诱人的是可观测性:Cilium 自带的 Hubble 能在不注入任何 sidecar 的前提下,给出流级别的 DNS、HTTP、TCP 明细,排障体验是质的飞跃。

但必须泼一盆冷水:eBPF 绝不是银弹。它把复杂度从”用户态规则下发”转移到了”内核态程序正确性 + 内核版本兼容性”上。它对内核版本、节点内核参数、甚至某些老网卡驱动都有要求,对运维的排障能力也提出了更高要求。迁移前必须把功课做足,否则就会像我们一样,上线当天连踩三个坑,半个核心服务受影响。

维度kube-proxy(iptables)Cilium(eBPF)
转发路径用户态下发、内核 iptables 匹配内核态 eBPF 直接转发
大规模 Service 延迟规则越多越慢与规模基本无关
网络策略需搭配 NetworkPolicy 实现原生 L3/L4/L7 策略
可观测性需额外 sidecar 或代理Hubble 原生流级观测
内核要求建议 ≥ 5.10

收益很明显,但 eBPF 对内核版本和节点环境更挑剔,迁移过程稍有不慎就会引发连锁故障。下面把我们的三次踩坑逐一拆开。

二、迁移前的环境与内核检查

动手之前,先确认内核版本、Cilium 组件状态与 kube-proxy 当前模式。我们的集群是 1.28,内核 5.15,满足 eBPF 数据面要求。这里多说一句:内核版本是 eBPF 数据面能否”完整启用”的决定性因素。低于 4.19 的内核几乎无法运行 kube-proxy-replacement;4.19 到 5.9 之间部分特性(如基于 eBPF 的 masquerade、socket 级负载均衡)会降级或不可用;5.10 以上才比较省心,5.15+ 则几乎全特性可用。如果你的节点还跑着 3.x 的老内核,先升级内核再谈迁移。

除了内核,还要确认节点没有通过 seccomp、AppArmor 或某些安全加固策略拦截 bpf 系统调用,否则 cilium-agent 会卡在加载程序阶段。最稳妥的做法是用 Cilium 官方提供的 preflight 检查,它会逐项校验节点是否具备 BPF、socket LB、DSR 等能力,把”能不能上”这件事在迁移前就讲清楚。

# 内核版本(建议 >= 5.10,部分特性需 >= 5.15)
uname -r
# 4.18.0 老内核需开启特定补丁,5.10+ 体验最佳

# 添加 chart 并查看待安装版本
helm repo add cilium https://helm.cilium.io/
helm search repo cilium --versions | head

# 确认 kube-proxy 当前是否以 iptables 模式运行
kubectl -n kube-system get pods -l k8s-app=kube-proxy
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=5 | grep -i mode

# 用官方 preflight 镜像校验节点 eBPF 能力
helm install cilium-preflight cilium/cilium --namespace kube-system \
  --set preflight.enabled=true \
  --set agent=false \
  --set operator.enabled=false
kubectl -n kube-system wait --for=condition=ready pod -l app=cilium-preflight
kubectl -n kube-system logs -l app=cilium-preflight | grep -i "all-checks"

三、踩坑一:CoreDNS 解析大面积失败

现象

切到 kube-proxy-replacement 后,业务 Pod 解析内部域名(如 mysql.default.svc)大面积超时,但 cluster-local 的 IP 直连正常,kube-dns 的 ClusterIP 本身也能 ping 通。监控里 DNS 成功率在切换后 3 分钟内从 99.9% 跌到 40%,告警瞬间被打爆。最迷惑人的是:用 kubectl exec 进 CoreDNS Pod 自己 dig 是正常的,说明 CoreDNS 进程没挂,问题出在”流量怎么到达 CoreDNS”这条路径上。

根因

Cilium 在替换 kube-proxy 后,Service 翻译改由 eBPF 完成。eBPF 数据面维护一张由 cilium-agent 从 Kubernetes Service/EndpointSlice 同步而来的后端映射表(即 service map)。问题在于:Cilium 启动后需要一段时间完成首次全量同步,这段”预热”窗口内,CoreDNS 这个关键 Service 的后端可能还没写入映射表,DNS 请求就会被 eBPF 程序判定为”无后端”而直接 DROP。这不是 CoreDNS 的 bug,而是数据面切换的时序问题。我们用 Hubble 的 DNS verdict 观测,5 分钟就定位到是 DROPPED 而非 TIMEOUT——如果是 TIMEOUT 大概率是 CoreDNS 本身问题,DROPPED 则明确指向数据面没把流量送过去。

排查与修复

核心教训只有一句话:永远等 cilium status --wait 报告全部组件健康、所有 Service 已同步完成之后,再切流量。我们当时为了赶发布窗口,装完就改 kube-proxy 模式,踩了预热时差的坑。修复就是补一次完整同步,并给 kube-system 显式放行 DNS 的集群级策略,避免任何兜底的网络策略把这 53 端口流量误杀。

# 等所有组件真正健康再放行流量,别一装完就切流量
cilium status --wait

# 用 Hubble 观察 DNS 流,定位被丢弃的请求
cilium hubble port-forward&
hubble observe --namespace kube-system --protocol DNS --last 50

# 确认 kube-dns 后端已被 eBPF 识别
kubectl -n kube-system exec -it ds/cilium -- cilium service list | grep kube-dns

# 若仍异常,给 kube-system 放行 DNS 的集群级策略(示例)
kubectl apply -f - <<'EOF'
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: allow-kube-dns
spec:
  endpointSelector: {}
  egress:
  - toPorts:
    - ports:
      - port: "53"
        protocol: UDP
      - port: "53"
        protocol: TCP
EOF

四、踩坑二:NodePort / ClusterIP 偶发不通

根因

我们一开始用的是 permissive(宽松)模式,也就是 kube-proxy 和 Cilium 同时在工作。两套转发规则打架:部分连接走了 Cilium 的 eBPF 路径,部分仍被旧 iptables 规则截获,表现为”时通时不通”。尤其是 NodePort 在跨节点访问时,masquerade(地址伪装)行为不一致导致源地址被错误改写,后端收到请求后回包找不到正确路径,连接就卡在半开状态。这种故障最恶心的地方在于它是概率性的——压测小流量时一切正常,真实流量一上来就随机失败,靠猜是永远猜不到根因的。

修复

正确做法是:等 Cilium 完全健康后,彻底停掉 kube-proxy,并把替换模式设为 strict。这样全集群只保留一条数据面,消除规则冲突。permissive 模式本意是方便灰度共存,但它要求你对两套规则的交互有极强掌控力;对绝大多数团队而言,健康的 Cilium 起来后,就应该干净利落地让 kube-proxy 退场,而不是长期双栈运行。值得强调:下线 kube-proxy 前务必确认 cilium status --wait 没有报错,否则会出现”旧规则已删、新规则未接管”的真空期。

# 1) 安装/升级时明确启用严格替换
helm upgrade cilium cilium/cilium --namespace kube-system \
  --reuse-values \
  --set kubeProxyReplacement=true \
  --set kubeProxyReplacementMode=strict \
  --set enable-bpf-masquerade=true

# 2) Cilium 健康后,彻底下线 kube-proxy
kubectl -n kube-system scale deployment kube-proxy --replicas=0

# 3) 验证 Service 转发已完全由 eBPF 接管
cilium status --wait
kubectl get svc --all-namespaces | wc -l
# 用临时 Pod 持续探活,确认不再偶发超时
kubectl run netcheck --rm -it --image=busybox -- wget -qO- http://kubernetes.default.svc:443

五、踩坑三:Hubble 观测数据为空

切换到 Cilium 的一大卖点就是 Hubble 流级可观测,但 UI 打开后一片空白,Flow 面板没有任何记录。排查发现我们只装了 cilium-agent,没启用 Hubble relay 与 metrics,UI 拿不到数据。这里要理清 Hubble 的组件关系:cilium-agent 每个节点本地采集流日志;hubble-relay 负责把各节点的流聚合起来;hubble-ui 提供可视化;而 metrics 则需要单独打开对应的采集项。我们只开了 agent,没开 relay 和 metrics,UI 自然空空如也。换句话说,Hubble 不是”装了就有”,而是一组需要显式开启的组件。

# 重新开启 Hubble 全套组件
helm upgrade cilium cilium/cilium --namespace kube-system \
  --reuse-values \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true \
  --set "hubble.metrics.enabled={dns,drop,tcp,flow,icmp,http}"

# 暴露 UI(按需改 ClusterIP -> NodePort / 用 port-forward)
cilium hubble port-forward&
# 浏览器访问 http://localhost:12000 即可看到实时流

# 命令行直接看被丢弃的流量,排查网络策略问题极好用
hubble observe --verdict DROPPED --last 20

六、灰度策略与安全回滚

eBPF 数据面一旦出错影响的是全集群网络,务必灰度。我们的做法分三步:第一步,先在单个非核心命名空间开启 Cilium 网络策略,观察 24 小时无异常再扩大范围;第二步,kube-proxy 下线不要一步到位,可以先用节点污点把新节点单独圈出来跑 Cilium,老节点保留 iptables 路径做对照;第三步,也是最重要的——永远保留 kube-proxy 的回滚通道,确保出问题能在 1 分钟内切回。迁移期间我们专门盯了三个看板:DNS 成功率、Service 对外连通率、Hubble DROPPED 计数,任何一个突然恶化就立即回滚,绝不硬扛。

# 回滚:关闭替换、拉起 kube-proxy 即可恢复原 iptables 路径
helm upgrade cilium cilium/cilium --namespace kube-system \
  --reuse-values \
  --set kubeProxyReplacement=false \
  --set hubble.enabled=false

kubectl -n kube-system scale deployment kube-proxy --replicas=1
kubectl -n kube-system rollout status deployment/kube-proxy

# 回滚后务必验证基础网络
cilium status || true
kubectl run netcheck2 --rm -it --image=busybox -- wget -qO- http://kubernetes.default.svc:443

七、小结

Cilium 用 eBPF 数据面替代 kube-proxy 是云原生网络的大势所趋,延迟与可观测性收益显著。但迁移绝对不是”helm install 完就行”。把这次踩坑浓缩成一张清单,供你上线前逐条核对:① 内核 ≥ 5.10,迁移前跑 preflight 校验;② 必须等 cilium status --wait 全部健康、Service 同步完成再切流量;③ 用 strict 模式彻底移除 kube-proxy,杜绝双栈规则冲突;④ 别忘开启 Hubble relay 与 metrics,否则可观测性归零;⑤ 始终保留 kube-proxy 秒级回滚通道,并盯紧 DNS 成功率与 DROPPED 计数。把这五条提前规避,你的迁移会平滑很多,也省下我们当晚的紧急救火。

如果你还在熟悉 Kubernetes 基础概念,建议先读 K8s 入门:Pod、Deployment、Service;想进一步做流量治理可看 Istio 服务网格入门,弹性伸缩参考 K8s HPA 实战,日常排障用 k9s 终端管理 效率翻倍。

上一篇 Java 设计模式实战:工厂与策略模式落地
下一篇 WebAssembly 实战:把 Rust 编译进浏览器,给前端加上原生级性能