压平规则
这里的核心原理是:kube-proxy 的 iptables 模式把 Service 转发规则编译成一串 netfilter 规则链,而 Traffic Manager 把 Service 查找变成一次 eBPF Map 查询。当 Service 数量变大时,这两种查找路径的增长方式不一样。
在 kube-proxy 的 iptables 模式下,每个 Service、每个端口、每个 Endpoint 都会生成对应的 iptables 规则。客户端访问一个 ClusterIP 时,数据包会进入内核网络协议栈,然后经过 netfilter 的 PREROUTING/OUTPUT 等链,沿着一组规则逐条匹配:这个目标 IP 是不是某个 Service?端口是不是匹配?协议是不是匹配?命中了以后再跳到对应的 Service chain,然后根据概率规则选择某个 Endpoint,最后做 DNAT。
问题在于,iptables 的匹配本质上更接近“规则链顺序扫描”。集群里的 Service 越多,规则越多,包在找到目标 Service 前可能要经过更多匹配判断。即使每条规则判断很快,数量上来以后,累计成本会变明显。尤其是短连接场景,每个新连接都要重新走这段路径,所以开销会被放大。
Traffic Manager 的做法不一样。它在 socket 层的 connect4 hook 拦截应用的 connect() 调用,拿到目标地址,也就是 Service 的 ClusterIP 和端口,然后直接用这个 (VIP, Port, Proto) 作为 key 去 eBPF Hash Map 里查 Service 元信息。查到之后,再根据后端数量和调度策略选择一个 backend,把 socket 目标地址直接改成 Pod IP 和 Pod Port。
所以它绕过了两部分成本:
- 绕过 iptables/netfilter 规则链匹配
- 不再让连接请求在大量 Service 规则里逐条判断,而是直接用 key 查 Map。
- 更早地改写目标地址
- 它在应用发起
connect()的时候就把目标 VIP 改成真实 Pod IP,后续包天然就是发往后端 Pod,不需要每个包都依赖复杂的 DNAT 路径。
- 它在应用发起
简单说,iptables 像是在一本很长的规则表里从头往后找“这个 Service 是谁”;eBPF Map 像是拿着 Service IP 和端口直接查哈希表。Service 少的时候,两者差距可能不大;Service 多的时候,规则链扫描成本增长,而 Hash Map 查找成本基本稳定,所以 eBPF 的相对优势就会变大。
这也是为什么脚本里有 --rule-bloat 这个测试:它会创建很多 dummy Service,人为把 kube-proxy 的规则数量撑大。这样做不是为了“作弊”,而是在模拟真实大集群里 Service 数量很多时 kube-proxy iptables 规则膨胀的情况。这个场景越明显,Traffic Manager 的设计优势越容易体现出来。
还有一个关键点是这个项目测的是 短连接压测。短连接里大量请求都会反复触发连接建立路径,connect() 和首次路由/NAT 的成本占比更高。Traffic Manager 正好把 Service 选择前移到 connect() 阶段,并且用 eBPF Map 直接完成后端选择,所以在 Siege 这种大量短连接的 benchmark 里提升会比较明显。
当然,这里也要诚实地说,性能更高不是无条件成立。它取决于场景:
- 如果 Service 数量很少,iptables 规则不多,收益可能有限。
- 如果业务是长连接,连接建立成本被摊薄,QPS 提升可能没短连接明显。
- 如果 eBPF 里加了很复杂的权重计算、状态维护或 Map 查询过多,也会吃掉一部分收益。
- 如果 kube-proxy 使用 IPVS 或 nftables,和 iptables 模式相比差距也会变化。
- 如果瓶颈在应用本身、Pod 网络、CPU 或后端服务,而不是 Service 转发路径,那么绕过 iptables 也不会带来很大提升。
所以我会把这个结论表述得更准确一点:
在 kube-proxy iptables 模式、Service 数量较多、短连接较多的场景下,把 Service 规则链匹配替换成 eBPF Hash Map 查找,可以减少连接建立路径上的匹配和 NAT 开销,因此性能更高,而且 Service 规模越大,相对收益越明显。
为什么能跳过 kube-proxy 的 Service DNAT 规则
这个项目把 eBPF 程序挂到了 socket connect4 钩子上,在应用真正发包、进入 kube-proxy/iptables 转发路径之前,就把目标地址从 Service VIP 改成了 Pod IP,所以后续流量看起来根本不是访问 Service,自然也就不会命中 kube-proxy 的 Service DNAT 规则。
正常 kube-proxy iptables 模式大概是这样:
| |
也就是说,kube-proxy 本身不直接转发每个包,它主要负责把 Kubernetes Service/Endpoint 信息同步成 iptables/IPVS/nftables 规则。真正转发时,包经过内核网络栈,由这些规则做 DNAT。
而这个项目的路径是:
| |
关键点在于 hook 的位置更早。
cgroup/connect4 这个 eBPF hook 发生在进程调用 connect() 的时候。你的 eBPF 程序能通过 bpf_sock_addr 看到原始目标地址,也就是应用想连的 ClusterIP:Port。如果它发现这个地址是一个 Kubernetes Service,就直接调用类似逻辑把 ctx->user_ip4、ctx->user_port 改成后端 Pod 的 IP 和端口。
项目里的 eBPF 程序就是在这里做的:
| |
所以原本应用以为自己连的是:
| |
但经过 eBPF hook 后,内核后续处理的目标已经变成:
| |
这时候 kube-proxy 的规则通常是匹配 Service ClusterIP 的,比如:
| |
但目标地址已经不是 10.96.x.x:80 了,所以这些 Service 规则就不会命中。换句话说,不是 eBPF 把 kube-proxy 禁用了,而是它把流量提前改道了,让流量不再走 kube-proxy 管的那条 Service VIP 路径。
这也是为什么这个项目需要自己维护 eBPF Map。因为跳过 kube-proxy 后,谁来知道 Service 后面有哪些 Pod?答案就是 Traffic Manager 的 Go 控制器:
| |
所以它本质上是在做一个 kube-proxy 的替代数据面:
- kube-proxy:监听 Service/Endpoint,然后写 iptables/IPVS 规则。
- Traffic Manager:监听 Service/EndpointSlice,然后写 eBPF Map。
- eBPF 程序:在 socket 层查 Map,直接改写目标地址。
但这里也要注意几个边界:
只能跳过被这个 eBPF 程序处理到的流量 你的项目挂的是
connect4,主要处理本机进程发起的 IPv4 TCP connect 路径。没有经过这个 hook 的流量,不会被它改写。kube-proxy 仍然可以存在 它没有被卸载,也没有失效。只是这部分流量提前被改成 PodIP,所以不再命中 Service VIP 规则。
如果 eBPF Map 里没有这个 Service,还是会走原路径 你的代码里如果查不到 Service,就返回 miss,不做改写。那目标仍然是 ClusterIP,后面还是可能由 kube-proxy 处理。
这不是通用意义上跳过所有 Kubernetes 网络路径 它跳过的是 Service VIP 到后端 Pod 的 kube-proxy 负载均衡/DNAT 逻辑。后续 Pod 网络、CNI 路由、veth、bridge/overlay 等路径还是要走。
一句话总结就是:
因为 eBPF 挂在 connect4 这个比 kube-proxy Service DNAT 更早的位置,提前把 Service ClusterIP 改成了真实 Pod IP。后续内核看到的目的地址已经不是 Service VIP,所以 kube-proxy 的 Service 规则没有机会命中,于是这条流量就绕过了 kube-proxy。