Kubernetes的Service抽象是整个体系里最基础的能力之一,而支撑Service转发的事实标准长期都是kube-proxy加iptables。随着集群规模扩大,iptables方案的短板越来越明显:规则数量线性膨胀、更新时全量刷新、conntrack表容易打满。eBPF技术的成熟让这个问题有了新的解法,Cilium项目提供的eBPF数据路径可以在完全替换kube-proxy的情况下实现Service转发,本文结合实际部署经验,把原理、配置和踩坑点讲清楚。

iptables数据路径到底慢在哪里
iptables的工作方式是先把所有规则编译成线性链表,挂在netfilter的各个钩子点上。数据包到达时,内核逐条遍历规则直到匹配命中。这个模型在规则量小的时候完全没问题,但在一个几千个Pod、几百个Service的集群里,KUBE-SERVICES链下的规则可能轻松超过数万条。更麻烦的是,iptables没有增量更新的能力,Service或者Endpoint任何一点变化,kube-proxy都要把整张规则表锁住重新写入,规则越多,这个操作越慢,极端情况下会出现秒级的转发中断。
另一个瓶颈是conntrack表。iptables的NAT依赖连接跟踪,每一条经过转发的连接都要占用一个表项。在高并发短连接场景下,比如微服务间高频RPC调用,conntrack表项的创建和销毁本身就是不小的开销,一旦表被写满,新连接会被直接丢弃,表现为业务偶发性超时。此外,每个包都要在用户态规则和内核态匹配之间来回穿梭的路径本身也比纯内核转发更长,CPU占用在NUMA架构下还会被放大。
还有一点容易被忽视:iptables转发路径上,包要经过PREROUTING做DNAT,路由决策后再FORWARD,到对端再SNAT回写,同一个包经历了多次钩子遍历。eBPF的思路则是把负载均衡直接嵌进内核网络栈的最短路径上,减少不必要的重复处理。
eBPF替代方案的核心原理
Cilium的eBPR数据路径分为几种模式。最彻底的是direct模式,即彻底不部署kube-proxy,所有Service逻辑由eBPF程序在tc(traffic control)层或XDP层完成。从Pod发出的包到达宿主机veth设备时,挂载在tc ingress的BPF程序会查询eBPF map中维护的Service和后端信息,直接完成后端选择和DNAT,随后通过bpf_fib_lookup辅助函数在内核做一次路由查找,借助重定向把包直接送到目标接口,整个过程一次完成,不再遍历iptables链。
对于入向流量,Cilium使用sockmap和socket cgroup钩子做socket层面加速。如果客户端和服务端Pod在同一个节点上,通信可以在L4层直接完成重定向,包根本不需要走完整的协议栈,这是iptables完全做不到的优化。跨节点场景下,包在tc层被封装或直接路由,Headless Service和ExternalIP等特殊形态也都有对应的处理逻辑。
在替换过程中有几个容易出问题的细节。一是MTU处理,如果启用了VXLAN或Geneve封装,eBPF程序需要正确处理封装头部的开销,否则会出现大包分片或者应用层偶发卡顿;二是checksum offload,部分网卡在BPF重定向后offload行为不一致,必要时需要在程序中主动重算校验和;三是南北向流量兼容性,如果节点上还有其他组件依赖iptables(比如某些网络策略或HostPort实现),需要确认它们的共存方式,Cilium提供了专门的iptables reconciler来处理这类遗留依赖。
落地部署与验证
部署Cilium并替换kube-proxy的核心配置如下,前提是内核版本建议5.4以上,更好是5.10+,并且启用了BPF相关编译选项:
# 安装cilium CLI curl -L --remote-name https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin # 使用helm安装,关键参数是kubeProxyReplacement和k8sServiceHost helm install cilium cilium/cilium --namespace kube-system \ --set kubeProxyReplacement=true \ --set k8sServiceHost=192.168.0.1 \ --set k8sServicePort=6443 \ --set hostServices.enabled=true \ --set externalIPs.enabled=true \ --set nodePort.enabled=true \ --set hostPort.enabled=true
安装完成后,如果集群里已经存在kube-proxy,需要先确认Cilium状态健康再删除它。cilium CLI提供了状态检查命令,会输出每个节点的BPF程序加载情况和KubeProxyReplacement模式是否真正生效:
# 检查整体状态 cilium status --wait # 确认kube-proxy替换状态 cilium status --verbose | grep "KubeProxyReplacement" # 验证Service转发连通性 cilium connectivity test
验证通过后,删除kube-proxy的DaemonSet。注意DaemonSet删除后iptables规则不会立刻消失,建议在每个节点上执行一次规则清理,再观察cilium-agent是否按预期重建了必要的规则(比如用于节点防火墙的部分)。回退方案也要提前准备:保留kube-proxy的 manifests文件,出现问题时重新apply,Cilium检测到kube-proxy存在后会自动降级回合作模式,这个能力在生产环境切换时非常关键。
性能收益与适用场景评估
从实测数据看,替换后的收益主要体现在三方面。首先是转发延迟,同节点Pod间通信走socket重定向,p99延迟可以下降一个数量级;其次是CPU,大规模集群中kube-proxy的规则同步和conntrack开销消失后,网络转发的整体CPU占用明显下降;最后是稳定性,eBPF map支持原子更新,单个Service的Endpoint变化只影响对应的map项,不存在全量刷新导致的抖动。
当然这套方案也不是银弹。内核版本是硬性门槛,老旧的3.x内核集群需要先升级;运行低版本内核的边缘节点可能无法享受全部特性。团队还需要有人具备一定的eBPF排障能力,出现问题时传统的iptables日志和tcpdump不再完全适用,需要转向cilium monitor和bpftool等工具。另外,如果集群中有大量依赖iptables的第三方组件,比如老的Ingress Controller或安全Agent,迁移前的兼容性梳理必不可少。
总体建议是:新集群或者以东西向流量为主的大规模集群优先考虑eBPF数据路径;存量集群可以先在测试环境跑通连通性测试和压测,用灰度节点池的方式逐步切换,同时保留回退路径。把iptables替换成eBPF不只是换一个转发实现,更是一次网络可观测性和运维方式的升级,配合Hubble可以拿到L7层的流量可视化,这部分收益往往比性能提升更让运维团队惊喜。