土法炼钢 · 系统与基础设施

【Cilium / eBPF】kube-proxy 替代:ClusterIP/NodePort 的 BPF 路径与失败模式

文章导航

分类入口
kubernetesnetwork
标签入口
#cilium#ebpf#kube-proxy#clusterip#nodeport#maglev#dsr#socket-lb#v1.20.0

目录

东西向包路径讲清 veth/TC 与 identity 之后,值班下一问往往是:「Service 的 VIP 到底在哪被改成后端?」在 kube-proxy 的 iptables/IPVS 世界里,答案是 netfilter 链与 conntrack;在 Cilium 的 kube-proxy replacement(KPR) 里,答案是 socket 级 LB + 包路径 LB + 若干 BPF service/backend map。本篇只钉这条替代路径的机制差与失败模式,不重写 k8s-network/13 的产品调参清单。

本文是「Cilium / eBPF 数据面内核」系列第 6 篇(共 16 篇)。→ 系列目录

篇目 核心内容
第 5 篇 · 东西向包路径 Pod→Pod 停顿点
第 6 篇 · kube-proxy 替代 ClusterIP/NodePort/外部流量 BPF 路径
第 7 篇 · 策略编译 NetworkPolicy → BPF

版本锚定:Cilium v1.20.0docs.cilium.io/en/v1.20/;源码 tag v1.20.0)。无真实集群时不粘贴伪造 cilium status / cilium bpf lb list 输出;命令只写语义与前置条件。


一、KPR 要解决的不变量

kube-proxy 的三种实现(userspace、iptables、IPVS)都遵守同一语义契约:

  1. ClusterIP:集群内把 ServiceIP:Port 映射到 ready 后端。
  2. NodePort / LoadBalancer / externalIPs:节点外可达前端,再落到后端。
  3. 会话亲和、优雅终止、externalTrafficPolicy:在 DNAT/SNAT 与本地优选之间折中。

iptables 模式的代价是熟悉的:规则数随 Service×后端增长,匹配近似线性;全量 iptables-restore 与全局锁在大规模下变成控制面税。k8s-network/09eBPF vs netfilter 已写过这条争论。Cilium KPR 的工程命题不是「再优化一条 iptables」,而是:用 BPF map 的常数时间查找与原子更新,替代链遍历与全量刷新,同时尽量保持 K8s Service 语义。

v1.20 文档(Kube-Proxy Free / kubeproxy-free)把完整替换钉成:kubeProxyReplacement=true 时,ClusterIP、NodePort、LoadBalancer、externalIPs 与 hostPort 由 eBPF 路径承担;Helm 默认 kubeProxyReplacement=false 时,通常只保留集群内 ClusterIP 的包路径 LB,其余仍可依赖 kube-proxy。混部不是「半开半关随便试」,而是有明确的冲突面(见第七节)。


二、两条 LB 平面:socket-LB 与 per-packet

KPR 不是单一 hook。文档与数据面分工可收成两层:

平面 典型挂载 服务的流量 失败时表象
Socket-LB cgroup connect / sendmsg / recvmsg / getpeername 本机进程(含 Pod)连 ClusterIP/NodePort 应用看到偶发连错后端、getpeername 暴露真实 Pod IP
Per-packet LB TC /(可选)XDP 跨节点、外部入站、未走 socket 改写的包 VIP 黑洞、NodePort 不通、DSR 回包被云厂商反欺骗丢掉
flowchart TD
  app["App connect ClusterIP"] --> sock["cgroup socket-LB\nrewrite destination"]
  sock --> backend["Local or remote backend"]
  ext["External to NodePort"] --> xdp["Optional XDP"]
  xdp --> tc["TC ingress on device"]
  tc --> svcmap["LB service/backend maps"]
  svcmap --> snat["SNAT mode: extra hop"]
  svcmap --> dsr["DSR mode: reply direct"]

Socket-LB 的意义:在 connect() 时刻就把目标改成后端,避免「每个包都做 DNAT」;同节点东西向甚至可以与 sockmap 短路路径协作(挂载细节见 第 3 篇)。内核下限在官方 kubeproxy-free 章有明确要求(历史门槛约 4.19.57 / 5.1.16 / 5.2.0+;更高内核有额外优化)。版本边界写进运维基线,比「装了 Cilium 就算 KPR」更准确。

Per-packet 路径负责:外部 NodePort、从网卡进来尚未进入本机 socket 层的流量,以及 socket-LB 覆盖不到的协议边角。Service 查找键语义对齐 LB4_SERVICES / LB6_SERVICES 一类 map(路径在 bpf/pkg/maps/lbmap/ @ v1.20.0):前端地址 + 端口 + 协议 → service 元数据 → backend 选择。map 容量默认量级可调(如 --bpf-lb-map-max);打满时新条目无法写入,表象是「新建 Service 不生效」或间歇黑洞——排障应先看 map 压力,再怀疑 DNS。


三、ClusterIP:集群内如何落到后端

3.1 东西向 ClusterIP

