kube-proxy 的 NFTables 模式

在 Kubernetes 1.29 中,引入了新的 kube-proxy nftables 模式作为 Alpha 功能。目前处于 Beta 阶段,预计在 1.33 版本中达到 GA(正式发布)。新模式解决了 iptables 模式中长期存在的性能问题,我们鼓励所有在拥有较新内核的系统上运行的用户尝试一下。(出于兼容性原因,即使 nftables 成为 GA,iptables 仍将是**默认**模式。)

为什么选择 nftables?第一部分:数据平面延迟

iptables API 是为实现简单防火墙而设计的,在扩展以支持拥有数万个 Service 的大型 Kubernetes 集群中的 Service 代理时存在问题。

通常,在 iptables 模式下,kube-proxy 生成的规则集中的 iptables 规则数量与 Service 数量和端点总数的总和成正比。特别地,在规则集的顶层,对于数据包可能的目标地址,每个可能的 Service IP(和端口)都有一条规则来测试。

# If the packet is addressed to 172.30.0.41:80, then jump to the chain
# KUBE-SVC-XPGD46QRK7WJZT7O for further processing
-A KUBE-SERVICES -m comment --comment "namespace1/service1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O

# If the packet is addressed to 172.30.0.42:443, then...
-A KUBE-SERVICES -m comment --comment "namespace2/service2:p443 cluster IP" -m tcp -p tcp -d 172.30.0.42 --dport 443 -j KUBE-SVC-GNZBNJ2PO5MGZ6GT

# etc...
-A KUBE-SERVICES -m comment --comment "namespace3/service3:p80 cluster IP" -m tcp -p tcp -d 172.30.0.43 --dport 80 -j KUBE-SVC-X27LE4BHSL4DOUIK

这意味着当一个数据包进入时,内核检查它与所有 Service 规则的时间复杂度是**O(n)**,其中 n 是 Service 的数量。随着 Service 数量的增加,新连接的第一个数据包的平均和最坏情况延迟都会增加(最好、平均和最坏情况之间的差异主要取决于给定的 Service IP 地址在 `KUBE-SERVICES` 链中出现的位置是靠前还是靠后)。

kube-proxy iptables first packet latency, at various percentiles, in clusters of various sizes

相比之下,使用 nftables,编写此类规则集的常规方法是使用**单一**规则,通过“verdict map”(判决映射)来进行调度。

table ip kube-proxy {

        # The service-ips verdict map indicates the action to take for each matching packet.
	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		comment "ClusterIP, ExternalIP and LoadBalancer IP traffic"
		elements = { 172.30.0.41 . tcp . 80 : goto service-ULMVA6XW-namespace1/service1/tcp/p80,
                             172.30.0.42 . tcp . 443 : goto service-42NFTM6N-namespace2/service2/tcp/p443,
                             172.30.0.43 . tcp . 80 : goto service-4AT6LBPK-namespace3/service3/tcp/p80,
                             ... }
        }

        # Now we just need a single rule to process all packets matching an
        # element in the map. (This rule says, "construct a tuple from the
        # destination IP address, layer 4 protocol, and destination port; look
        # that tuple up in "service-ips"; and if there's a match, execute the
        # associated verdict.)
	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
	}

        ...
}

由于只有一条规则,并且映射查找的时间复杂度大致为 **O(1)**,所以无论集群大小如何,数据包处理时间都或多或少是恒定的,最好、平均和最坏情况都非常相似。

kube-proxy nftables first packet latency, at various percentiles, in clusters of various sizes

但请注意 iptables 和 nftables 图表在垂直尺度上的巨大差异!在拥有 5000 和 10000 个 Service 的集群中,nftables 的 p50(平均)延迟与 iptables 的 p01(大约是最佳情况)延迟大致相同。在拥有 30000 个 Service 的集群中,nftables 的 p99(大约是最坏情况)延迟甚至比 iptables 的 p01 延迟快了几个微秒!这里是两组数据放在一起的图表,但你可能需要眯着眼睛才能看到 nftables 的结果!

kube-proxy iptables-vs-nftables first packet latency, at various percentiles, in clusters of various sizes

为什么选择 nftables?第二部分:控制平面延迟

虽然在大型集群中数据平面延迟的改进非常棒,但 iptables kube-proxy 还存在另一个问题,这个问题常常导致用户甚至无法将集群扩展到那么大的规模:当 Service 及其端点发生变化时,kube-proxy 编写新 iptables 规则所需的时间。

对于 iptables 和 nftables,整个规则集的大小(实际规则加上相关数据)与 Service 及其端点的总数成 **O(n)** 关系。最初,iptables 后端会在每次更新时重写所有规则,当有数万个 Service 时,这可能会变成数十万条 iptables 规则。从 Kubernetes 1.26 开始,我们开始改进 kube-proxy,使其在每次更新中可以跳过更新**大部分**未更改的规则,但 `iptables-restore` 作为 API 的局限性意味着仍然需要发送一个与 Service 数量成 **O(n)** 关系的更新(尽管常数比以前小得多)。即使有了这些优化,仍然可能需要使用 kube-proxy 的 `minSyncPeriod` 配置选项,以确保它不会把所有时间都花在推送 iptables 更新上。

nftables API 允许进行更增量的更新,当 kube-proxy 在 nftables 模式下进行更新时,更新的大小仅与自上次同步以来已更改的 Service 和端点数量成 **O(n)** 关系,而与 Service 和端点的总数无关。nftables API 允许每个使用 nftables 的组件拥有自己的私有表,这也意味着组件之间不会像 iptables 那样存在全局锁争用。因此,kube-proxy 的 nftables 更新可以比 iptables 更高效地完成。

(很遗憾,这部分我没有酷炫的图表。)

为什么**不**用 nftables?

尽管如此,目前有几个原因可能让你不想立即切换到使用 nftables 后端。

首先,代码还很新。虽然它有大量的单元测试,在我们的 CI 系统中表现正常,并且已经被多个用户在实际环境中使用,但它的实际使用量远不及 iptables 后端,所以我们不能保证它同样稳定和无 bug。

其次,nftables 模式在较旧的 Linux 发行版上无法工作;目前它需要 5.13 或更新的内核。此外,由于早期版本 `nft` 命令行工具中的 bug,你不应该在主机文件系统中有旧版本(早于 1.0.0)`nft` 的节点上以 nftables 模式运行 kube-proxy(否则 kube-proxy 对 nftables 的使用可能会干扰系统上其他对 nftables 的使用)。

第三,你的集群中可能还有其他网络组件,例如 Pod 网络或 NetworkPolicy 实现,它们可能尚不支持 nftables 模式下的 kube-proxy。你应该查阅这些组件的文档(或论坛、bug 跟踪器等),看看它们是否与 nftables 模式存在问题。(在许多情况下它们不会有问题;只要它们不直接与 kube-proxy 的 iptables 规则交互或覆盖它们,它们就不应该关心 kube-proxy 是使用 iptables 还是 nftables。)此外,未更新的可观测性和监控工具在 nftables 模式下报告的数据可能比在 iptables 模式下要少。

最后,nftables 模式下的 kube-proxy 故意与 iptables 模式下的 kube-proxy 不 100% 兼容。有一些旧的 kube-proxy 功能,其默认行为不如我们期望的那样安全、高性能或直观,但我们认为更改默认值会破坏兼容性。由于 nftables 模式是可选的,这给了我们一个机会来修复那些不好的默认值,而不会影响那些不期望有变化的用户。(特别是,在 nftables 模式下,NodePort Service 现在只能在其节点的默认 IP 上访问,而在 iptables 模式下,它们可以在所有 IP 上访问,包括 `127.0.0.1`。)kube-proxy 文档提供了更多关于这方面的信息,包括有关可以查看哪些指标来确定你是否依赖于任何已更改的功能,以及有哪些配置选项可用于获得更向后兼容的行为。

试用 nftables 模式

准备好试用了吗?在 Kubernetes 1.31 及更高版本中,你只需向 kube-proxy 传递 `--proxy-mode nftables`(或在你的 kube-proxy 配置文件中设置 `mode: nftables`)。

如果你使用 kubeadm 来设置集群,kubeadm 文档解释了如何向 `kubeadm init` 传递 `KubeProxyConfiguration`。你也可以使用 `kind` 部署基于 nftables 的集群

你还可以通过更新 kube-proxy 配置并重新启动 kube-proxy Pod,将现有集群从 iptables(或 ipvs)模式转换为 nftables 模式。(你不需要重新启动节点:当以 nftables 模式重新启动时,kube-proxy 将删除任何现有的 iptables 或 ipvs 规则,同样,如果你稍后恢复到 iptables 或 ipvs 模式,它将删除任何现有的 nftables 规则。)

未来计划

如上所述,虽然 nftables 现在是**最佳**的 kube-proxy 模式,但它并不是**默认**模式,我们目前还没有改变这一点的计划。我们将继续长期支持 iptables 模式。

kube-proxy 的 IPVS 模式的未来则不那么确定:它相对于 iptables 的主要优势是速度更快,但 IPVS 架构和 API 的某些方面对于 kube-proxy 的目的来说很笨拙(例如,`kube-ipvs0` 设备需要分配**每个** Service IP 地址),并且 Kubernetes Service 代理的某些语义很难用 IPVS 实现(特别是某些 Service 根据你是从本地还是远程客户端连接而需要有不同的端点)。而现在,nftables 模式具有与 IPVS 模式相同的性能(实际上略好),而且没有任何缺点。

kube-proxy ipvs-vs-nftables first packet latency, at various percentiles, in clusters of various sizes

(理论上,IPVS 模式还有一个优势,即能够使用各种其他 IPVS 功能,例如用于平衡端点的替代“调度器”。实际上,这最终并不十分有用,因为 kube-proxy 在每个节点上独立运行,并且每个节点上的 IPVS 调度器无法与其他节点上的代理共享其状态,从而阻碍了更智能地平衡流量的努力。)

虽然 Kubernetes 项目没有立即放弃 IPVS 后端的计划,但从长远来看,它很可能被淘汰,目前使用 IPVS 模式的人应该尝试 nftables 模式(如果你认为 nftables 模式中缺少你无法绕过的功能,请提交 bug)。

了解更多