典型路径(与第 5 篇衔接):

  1. Pod 内进程 connect(ClusterIP, port)
  2. Socket-LB 查 service map,选出 backend(默认随机;可配置)。
  3. 若 backend 在本节点:本地投递 / redirect,不必出网卡。
  4. 若 backend 在远端:包按集群路由模式(native / tunnel)发往远端节点;对端再做策略与投递。

与 iptables 的差:没有 KUBE-SERVICES 链上的逐条规则匹配;后端集合变更是 map 增量更新,而不是整张表 rewrite。与 IPVS 的差:IPVS 仍坐在 netfilter 附近,依赖内核 IPVS 调度器与 conntrack 协作;Cilium 把「调度结果」放进 BPF map,并可用 Maglev 表做南北向一致性哈希(见第五节)。

3.2 会话亲和与优雅终止

v1.20 KPR 状态面可报告 Session Affinity、Graceful Termination 等开关。优雅终止依赖 EndpointSlice 的 terminating 条件(K8s 1.20+ 与对应 feature gate 叙事)。工程含义:后端进入 terminating 时,新连接应停止打到该后端,已有连接可按宽限期排空。失败模式是「滚动更新时偶发 502」:若 map 未及时摘除 terminating 后端,或客户端连接池复用了旧五元组,表现会像应用故障。归因顺序应是:EndpointSlice 条件 → agent 是否编程 backend → CT/socket 是否仍钉死旧后端。


四、NodePort 与外部流量:SNAT、DSR、Hybrid

4.1 默认 SNAT

默认 loadBalancer.mode=snat:外部请求打到节点 A 的 NodePort,若选中的后端在节点 B,则 A 代发并做 SNAT;回包必须回到 A 做反向转换再回客户端。代价清晰:

优点是不强制改 MTU、不依赖云厂商放行「源 IP = VIP」的回包。

4.2 DSR

loadBalancer.mode=dsr:回包由后端节点直接回客户端,源地址用 Service IP/port。客户端源 IP 可保留到后端。代价转移到:

Hybrid 模式对 TCP 走 DSR、对 UDP 走 SNAT,是「要源 IP 又不想全面砍 MTU」的折中。Annotation-based 模式允许按 Service 注解覆盖全局默认(需 bpf.lbModeAnnotation 与显式 loadBalancer.dsrDispatch)。

4.3 externalTrafficPolicy

策略 KPR 语义要点 常见踩坑
Cluster(默认) 可跨节点转发;源 IP 是否保留取决于 SNAT/DSR 以为永远能看到客户端 IP
Local 优先/仅本地后端;无本地后端时外部流量行为与健康检查联动 节点被 LB 打满但本地无 Pod → 黑洞;集群内访问语义与外部不同

internalTrafficPolicy 把类似语义映射到集群内流量。排障时必须先问:失败的是集群内 curl ClusterIP,还是云 LB → NodePort? 两条路径的 map 键与 NAT 模式可以不同。


五、Maglev:南北向一致性哈希,不是东西向银弹

Cilium 实现 Maglev 变体作为后端选择算法(loadBalancer.algorithm=maglev)。奠基叙述来自 Eisenbud 等,NSDI 2016 Maglev: A Fast and Reliable Software Network Load Balancer:用大查找表 \(M\)(素数)近似一致性哈希,后端变更时尽量限制无关流的重映射比例(论文与 Cilium 文档都强调「约 1%」量级的工程目标;表大小应显著大于最大后端数 \(N\),实践上常取 \(M \gg 100N\))。

v1.20 文档钉死两条常被误读的边界:

  1. Maglev 主要用于外部(N-S)流量。集群内(E-W)socket 级分配在 connect 时完成,不走 Maglev 表
  2. 全集群 agent 必须共享同一 maglev.hashSeed,否则「同一五元组在不同入口节点选到不同后端」,会话粘滞与排障都会崩。

内存税:每个 Service 一份查找表;相对 random 更吃节点内存。开放配置项包括 maglev.tableSize(默认文档叙述为 16381 等素数档)。把 Maglev 当成「所有 Service 延迟银弹」是错的;它优化的是入口一致性与故障转移时的扰动上界,不是东向 socket-LB 的路径长度。


六、相对 iptables / IPVS 的机制对照

维度 iptables kube-proxy IPVS kube-proxy Cilium KPR (v1.20)
查找 链上近似 O(规则) 哈希到虚拟服务 BPF map O(1)
更新 常整表 restore 增量更好但仍经 netfilter 原子 map 更新
Conntrack 内核 nf_conntrack 同左 + IPVS BPF CT map(per-CPU 叙事)
一致性哈希 弱(statistic 随机) 有调度算法,非 Maglev 表 Maglev(N-S)
与策略耦合 另一套 iptables 另一套 同数据面 identity/policy map

争论(有文献与工程两侧):一边是「可编程数据面用常数查找压过 netfilter 线性税」(Cilium 设计叙述;XDP/eBPF 路径见 Høiland-Jørgensen 等 CoNEXT 2018 The eXpress Data Path);另一边是「完全旁路 netfilter 后,运维失去统一的 iptables 观测面,故障语义更陡」。站内 eBPF vs netfilter 已展开;本篇只强调:KPR 赢在更新路径与查找复杂度,不自动消除错误配置与 map 容量事故。


七、失败模式清单(按层归因)

观察 更可能落点 不要先怀疑
仅 ClusterIP 不通,NodePort 正常 socket-LB / cgroup 挂载、hostNetwork 例外 应用代码
外部 LB 健康检查失败、本地无 endpoints externalTrafficPolicy=Local DNS
跨节点 NodePort 单向通 SNAT 回程、DSR 被丢、MTU 证书
滚动更新偶发连旧 Pod Graceful / EndpointSlice terminating 未编程 内核 bug
新建 Service 全员不通 lb map 满、agent 未 sync API 单 Pod 日志
与 Istio sidecar「有时绕过」 socket-LB 与 sidecar 抢改写;文档建议 socketLB.hostNamespaceOnly 一类约束 随机丢包
迁移中双 NAT 同节点 kube-proxy 与 KPR 同时做 Service 「Cilium 坏了」

共存与迁移:官方 Per-node configuration 给出用 CiliumNodeConfig + 节点标签逐步迁移、并把 kube-proxy DaemonSet 亲和到未迁移节点的流程。硬条件:必须配置 k8sServiceHost / k8sServicePort,否则去掉 kube-proxy 后 agent 自己都连不上 apiserver。禁止在未 drain/未分节点的情况下「原地同时跑两套完整 Service 实现」——NAT 与 conntrack 双写是经典事故源。

无集群时可用的自检命令语义(需可调度集群与 Cilium agent):

# 语义:确认 KPR 是否真正 True,以及 SNAT/DSR、算法、各 Service 类型开关
cilium status
cilium status --verbose   # 关注 KubeProxyReplacement Details
# 语义:查看 service/backend 是否已编程(输出勿伪造)
# cilium bpf lb list

八、谱系、争论与开放问题

谱系:K8s Service 语义 → kube-proxy iptables/IPVS 实现 → eBPF/XDP 可编程数据面(CoNEXT 2018 XDP)→ Cilium 把 LB 状态搬进 BPF map,并用 Maglev(NSDI 2016)填南北向一致性哈希空缺。Cilium 不是「发明了 Service」,而是改了执行位置与更新代价模型

争论

  1. 完全 KPR vs 混部:运维安全感 vs 双路径复杂度;混部只适合受控迁移窗口。
  2. DSR vs SNAT:源 IP 与回程跳数 vs 云网络兼容性;没有无条件最优解。
  3. Maglev 表内存 vs random:一致性与故障扰动 vs 节点内存;大规模 Service 数时要显式容量规划。

开放问题

  1. map 压力与节点规模的可观测闭环:何时把 lb map 占用提升为与「API Server 延迟」同级的 SLO 信号?(系列第 12–13 篇方向)
  2. 与用户态网格抢 socket 改写:sidecar / Ambient 并存时,socket-LB 的默认开启面如何做成可证明的「不会双 DNAT」?(边界见 第 10 篇

九、hostPort、healthz 与「半开 KPR」

完整 KPR 还接管 hostPort:容器声明的主机端口映射不必再依赖 portmap CNI 插件。失败表象常是「Pod Running 但主机端口不通」——此时应查 agent 是否把 hostPort 编进 LB 相关 map,而不是先怪应用 listen 地址。与 NodePort 共用端口空间时,冲突诊断要同时看 Service 与 Pod hostPort,避免两边各查各的。

健康检查面:迁移文档要求配置 kube-proxy-replacement-healthz-bind-address 一类地址,让负载均衡器或节点探针仍能打到「原 kube-proxy healthz 语义」的替代端点。漏配时,云 LB 可能把节点标 unhealthy,而集群内 ClusterIP 一切正常——典型的「内外分裂」。

半开配置kubeProxyReplacement=false 但单独打开 socketLB / nodePort / externalIPs / hostPort)只适合受控混部:每一项开启都要回答「kube-proxy 是否仍在写同一前端」。两边都写同一 NodePort 时,数据包走哪条路径取决于 hook 顺序与 conntrack 状态,排障几乎不可复现。迁移窗口之外,目标态应是 单一权威:要么完整 KPR,要么完整 kube-proxy。

XDP 加速(文档中的 NodePort XDP)把部分南北向决策前移到驱动更早的点;它不改变 Service 语义,但改变「丢包发生在 XDP 还是 TC」的观测位置。未开 XDP 时用 XDP 术语解释故障,会找错层。


十、本篇边界

不写:每个云厂商 LB 注解百科、hostPort 全参数表、伪造压测 pps。
下一篇把 NetworkPolicy / CiliumNetworkPolicy 如何编译进 policy map、以及默认拒绝窗口与 identity 漂移的交互钉死。

一句话收束:KPR 把 Service 从 netfilter 链变成 BPF map 上的状态机;ClusterIP 多走 socket-LB,NodePort/外部流量在 SNAT/DSR/Hybrid 之间买源 IP 与兼容性,失败时先分清平面再查 map。

参考资料

规范 / 官方文档(A)

源码(A)

论文(A)

站内

上一篇:东西向包路径 · 系列目录 · 下一篇:策略编译

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .