摘要

Linux 上默认的 kube-proxy 实现目前基于 iptables。IPTables 是多年来 Linux 内核中首选的数据包过滤和处理系统(自 2001 年 2.4 内核开始)。然而,iptables 的问题导致了其继任者 nftables 的开发,nftables 首次在 2014 年 3.13 内核中可用,并且自那时以来作为 iptables 的替代方案,其功能和可用性不断增强。iptables 的开发基本上已经停止,新的功能和性能改进主要进入了 nftables。

此 KEP 提议创建一个新的官方/支持的 kube-proxy 的 nftables 后端。虽然希望这个后端最终会取代 iptablesipvs 后端并成为 Linux 上的默认 kube-proxy 模式,但这种替换/弃用将会在一个单独的未来 KEP 中处理。

动机

目前 Linux 上有两个官方支持的 kube-proxy 后端:iptablesipvs。(原始的 userspace 后端在几个版本前已被弃用,并在 1.26 版本中从代码库中移除。)

kube-proxy 的 iptables 模式目前是默认模式,并且通常被认为对大多数使用场景来说“足够好”。尽管如此,有充分的理由用新的 nftables 模式来替代它。

iptables 内核子系统存在无法解决的性能问题

尽管已经做了大量工作来提高 kube-proxy iptables 后端的性能,但内核中 iptables 的实现存在一些根本性的性能问题,这些问题既出现在控制平面方面,也出现在数据平面方面:

  • 控制平面存在问题,因为 iptables API 不支持对规则集进行增量更改。如果你想添加一条 iptables 规则,iptables 二进制文件必须获取一个锁,从内核下载整个规则集,找到添加新规则的适当位置,添加规则,重新上传整个规则集到内核,并释放锁。随着规则集的增大(即 Kubernetes 服务数量的增加),这一过程会变得越来越慢。如果你想替换大量规则(如 kube-proxy 经常需要的),那么仅仅是 /sbin/iptables-restore 解析所有规则所需的时间就会变得相当可观。

  • 数据平面存在问题,因为(大多数情况下)用于实现一组 Kubernetes 服务的 iptables 规则数量与服务数量直接成正比。系统中的每个数据包都需要通过所有这些规则,从而减慢流量速度。

IPTables 是 kube-proxy 性能的瓶颈,直到我们停止使用它,这个问题才会得到解决。

上游开发已经从 iptables 转移到 nftables

在很大程度上由于其无法解决的问题,内核中 iptables 的开发已经放缓并几乎停止。新的功能不再添加到 iptables 中,因为 nftables 应该能更好地完成 iptables 所做的一切。

虽然没有计划将 iptables 从上游内核中移除,但这并不保证 iptables 将永远受到发行版的支持。特别是,红帽公司已经宣布 [iptables 在 RHEL 9 中被弃用],并可能在几年后的 RHEL 10 中完全移除。其他发行版也在朝这个方向迈出小步伐;例如,[Debian 在 Debian 11 (Bullseye) 中将 iptables 从“必需”软件包集中移除]。

RHEL 的弃用特别对 Kubernetes 产生了两方面的影响:

  1. 许多 Kubernetes 用户运行 RHEL 或其下游版本,所以在几年后 RHEL 10 发布时,他们将无法在 iptables 模式下使用 kube-proxy(或者,更确切地说,也无法在 ipvsuserspace 模式下使用,因为这些模式也大量使用了 iptables API)。

  2. 过去几年中,红帽开发人员修复了几个影响 Kubernetes 的上游 iptables 错误和性能问题。随着红帽不再努力维护 iptables,未来影响 Kubernetes 的上游 iptables 错误是否能及时修复(如果能修复的话)变得不太可能。

kube-proxy 的 ipvs 模式不能拯救我们

由于 iptables 的问题,一些开发人员在 2017 年为 kube-proxy 添加了 ipvs 模式。人们普遍希望这能最终解决 iptables 模式的所有问题并成为其替代方案,但这从未真正实现。原因并不完全清楚……kubeadm #817,“跟踪何时可以默认启用 kube-proxy 的 ipvs 模式”大概是从最初的兴奋到对 ipvs 模式逐渐失望的一个好例子:

  • “一些问题……关于 kube-proxy 容器镜像中提供的 iptables/ipset 版本”
  • “显然还没有准备好成为默认模式”
  • “复杂性……用户节点上缺少或禁用了 IPVS 内核模块”
  • “我们仍然缺乏测试”
  • “仍然没有完全与我们在 iptables 模式下支持的内容对齐”
  • “iptables 工作正常,人们也熟悉它”
  • “[不确定是否曾经打算让 IPVS 成为默认模式]”

此外,内核 IPVS API 本身并未提供足够的功能来完全实现 Kubernetes 服务,因此 ipvs 后端也大量使用了 iptables API。因此,如果我们担心 iptables 被弃用,那么为了切换到使用 ipvs 作为默认模式,我们无论如何都必须将其中的 iptables 部分移植到使用 nftables。但到了那时,几乎没有理由使用 IPVS 进行核心负载均衡部分,特别是考虑到 IPVS 和 iptables 一样,也不再是一个积极开发的技术。

/sbin/iptablesnf_tables 模式不能拯救我们

在 2018 年,随着 iptables 客户端二进制文件 1.8.0 版本的发布,二进制文件中添加了一种新模式,允许它们使用内核中的 nftables API,而不是旧的 iptables API,同时仍保留原始 iptables 二进制文件的“API”。截至 2022 年,大多数 Linux 发行版现在都使用这种模式,因此旧的 iptables 内核 API 大多已经废弃。

然而,这种新模式并没有添加任何新的_语法_,因此无法使用任何 iptables 中不存在的新 nftables 功能(如映射)。

此外,由 iptables 二进制文件的用户界面 API 施加的兼容性限制,阻止它们利用许多与 nftables 相关的性能改进。

(另外,RHEL 对 iptables 的弃用也包括 iptables-nft。)

kube-proxy 的 iptables 模式已经变得陈旧

由于 iptables 是 kube-proxy 的默认模式,它受到强烈的向后兼容性约束,这意味着某些现在被认为是糟糕主意的“功能”无法移除,因为它们可能会破坏某些现有用户。几个例子:

  • 它允许在 localhost 上访问 NodePort 服务,这需要将 sysctl 设置为一个可能在系统上引入安全漏洞的值。更普遍地,它默认让 NodePort 服务在_所有_节点 IP 上都可访问,而大多数用户可能更希望它们受到更多限制。
  • 它为直接发送到 LoadBalancer IP 的流量实现了 LoadBalancerSourceRanges 功能,但不适用于由外部 LoadBalancer 重定向到 NodePort 的流量。
  • 某些新功能只有在管理员向 kube-proxy 传递某些命令行选项(例如 --cluster-cidr)时才能正确工作,但我们不能让这些选项成为强制性的,因为这会破坏那些没有传递这些选项的旧集群。

一个新的 kube-proxy 模式,现有用户必须显式选择加入,可以重新审视这些和其他决策。(尽管如果我们期望它最终成为默认模式,那么我们可能会决定避免这样的改变。)

希望我们能够用 1 个支持的后端取代 2 个

目前,SIG Network 正在支持 kube-proxy 的 iptablesipvs 后端,但由于 iptables 的性能问题,感觉不能放弃 ipvs。如果我们创建一个新后端,其功能和无故障程度与 iptables 相当,但性能与 ipvs 一样好,那么我们最终可能会弃用现有的两个后端,并在未来只需支持一个 Linux 后端。

编写新的 kube-proxy 模式将有助于集中我们的清理/重构工作

人们希望提供一个 kube-proxy 库,第三方可以将其用作外部服务代理实现的基础(KEP-3786)。现有的核心 kube-proxy 代码虽然能正常工作,但设计得并不好,我们不希望支持其他人使用其当前形式。

编写新的 proxy 后端将迫使我们重新审视所有这些共享代码,这或许会给我们带来如何清理、合理化和优化它的新思路。

目标

  • 设计并实现 kube-proxy 的 nftables 模式。
    • 考虑对旧 iptables 模式行为的各种修复。
      • 不启用 route_localnet sysctl。
      • 为 kube-proxy 添加更为严格的启动模式,当配置无效(例如 “--detect-local-mode ClusterCIDR” 没有指定 “--cluster-cidr")或不完整(例如,部分双栈但不是完全双栈)时,将出错。
      • (可能还有在此 KEP 中讨论的其他更改。)
      • 确保这些更改对用户有明确的文档说明。
      • 在可能的范围内,提供指标,以便 iptables 用户可以轻松确定他们是否在使用在 nftables 模式下行为不同的功能。
    • 记录我们希望视为 API 的 nftables 实现的具体细节。特别是,记录网络插件作者可以依赖的高层次行为。我们还可以记录第三方或管理员可以以较低级别与 kube-proxy 规则集成的方法。
  • 允许在 iptables(或 ipvs)模式和 nftables 模式之间切换,而无需手动清理中间的规则。
  • 记录新后端的最低内核/发行版要求。
  • 记录 iptables 模式和 nftables 模式之间的不兼容更改(例如 localhost NodePorts、防火墙处理等)。
  • 在小型、中型和大型集群中进行性能测试,比较 iptablesipvsnftables 后端,比较“控制平面”方面(重新编程规则所花费的时间/CPU 使用情况)和“数据平面”方面(到服务 IP 的数据包的延迟和吞吐量)。
  • 帮助清理和重构 kube-proxy 库代码。
  • 尽管此 KEP 不包括 GA 之后的内容(例如,使 nftables 成为默认后端,或更改 iptables 和/或 ipvs 后端的状态),但我们应该在此 KEP 达到 GA 时至少有一个未来计划的开始,以确保我们不会最终永久地维护 3 个后端而不是 2 个。

非目标

  • 避免陷入 ipvs 后端相同的陷阱,尽可能识别这些陷阱是什么。

  • 从 kubelet 移除 iptables 的 KUBE-IPTABLES-HINT 链;该链存在的目的是为了节点上任何想要使用 iptables 的组件的利益,即使 Kubernetes 核心的任何部分不再使用 iptables,该链也应该继续存在。(并且没有必要为 nftables 添加类似的东西,因为没有与 nftables 相关的主机文件系统配置需要容器化的 nftables 用户担心。) 相对于该 KEP 之前讨论的一些非目标:

  • 改变会话亲和性行为;nftables 后端将实现与 iptables 相同的行为(这与 ipvs 和一些第三方代理实现不同)。如果我们决定在未来重新审视会话亲和性,可以很容易地添加或更改 nftables 后端的行为,因为它是“手动”实现的。

  • 实现对 NodePort(或 ExternalIPs)流量的 LoadBalancerSourceRanges 过滤。kube-proxy 对该功能的实现主要是为了 pod 到负载均衡器的短路情况。希望获得更一致过滤行为的用户可以使用 Gateway API。

  • 支持运行代理的多个实例(以及 service.kubernetes.io/service-proxy-name 标签)。现在已经有该想法的概念验证(kubernetes #122814),因此我们知道设计支持它,并且可以在未来实现。

  • 显式支持“调试”/“管理员覆盖”规则。nftables 后端将保留 iptables 后端的行为,即“你可以更改我们的规则,但你的更改最终会被覆盖”。我们可能仍会在某天添加对显式覆盖的支持,如下文所讨论,但这不会是初始版本的一部分。

提案

注意事项/限制/警告

至少已经有三个基于 nftables 的 kube-proxy 实现,但它们都不适合直接采用或用作起点:

  • kube-nftlb:这是基于一个名为 nftlb 的独立 nftables 负载均衡项目构建的,这意味着它不是直接将 Kubernetes 服务转换为 nftables 规则,而是将其转换为 nftlb 负载均衡对象,然后这些对象再被转换为 nftables 规则。这不仅使得代码对于那些不熟悉 nftlb 的用户来说更加混乱,还意味着在许多情况下,新的服务功能需要先添加到 nftlb 核心中,然后 kube-nftld 才能使用它们。(此外,自 2020 年 11 月以来它未曾更新。)

  • nfproxy:其 README 中提到「nfproxy 在功能方面不是 kube-proxy(iptables)的 1:1 复制。nfproxy 不会涵盖 kube-proxy 处理的所有极端情况和特殊功能。」(此外,自 2021 年 1 月以来它未曾更新。)

  • kpng’s nft backend:这被写成一个概念验证,主要是将 iptables 规则直接翻译为 nftables,并没有很好地利用 nftables 的功能来减少规则的总数。它还大量使用了 kpng 的 API,如 DiffStore,而关于是否在上游采用这些 API 尚无共识。

风险和缓解措施

功能性

该提案的主要风险是功能或稳定性回退,这将通过测试以及新代理模式的逐步、可选的推出来解决。

缓解此风险的最重要措施是确保从 nftables 模式回滚到 iptables/ipvs 模式能够可靠地工作。

兼容性

许多 Kubernetes 网络实现使用 kube-proxy 作为其服务代理实现。鉴于 kube-proxy 行为的低级细节很少被明确指定,在更大的网络实现中使用它(特别是在编写与其正确互操作的网络策略实现时)必然需要对其行为的(当前)未记录方面(例如数据包何时以及如何被重写)做出假设。

虽然从外部看,nftables 模式可能与 iptables 模式非常相似,但某些 CNI 插件、网络策略实现等可能需要更新才能与其协同工作。(如果在 Alpha 阶段时它尚未与流行的网络插件兼容,这可能进一步限制新模式的测试量。)在这里我们能做的不多,除了避免 无谓的 行为差异。

安全性

iptables 模式相比,nftables 模式不应引发任何新的安全问题。

设计细节

高层设计

从高层来看,新模式应该与现有模式具有相同的架构;它将使用 k8s.io/kubernetes/pkg/proxy 中的服务/端点跟踪代码来监控变化,并相应地更新内核中的规则。

底层设计

一些细节将在实现过程中确定。我们可能会先从一个在架构上更接近 iptables 模式的实现开始,然后随着时间的推移重写它,以利用 nftables 的附加功能。

Tables

与 iptables 不同,nftables 没有任何默认 table 或 chain(例如,natPREROUTING)。相反,每个 nftables 用户都需要创建并使用自己的 table,并忽略其他组件创建的 table(例如,当 firewalld 在 nftables 模式下运行时,重新启动它只会刷新 firewalld 表中的规则,而不是像在 iptables 模式下运行时那样,重新启动它会刷新所有规则)。

在每个 table 中,基础 chain 可以连接到 hooks,赋予它们类似于内置 iptables chain 的行为。(例如,具有 type nathook prerouting 属性的链将像 iptables nat table 中的 PREROUTING chain 一样工作。)基础 chain 的优先级控制它相对于连接到相同或其他表中相同钩子的其他链的运行时间。

一个 nftables 表只能包含单个 family(ip(v4),ip6inet(IPv4 和 IPv6),arpbridgenetdev)的规则。我们将在 ip 族中创建一个 kube-proxy 表,在 ip6 family 中创建另一个表。我们所有的链、集合、映射等都会放入这些表中。

(理论上,我们可以在 inet 族中创建一个表,并将 IPv4 和 IPv6 的链/规则都放在那里,而不是在 ipip6 族中各创建一个表。然而,这并不会真正简化多少,因为我们仍然需要分别匹配 IPv4 地址和 IPv6 地址的集合/映射。(没有可以存储/匹配 IPv4 地址或 IPv6 地址的数据类型。)此外,由于 Kubernetes 服务与现有的 kube-proxy 实现并行发展,我们最终得到的双栈服务语义最容易通过完全分别处理 IPv4 和 IPv6 来实现。)

与内核 nftables 子系统通信

我们将使用 nft 命令行工具来读写规则,就像我们在 iptablesipvs 后端中使用命令行工具一样。

然而,nft 工具大多只是 libnftables 的一个薄包装,所以任何包装 nft 命令行的 golang API 都可以在将来直接重写为使用 libnftables(通过 cgo 包装器),如果这看起来是一个更好的主意。(理论上,我们也可以直接使用 netlink,而不需要 cgo 或外部库,但这可能是一个坏主意;libnftables 在原始 netlink API 之上实现了相当多的功能。)

nftables 命令行工具允许每次调用一个命令(如 /sbin/iptables):

1
2
3
$ nft add table ip kube-proxy '{ comment "Kubernetes service proxying rules"; }'
$ nft add chain ip kube-proxy services
$ nft add rule ip kube-proxy services ip daddr . ip protocol . th dport vmap @service_ips

或者在单个原子事务中执行多个命令(如 /sbin/iptables-restore,但更灵活):

1
2
3
4
5
$ nft -f - <<EOF
add table ip kube-proxy { comment "Kubernetes service proxying rules"; }
add chain ip kube-proxy services
add rule ip kube-proxy services ip daddr . ip protocol . th dport vmap @service_ips
EOF

两种模式的语法是相同的,只是在前一种情况下需要转义 shell 元字符。

从内核读取数据时(nft list ...),nft 以嵌套的“对象”形式输出数据:

1
2
3
4
5
6
7
8
$ nft list table ip kube-proxy
table ip kube-proxy {
  comment "Kubernetes service proxying rules";

  chain services {
    ip daddr . ip protocol . th dport vmap @service_ips
  }
}

(也可以将数据以这种形式传递给 nft -f,但这对我们没有用,因为我们必须传递 table ip kube-proxy 的全部内容,而不是仅仅添加、删除和更新我们想要更改的特定规则、集合等。)

nft 也有一个 JSON API,理论上对于编程使用来说比“纯文本”API 更好。不幸的是,这种模式下规则的表示方式与“纯文本”模式下规则的表示方式大不相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ nft --json list table ip kube-proxy | jq .
...
    {
      "rule": {
        "family": "ip",
        "table": "kube-proxy",
        "chain": "services",
        "handle": 19,
        "expr": [
          {
            "vmap": {
              "key": {
                "concat": [
                  {
                    "payload": {
                      "protocol": "ip",
                      "field": "daddr"
                    }
                  },
                  {
                    "payload": {
                      "protocol": "ip",
                      "field": "protocol"
                    }
                  },
                  {
                    "payload": {
                      "protocol": "th",
                      "field": "dport"
                    }
                  }
                ]
              },
              "data": "@service_ips"
            }
          }
        ]
      }
...

虽然很明显如何在这两种形式之间来回转换这个特定规则,但没有办法在不为每种规则类型编写单独代码的情况下在所有规则之间来回映射。此外,个别规则的 JSON 语法记录不充分,网上几乎所有的示例(包括 nftables 维基、随机博客文章等)都使用非 JSON 语法。因此,如果我们在 kube-proxy 中使用 JSON 语法,会使代码更难理解和维护。

因此,我们的计划是,对于我们内部的 nftables API:

  • 在传递数据 nft 时,我们将使用纯文本 API。特别是,这意味着所有的 add rule ... 命令将使用文档详细记录的纯文本规则形式。
  • 在从 nft 读取数据时,我们将使用 JSON API,以确保结果是明确可解析的(而不必假设 nft 在纯文本模式下输出特定情况的确切空白、标点等)。

这意味着我们的内部 nftables API 将不支持以“可读”形式读取规则。然而,考虑到我们的内部 iptables API(pkg/util/iptables)也不明确支持这一点,并且这对 iptables 后端不是问题,因此这不应成为问题。

关于此 KEP 中示例规则的说明

下面的示例全部以纯文本“对象”形式显示数据,但这只是为了方便读者,并不对应于我们将写入数据的形式(多命令事务形式)或我们将读取数据的形式(JSON)。(同样,请注意,# 前缀的注释会被 nft 忽略,仅供 KEP 读者参考,而 comment "..." 注释是实际的对象元数据,会存储在 nftables 中,类似于 iptables 的 --comment "..."。每个表、链、集合、映射、规则和集合/映射元素都可以有自己的注释,所以如果我们愿意的话,有很多机会让规则集自我记录。)

下面的示例也都是特定于 IPv4 的,为了简化。在实际为 nft 编写规则时,我们需要在 “ip daddr” 和 “ip6 daddr” 之间适当地切换,以匹配 IPv4 或 IPv6 目标地址。这实际上相当简单,因为 nft 命令允许你创建“变量”(实际上是常量)并将它们的值替换到规则中。因此,我们可以让规则生成代码总是编写 “$IP daddr",然后传递 “-D IP=ip” 或 “-D IP=ip6” 给 nft 来修正它。

下面的每服务/每端点链名使用了哈希字符串来缩短名称,就像 iptables 后端中的那样(例如,"svc_4SW47YFZTEDKD3PK",该哈希值是从现有的 iptables 单元测试中复制出来的,恰好代表 “ns4/svc4:p80tcp")。然而,事实证明 nftables 链名可以比 iptables 链名长得多(256 个字符而不是 30 个字符),所以我们应该能够在 nftables 后端创建更具识别性的链名。

示例中的多字名称在使用下划线和连字符方面也不一致;下划线在大多数 nftables 文档中是标准的,但连字符更符合 iptables-kube-proxy 的风格。我们最终应选择其中一种。

(此外,下面的大多数示例实际上并未经过测试,可能存在语法错误。读者请注意。)

版本管理和兼容性

由于 nftables 的开发比最近的 iptables 活跃得多,我们需要更加关注内核和工具的版本问题。

nft 命令有一个 --check 选项,可以用来检查命令是否可以成功运行;它解析输入,然后(假设成功)将数据上传到内核,并要求内核检查它(但实际上不执行)。因此,通过在启动时运行一些 nft --check,我们应该能够确认哪些特性是工具和内核都支持的。

目前还不清楚 nftables 后端所需的最低内核或 nft 命令行版本是什么。下面示例中使用的最新特性是在 Linux 5.6 中添加的,该版本于 2020 年 3 月发布(尽管它们可以重写,以不需要该特性)。

有些用户可能无法从 iptablesipvs 后端升级到 nftables。(当然,nftables 后端将不支持 RHEL 7,而有些人仍在使用 RHEL 7 运行 Kubernetes。)

NAT 规则

一般服务调度

对于 ClusterIP 和外部 IP 服务,我们将使用 nftables 的“判决映射”来存储有关将流量调度到何处的逻辑,基于目标 IP、协议和端口。然后我们只需要一个实际规则将判决映射应用于所有入站流量。(或者,为 ClusterIP、ExternalIP 和 LoadBalancer IP 使用单独的判决映射可能更有意义?)无论哪种方式,服务调度大致将是 O(1),而不是 iptables 后端中的 O(n)

同样,对于 NodePort 流量,我们将使用仅匹配目标协议/端口的判决映射,规则设置为只检查发送到本地 IP 的数据包的 nodeports 映射。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
map service_ips {
  comment "ClusterIP, ExternalIP and LoadBalancer IP traffic";

  # type 子句定义了映射的数据类型;key 的类型在 `:` 的左侧,values 类型在右侧。
  # 在这种情况下,映射键是三个值的连接(“.”);一个 IPv4 地址、一个协议
  #(tcp/udp/sctp)和一个端口(又称 inet_service)。映射值是一个 verdict,
  # 即一组有限的 nftables 操作之一。在本例中,判决都是 goto 语句。

  type ipv4_addr . inet_proto . inet_service : verdict;

  elements {
    172.30.0.44 . tcp . 80 : goto svc_4SW47YFZTEDKD3PK,
    192.168.99.33 . tcp . 80 : goto svc_4SW47YFZTEDKD3PK,
    ...
  }
}

map service_nodeports {
  comment "NodePort traffic";
  type inet_proto . inet_service : verdict;

  elements {
    tcp . 3001 : goto svc_4SW47YFZTEDKD3PK,
    ...
  }
}

chain prerouting {
  jump services
  jump nodeports
}

chain services {
  # Construct a key from the destination address, protocol, and port,
  # then look that key up in the `service_ips` vmap and take the
  # associated action if it is found.

  ip daddr . ip protocol . th dport vmap @service_ips
}

chain nodeports
  # Return if the destination IP is non-local, or if it's localhost.
  fib daddr type != local return
  ip daddr == 127.0.0.1 return

  # If --nodeport-addresses was in use then the above would instead be
  # something like:
  #   ip daddr != { 192.168.1.5, 192.168.3.10 } return

  # dispatch on the service_nodeports vmap
  ip protocol . th dport vmap @service_nodeports
}

# Example per-service chain
chain svc_4SW47YFZTEDKD3PK {
  # 使用 inline vmap 发送到随机 endpoint chain
  numgen random mod 2 vmap {
    0 : goto sep_UKSFD7AGPMPPLUHC,
    1 : goto sep_C6EBXVWJJZMIWKLZ
  }
}

# Example per-endpoint chain
chain sep_UKSFD7AGPMPPLUHC {
  # masquerade hairpin traffic
  ip saddr 10.180.0.4 jump mark_for_masquerade

  # send to selected endpoint
  dnat to 10.180.0.4:8000
}
伪装

上面的示例规则包括

1
ip saddr 10.180.0.4 jump mark_for_masquerade

来伪装发件人流量,就像 iptables proxier 中一样。这假设存在一个名为 mark_for_masquerade 的链,未显示。

nftables 对 DNAT 和伪装有与 iptables 相同的约束;你只能在 prerouting 阶段进行 DNAT,并且只能在 postrouting 阶段进行伪装。因此,与 iptables 一样,nftables 代理必须在不同时间处理 DNAT 和伪装。一种可能性是简单地复制 iptables 代理中的现有逻辑,使用数据包标记在 prerouting 链和 postrouting 链之间进行通信。

然而,在 nftables 中应该可以不用标记或任何其他外部可见状态来实现这一点;我们可以创建一个 nftables set,并使用它在链之间传递信息。类似于:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 需要伪装的连接的 5 元组集合
set need_masquerade {
  type ipv4_addr . inet_service . ipv4_addr . inet_service . inet_proto;
  flags timeout ; timeout 5s ;
}

chain mark_for_masquerade {
  update @need_masquerade { ip saddr . th sport . ip daddr . th dport . ip protocol }
}

chain postrouting_do_masquerade {
  # 我们在这里使用 "ct original ip daddr" 和 "ct original proto-dst"
  # 因为此时数据包可能已经被 DNAT 过了。

  ip saddr . th sport . ct original ip daddr . ct original proto-dst . ip protocol @need_masquerade masquerade
}

这尚未测试,但一些内核 nftables 开发人员确认它应该可以工作。我们应测试以确保具有潜在高变动的 need_masquerade 集合不会成为性能问题。

会话亲和性

会话亲和性可以以与 iptables 代理大致相同的方式完成,只是使用更通用的 nftables “set” 框架,而不是 iptables recent 模块提供的特定于亲和性的集合版本。实际上,由于 nftables 允许任意的集合键,我们可以相对于 iptables 进行优化,每个服务只有一个亲和性集合,而不是每个端点一个集合。(如果我们愿意,我们还可以在未来更改亲和性键,例如,基于源 IP+端口,而不仅仅是源 IP。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
set affinity_4SW47YFZTEDKD3PK {
  # 源 IP . 目标 IP . 目标端口
  type ipv4_addr . ipv4_addr . inet_service;
  flags timeout; timeout 3h;
}

chain svc_4SW47YFZTEDKD3PK {
  # 检查每个端点的现有会话亲和性
  ip saddr . 10.180.0.4 . 80 @affinity_4SW47YFZTEDKD3PK goto sep_UKSFD7AGPMPPLUHC
  ip saddr . 10.180.0.5 . 80 @affinity_4SW47YFZTEDKD3PK goto sep_C6EBXVWJJZMIWKLZ

  # 发送到随机端点链
  numgen random mod 2 vmap {
    0 : goto sep_UKSFD7AGPMPPLUHC,
    1 : goto sep_C6EBXVWJJZMIWKLZ
  }
}

chain sep_UKSFD7AGPMPPLUHC {
  # 标记源具有对此端点的亲和性
  update @affinity_4SW47YFZTEDKD3PK { ip saddr . 10.180.0.4 . 80 }

  ip saddr 10.180.0.4 jump mark_for_masquerade
  dnat to 10.180.0.4:8000
}

# 同样适用于其他端点...

过滤规则

iptables 模式使用 filter 表进行三种规则:

丢弃或拒绝没有端点的服务的数据包

与服务调度一样,这可以很容易地用判决映射来处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
map no_endpoint_services {
  type ipv4_addr . inet_proto . inet_service : verdict
  elements = {
    192.168.99.22 . tcp . 80 : drop,
    172.30.0.46 . tcp . 80 : goto reject_chain,
    1.2.3.4 . tcp . 80 : drop
  }
}

chain filter {
  ...
  ip daddr . ip protocol . th dport vmap @no_endpoint_services
  ...
}

# helper chain needed because "reject" is not a "verdict" and so can't
# be used directly in a verdict map
chain reject_chain {
  reject
}
丢弃被 LoadBalancerSourceRanges 拒绝的流量

LoadBalancer 源范围的实现将类似于 ipvs kube 代理中的基于 ipset 的实现:我们使用一个集合来识别“受源范围限制的流量”,然后使用另一个集合来识别“被其服务的源范围 接受 的流量”。匹配第一个集合但不匹配第二个集合的流量将被丢弃:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
set firewall {
  comment "destinations that are subject to LoadBalancerSourceRanges";
  type ipv4_addr . inet_proto . inet_service
}
set firewall_allow {
  comment "destination+sources that are allowed by LoadBalancerSourceRanges";
  type ipv4_addr . inet_proto . inet_service . ipv4_addr
}

chain filter {
  ...
  ip daddr . ip protocol . th dport @firewall jump firewall_check
  ...
}

chain firewall_check {
  ip daddr . ip protocol . th dport . ip saddr @firewall_allow return
  drop
}

例如,添加一个具有负载均衡器 IP 10.1.2.3、端口 80 和源范围 ["192.168.0.3/32", "192.168.1.0/24"] 的服务将导致:

1
2
3
add element ip kube-proxy firewall { 10.1.2.3 . tcp . 80 }
add element ip kube-proxy firewall_allow { 10.1.2.3 . tcp . 80 . 192.168.0.3/32 }
add element ip kube-proxy firewall_allow { 10.1.2.3 . tcp . 80 . 192.168.1.0/24 }
强制接受 HealthCheckNodePort 的流量

iptables 模式添加了规则以确保允许流向 NodePort 服务的健康检查端口的流量通过防火墙。例如:

1
-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT

(还有一些规则接受已经被 conntrack 标记的任何流量。)

这在 nftables 中无法可靠地完成;accept(或 iptables 中的 -j ACCEPT)的语义是结束对 当前表 的处理。在 iptables 中,这有效地保证了数据包被接受(因为 -j ACCEPT 大多只在 filter 表中使用),但在 nftables 中,仍然有可能稍后在另一个表中对数据包调用 drop,导致它被丢弃。无法像在 iptables 中那样可靠地“绕过防火墙”;如果基于 nftables 的防火墙正在丢弃 kube-proxy 的数据包,那么你需要实际配置 那个防火墙 来接受它们。

然而,这种绕过防火墙的行为在某种程度上是遗留的;iptables 代理能够绕过 本地 防火墙,但无法绕过在云网络层实现的防火墙,而这可能是当今更常见的配置。使用非本地防火墙的管理员已经被要求正确配置这些防火墙以允许 Kubernetes 流量通过,合理的做法是将这一要求扩展到使用本地防火墙的管理员。

因此,nftables 后端将不会尝试复制这些 iptables 后端规则。

未来的改进

进一步的改进是可能的。

例如,不需要为每个端点进行单独的“发夹”检查将会很好。没有直接询问“此数据包是否具有相同的源和目标 IP?”的方法,但概念验证的 kpng nftables 后端 改用了这种方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
set hairpin {
  type ipv4_addr . ipv4_addr;
  elements {
    10.180.0.4 . 10.180.0.4,
    10.180.0.5 . 10.180.0.5,
    ...
  }
}

chain ... {
  ...
  ip saddr . ip daddr @hairpin jump mark_for_masquerade
}

更有效率的是,如果 nftables 最终获得了在规则处理过程中调用 eBPF 程序的能力(如 iptables 的 -m ebpf),那么我们可以编写一个简单的 eBPF 程序来检查“源 IP 等于目标 IP”,然后调用该程序,而不是需要大量冗余的 IP 集合。

如果我们这样做,那么就不需要每个端点的发夹检查规则了。如果我们也能摆脱每个端点的亲和性更新规则,那么我们可以完全去掉每个端点链,因为 dnat to ... 是允许的 vmap 判决:

1
2
3
4
5
6
7
8
9
chain svc_4SW47YFZTEDKD3PK {
  # FIXME 以某种方式处理亲和性

  # 发送到随机端点
  random mod 2 vmap {
    0 : dnat to 10.180.0.4:8000
    1 : dnat to 10.180.0.5:8000
  }
}

通过当前的 nftables 功能,这似乎是不可能的(在使用亲和性的情况下),但未来的功能可能会使其成为可能。

尚不清楚这种重写的权衡是什么,无论是在运行时性能方面,还是在规则集的管理员/开发者可理解性方面。

iptables kube-proxy 后端的变化

切换到一个人们必须选择加入的新后端,使我们有机会在我们不喜欢当前 iptables kube-proxy 行为的地方打破向后兼容性。

然而,如果我们打算最终将 nftables 模式设为默认模式,那么与 iptables 模式的差异将成为一个更大的问题,因此我们应该将这些变化限制在收益大于成本的情况下。

本地主机 NodePorts

iptables 模式下的 Kube-proxy 默认支持 127.0.0.1 (对于 IPv4 服务) 上的 NodePorts。(ipvs 模式的 Kube-proxy 不支持这一点,两种模式都不支持 IPv6 服务的本地主机 NodePorts,尽管 userspace 模式在单栈 IPv6 集群中支持这一点。)

本地主机 NodePort 流量在基于 DNAT 的 NodePorts 方法中无法正常工作,因为将本地主机数据包移动到 lo 以外的网络接口会导致内核将其视为“火星包”(无效包)并拒绝路由。有多种方法可以解决这个问题:

  1. userspace 方法:在用户空间代理数据包,而不是通过 DNAT 重定向它们。(userspace 代理对所有 IP 都这样做;本地主机 NodePorts 可以与 userspace 代理一起工作是一个巧合,而不是一个明确的特性。)

  2. iptables 方法:启用 route_localnet sysctl,它告诉内核永远不要将 IPv4 回环地址视为“火星包”,以便 DNAT 能够工作。这只适用于 IPv4;IPv6 没有对应的 sysctl。不幸的是,启用这个 sysctl 会打开安全漏洞(CVE-2020-8558),然后 kube-proxy 需要尝试关闭这些漏洞,它通过创建 iptables 规则来阻止所有 route_localnet 会阻止的数据包,_除了_我们想要的那些(这假设管理员没有更改其他某些 sysctls,如果我们没有设置 route_localnet 可能是安全更改的,并且根据一些报告,在某些配置中可能会阻止合法流量)。

  3. Cilium 方法:使用 eBPF 拦截 connect(2) 调用并在此处重写目标 IP,这样网络栈实际上从未看到目标为 127.0.0.1 / ::1 的数据包。(如同 userspace kube-proxy 的情况,这不是本地主机的特殊情况,这是 Cilium 进行服务代理的方式。)

  4. 如果你控制客户端,你可以在连接之前显式地将套接字绑定到 127.0.0.1 / ::1。(我不确定为什么这有效,因为数据包最终仍会从 lo 路由出去。)似乎不可能在创建套接字后“伪造”这一点,不过如同前一种情况,你可以通过使用 eBPF 拦截系统调用来实现这一点。

在关于此功能的讨论中,只提出了一个实际的用例:它允许你在一个 pod 中运行一个 docker registry,然后通过 127.0.0.1 使用 NodePort 服务来访问该 registry。Docker 默认将 127.0.0.1 视为“非安全 registry”(虽然 containerd 和 cri-o 不这样认为),因此在这种情况下不需要 TLS 认证;使用任何其他 IP 都需要设置 TLS 证书,使部署变得更复杂。(换句话说,这基本上是有意利用 CVE-2020-8558 警告的安全漏洞:启用 route_localnet 可能允许某人访问一个不需要认证的服务,因为它假设只有本地主机可以访问。)

在所有其他情况下,一般来说(尽管不总是方便)可以简单地重写使用节点 IP 而不是本地主机(或使用 ClusterIP 而不是 NodePort)。事实上,由于本地主机 NodePorts 在 ipvs 模式或 IPv6 中不起作用,许多以前在 127.0.0.1 上使用 NodePorts 的地方已经被重写以不再这样做(例如 contiv/vpp#1434)。

所以:

  • 没有办法使 IPv6 本地主机 NodePorts 与基于 NAT 的解决方案一起工作。
  • 使 IPv4 本地主机 NodePorts 与 NAT 一起工作的方式引入了一个安全漏洞,我们不一定有一个完全通用的方法来缓解它。
  • 该功能唯一常被提出的用例涉及以其文档描述为不安全且“仅适用于测试”的配置部署服务。
    • 所讨论的用例默认情况下适用于 cri-dockerd,但不适用于默认配置的 containerd 或 cri-o。
    • cri-dockerd、containerd 和 cri-o 都允许配置额外的“非安全 registry” IPs/CIDRs,因此管理员可以配置它们以允许针对 ClusterIP 的非 TLS 镜像拉取。

鉴于此,我认为我们不应该尝试在 nftables 后端支持本地主机 NodePorts。

NodePort 地址

除了本地主机问题之外,iptables kube-proxy 默认接受所有本地 IP 上的 NodePort 连接,这会导致从预期但意外的效果(“为什么人们可以从管理网络连接到 NodePort 服务?”)到明显错误的效果(“为什么人们可以在 LoadBalancer IP 上连接到 NodePort 服务?”)。

nftables 代理应默认仅在单个接口上打开 NodePorts,可能默认是在具有默认路由的接口上。(理想情况下,你确实希望它在持有云负载均衡器路由的接口上接受 NodePorts,但我们不一定能提前知道那是什么。)管理员可以使用 --nodeport-addresses 覆盖此设置。

服务 IP 的行为

nftables 代理将拒绝发送到活动集群 IP 上无效端口的流量。如果启用了 MultiServiceCIDRAllocator 功能门,它还将丢弃发送到未分配集群 IP 的流量。

为与管理员/调试/第三方规则集成定义 API

管理员有时希望添加规则来记录或丢弃某些数据包。由于 Kube-proxy 不断重写其规则,这使得添加的管理员规则可能会在添加后不久被删除,从而使这一操作变得困难。

同样,外部组件(例如网络策略实现)可能希望以定义良好的方式编写与 kube-proxy 规则集成的规则。

现有的 kube-proxy 模式没有提供任何显式的“API”来与其集成,尽管特别是 iptables 后端的某些实现细节(例如在 iptables 的 PREROUTING 阶段将数据包中的服务 IP 重写为端点 IP,以及在 POSTROUTING 之前不会发生伪装)实际上是 API,因为我们知道更改它们会导致显著的生态系统破坏。

我们应该在 nftables 后端中提供这些大规模“黑盒”保证的更强定义。与 iptables 相比,NFTables 在某些方面使这变得更容易,因为每个应用程序都需要创建自己的表,并且不会干扰其他人的表。如果我们记录我们用来连接到每个 nftables 钩子的 priority 值,那么管理员和第三方开发人员应该能够可靠地在 kube-proxy 之前或之后处理数据包,而无需修改 kube-proxy 的链/规则。

在管理员希望将规则插入到特定服务或端点链中间的情况下,我们将遇到与 iptables 后端相同的问题,即在更新规则时我们很难避免意外覆盖它们。此外,我们希望保持以后重新设计规则以更好地利用 nftables 功能的能力,如果我们正式允许用户修改现有规则,这将是不可能的。

一种可能性是添加“管理员覆盖” vmap,这通常是空的,但管理员可以为特定服务添加 jump/goto 规则以增强/绕过正常的服务处理。最初将这些排除在外并看看人们是否确实需要它们,或者在另一个表中创建规则是否足够,可能是有意义的。

1
2
3
4
5
<<[UNRESOLVED external rule integration API]>>

Tigera 目前正在 Calico 中实现 nftables 支持,因此希望到 1.32 版时,我们应该能够很好地了解它从 nftables kube-proxy 中需要哪些保证。

<<[/UNRESOLVED]>>

规则监控

鉴于 iptables API 的限制,以“标准”风格执行控制器循环将会极其低效:

1
2
3
4
5
for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}

(特别是,“getCurrentState”和“makeChanges”的组合比跳过“getCurrentState”每次从头重写所有内容更慢。)

过去,iptables 后端确实每次都从头重写所有内容:

1
2
3
4
for {
    desired := getDesiredState()
    makeChanges(desired, nil)
}

KEP-3453“最小化 iptables-restore 输入大小”改变了这一点,以提高性能:

1
2
3
4
5
6
7
for {
    desired := getDesiredState()
    predicted := getPredictedState()
    if err := makeChanges(desired, predicted); err != nil {
        makeChanges(desired, nil)
    }
}

也就是说,它在假设当前状态是正确的情况下进行增量更新,但如果更新失败(例如因为它假设存在一个不存在的链),kube-proxy 会退回到完全重写。(如果足够长时间过去了,它也最终会退回到完全更新。)

基于 iptables 的代理历史上也存在这样的问题:系统进程(特别是防火墙实现)有时会刷新所有 iptables 规则并以干净状态重新启动,从而完全破坏 kube-proxy。对此问题的初始解决方案是每隔 30 秒重新创建所有 iptables 规则,即使没有服务/端点发生变化。后来,这一方案改为创建一个“金丝雀”链,并每隔 30 秒检查一次金丝雀是否被删除,只有在金丝雀消失时才重新从头创建所有内容。

NFTables 提供了一种无需轮询即可监控变化的方法;你可以保持一个 netlink socket 与内核(或一个打开的 nft monitor 进程的管道)连接,并在特定类型的 nftables 对象被创建或销毁时接收通知。

然而,nftables 的“每个人都使用自己的表”设计意味着这不应该是必要的。基于 iptables 的防火墙实现刷新所有 iptables 规则,因为每个人的 iptables 规则都混在一起,很难做到不这样做。但在 nftables 中,防火墙应该只在重新启动时刷新_自己的_表,并且不触及其他人的表。特别是,当使用 nftables 时,firewalld 就是这样工作的。我们需要看看其他防火墙实现会怎么做。

切换 kube-proxy 模式

过去,kube-proxy 试图允许用户通过重新启动 kube-proxy 并使用新的参数来在 userspaceiptables 模式(后来还有 ipvs 模式)之间切换。每种模式在启动时都会尝试清理其他模式使用的 iptables 规则。

不幸的是,这种方法效果不佳,因为这三种模式都使用了一些相同的 iptables 链。例如,当 kube-proxy 以 iptables 模式启动时,它会尝试删除 userspace 规则,但这也会删除 iptables 模式创建的规则,这意味着每次重新启动 kube-proxy 时,它都会立即删除一些规则,并处于损坏状态,直到它从 apiserver 重新同步。因此,这段代码已通过 KEP-2448 被移除。

然而,在 iptables 模式和 nftables 模式之间切换时,这个问题不会发生;当 kube-proxy 以 nftables 模式启动时,删除所有 iptablesipvs 规则是安全的,而当 kube-proxy 以 iptablesipvs 模式启动时,删除所有 nftables 规则也是安全的。这将使用户更容易在模式之间切换。

由于从 nftables 模式回滚最重要的是在 nftables 模式实际上无法正常工作时,我们应尽最大努力确保在回滚到 iptables/ipvs 模式时运行的清理代码,即使其余的 nftables 代码已损坏,也能正常工作。为此,我们可以直接运行 nft,绕过其余代码使用的抽象。由于我们的规则将被隔离在我们自己的表中,清理所有规则所需做的只是:

1
2
nft delete table ip kube-proxy
nft delete table ip6 kube-proxy

事实上,这足够简单,我们可以明确地将其记录为管理员在回滚时遇到问题时可以执行的操作。

测试计划

[X] 我/我们理解相关组件的所有者可能需要更新现有测试,以使此代码在提交实施此增强功能所需的更改之前足够稳固。

前置测试更新

无。(我们曾考虑重构 iptables 单元测试,以便在两个后端之间共享相同的测试,但我们最终只是复制了它们。)

单元测试

我们将为 nftables 模式添加与 iptables 模式相当的单元测试。特别是,我们将移植那些将服务和 EndpointSlices 输入代理引擎,转储生成的规则集,然后模拟通过规则集运行数据包以确定其行为的测试。

由于几乎所有新代码都将在一个新目录中,因此任何现有目录的测试覆盖率都不会有大变化。

截至2023年9月22日,pkg/proxy/iptables 的单元测试代码覆盖率为70.6%。对于 Alpha 阶段,我们将为 nftables 达到相当的覆盖率。然而,由于 nftables 实现是新的,并且比较旧且广泛使用的 iptables 实现更容易出现错误,我们还将在 Beta 阶段之前添加额外的单元测试。

  • k8s.io/kubernetes/pkg/proxy/nftables: 2024-05-24 - 74.7%

作为对比:

  • k8s.io/kubernetes/pkg/proxy/iptables: 2024-05-24 - 68.4%
  • k8s.io/kubernetes/pkg/proxy/ipvs: 2024-05-24 - 60.9%
集成测试

Kube-proxy 没有集成测试。

e2e 测试

kube-proxy 的大多数 e2e 测试是后端无关的。最初,我们将需要一个单独的 e2e 作业来测试 nftables 模式(就像我们对 ipvs 所做的那样)。最终,如果 nftables 成为默认设置,那么这将被调整为具有遗留的 “iptables” 作业。

测试“[它应该在 iptables 规则被删除时重新创建它们]”测试 (a) kubelet 在 KUBE-IPTABLES-HINT 被删除时重新创建它,(b) 删除所有 KUBE-* iptables 规则不会导致服务永远中断。这部分显然在 nftables kube-proxy 下是无效的,但我们仍然可以运行它。(我们目前假设我们不需要此测试的 nftables 版本,因为一个组件删除另一个组件的规则的问题不应该存在于 nftables。)

(虽然与 kube-proxy 直接无关,但还有其他使用 iptables 的 e2e 测试最终应该移植到 nftables;特别是,使用 TestUnderTemporaryNetworkFailure 的那些测试。)

在大多数情况下,我们不需要添加任何特定于 nftables 的 e2e 测试;nftables 后端的工作只是实现与其他后端相同规范的服务代理 API,因此现有的 e2e 测试已经涵盖了所有相关内容。唯一的例外是我们更改了 iptables 后端的默认行为的情况,在这种情况下,我们可能需要针对不同的行为进行新测试。

我们最终需要 e2e 测试在现有集群中切换 iptablesnftables 模式。

可扩展性与性能测试

我们有一个 nftables 可扩展性作业。初步性能良好;我们还没有进行大量的进一步测试/改进。

毕业标准

Alpha

  • kube-proxy --proxy-mode nftables 可在一个功能开关后启用
  • nftables 模式的单元测试与 iptables 相当
  • 存在一个 nftables 模式的 e2e 作业,并且通过
  • 文档描述了 iptablesipvs 模式与 nftables 模式之间的任何行为变化。
  • 文档解释了在情况非常糟糕时如何手动清理 nftables 规则。

Beta

  • 自 Alpha 以来至少有两个发布版本。
  • nftables 模式已经有至少一些实际使用情况。(是的;我们已经收到了用户实验时的错误报告和 PR。)
  • 没有主要的未解决错误。
  • nftables 模式具有比 iptables 模式更好的单元测试覆盖率(目前)。(在此过程中,我们可能会最终为 iptables 后端添加相应的单元测试。)
  • 存在一个“kube-proxy 模式切换” e2e 作业,确认您可以在现有集群中以不同模式重新部署 kube-proxy。回滚已被确认是可靠的。
  • 存在一个 nftables e2e 定期性能/规模作业,并且显示出与 iptables 和 ipvs 一样好的性能。
  • 文档描述了 iptablesipvs 模式与 nftables 模式之间的任何行为变化。我们决定为使用在 nftables 中表现不同的功能的 iptables 用户添加的任何警告已经添加。

GA

  • 自 Beta 以来至少有两个发布版本。
  • nftables 模式已经有显著的实际使用情况。
  • nftables 模式没有会让我们犹豫推荐它的错误/回归。
  • 我们至少有一个关于下一步计划的开始(改变默认模式、弃用旧的后端等)。
  • KEP 中没有未解决的部分。(特别是,我们已经弄清楚了我们将为集成第三方 nftables 规则提供哪种“API”。)

升级/降级策略

新模式不应引入任何升级/降级问题,除了您不能在使用新 kube-proxy 模式的集群中降级或禁用功能,除非先将其切换回 iptablesipvs。(如果使用 --proxy-mode nftables,旧的 kube-proxy 将拒绝启动,并且如果存在任何陈旧的 nftables 服务规则,它也不知道如何清理它们。)

在推出或回滚该功能时,启用功能开关并同时更改配置应该是安全的,因为除了 kube-proxy 本身之外,没有其他组件关心功能开关。同样,预计在活动集群中推出该功能是安全的,即使这会导致不同节点上运行不同的代理模式,因为 Kubernetes 服务代理的定义方式使得任何节点都不需要了解其他节点上服务代理实现的实现细节。

版本偏差策略

该功能仅限于 kube-proxy,并且不引入任何 API 更改,因此其他组件的版本无关紧要。

kube-proxy 在不同节点上与不同版本的自身偏差没有问题,因为 Kubernetes 服务代理的定义方式使得任何节点都不需要了解其他节点上服务代理实现的实现细节。

生产就绪审查问卷

功能启用和回滚

如何在活动集群中启用/禁用此功能?

管理员必须启用功能开关以使该功能可用,然后必须运行带有 --proxy-mode=nftables 标志的 kube-proxy。

  • 功能开关(也在 kep.yaml 中填写值)
    • 功能开关名称:NFTablesProxyMode
    • 依赖功能开关的组件:
      • kube-proxy
  • 其他
    • 描述机制:
      • 必须重新启动 kube-proxy 并使用新的 --proxy-mode
    • 启用/禁用该功能是否需要控制平面的停机时间?
      • 不需要
    • 启用/禁用该功能是否需要节点的停机时间或重新配置?(不要假设启用了 Dynamic Kubelet Config 功能)。
      • 不需要

启用该功能会改变任何默认行为吗?

启用功能开关不会改变任何行为;它只是使 --proxy-mode=nftables 选项可用。

--proxy-mode=iptables--proxy-mode=ipvs 切换到 --proxy-mode=nftables 可能会改变一些行为,具体取决于我们决定如何处理某些不受欢迎的 kube-proxy 功能,如本地主机 nodeports。任何存在的行为差异将由文档明确解释;这与用户从 iptables 切换到 ipvs 并没有什么不同,后者最初与 iptables 没有功能对等。

(假设我们最终将 nftables 设为默认值,那么与 iptables 的行为差异将更加重要,但将其设为默认值不是此 KEP 的一部分。)

启用该功能后能否禁用(即我们能否回滚启用)?

可以,不过有必要清理创建的 nftables 规则,否则它们将继续拦截服务流量。在任何正常情况下,当以 iptablesipvs 模式重新启动 kube-proxy 时,这应该会自动发生,然而,这假设用户回滚到一个仍然足够新的 kube-proxy 版本。如果用户想要将集群回滚到没有 nftables kube-proxy 代码的 Kubernetes 版本(即,从 Alpha 回滚到 Pre-Alpha),或者如果他们回滚到外部服务代理实现(例如,kpng),那么他们需要确保在回滚之前清理 nftables 规则,否则需要手动清理它们。(我们可以记录如何操作。)

(到我们考虑在未来将 nftables 后端设为默认值时,该功能将已经存在并且 GA 几个版本,因此在那时,回滚(到另一个版本的 kube-proxy)总是会回到一个仍然支持 nftables 并且可以正确清理它的版本。)

如果之前回滚过该功能,现在重新启用会发生什么?

它应该可以正常工作。

是否有功能启用/禁用的测试?

实际的功能开关启用/禁用本身并不有趣,因为它只控制是否可以选择 --proxy-mode nftables

我们将需要一个 e2e 测试,从 iptables(或 ipvs)模式切换到 nftables,反之亦然。毕业标准目前将此 e2e 测试列为 Beta 的标准,而不是 Alpha,因为我们并不真的期望人们将其现有集群切换到 Alpha 版本的 kube-proxy。

推出、升级和回滚计划

推出或回滚如何失败?它能影响已经运行的工作负载吗?

仅仅启用该功能(或升级到 Beta 版本)没有任何影响。管理员必须明确选择切换到新的后端。

切换到新的后端实际上不能“失败”,除了在存在错误的情况下,这可能会产生从“几乎察觉不到”到“完全灾难性”的结果。这样的失败几乎肯定会影响已经运行的工作负载。然而,每个节点必须独立切换到新的后端,所以任何特别严重的失败可能会在切换第一个节点后被注意到,并且可以在那时回滚。

除非在 nftables 清理代码中存在错误,否则回滚不应该会失败,该代码非常简单。

哪些特定的指标应该通知回滚?

如果 sync_proxy_rules_nftables_sync_failures_total 在增长,这表明有_某些_问题,并且 kube-proxy 日志可能提供更多信息。

是否测试了升级和回滚?是否测试了升级->降级->升级路径?

待定;我们计划在 1.31 添加一个 e2e 作业来测试从 iptables 模式切换到 nftables 模式。

该推广是否伴随着任何特性、API、API类型字段、标志等的弃用和/或删除?

新的后端与 iptables 后端并不是 100% 兼容。这将被记录在案,并且在 iptables 后端中有新的指标可以帮助用户确定他们是否依赖于在 nftables 中未实现或工作方式不同的特性。

监控要求

操作员如何确定工作负载是否在使用该特性?

操作员是启用该特性的人,他们可以通过查看 kube-proxy 配置来确定该特性是否在使用中。

使用该特性的人如何知道它在其实例中正常工作?

  • 其他(作为最后的手段)
    • 详情:如果服务仍然正常工作,那么该特性就正常工作。

该增强功能的合理 SLO(服务级目标)是什么?

对于 Beta 版本,目标是 网络编程延迟 应等同于旧的、KEP-3453 之前的 iptables 性能(因为当前代码尚未经过大量优化)。

对于 GA 版本,目标是它至少与当前的 iptables 性能一样好。

操作员可以使用哪些 SLI(服务级指标)来确定服务的健康状况?

sync_proxy_rules_nftables_sync_failures_total 表示失败的同步次数;如果这个数字在增长,表明后端以某种方式在失败。

各种通用的 kube-proxy 指标如 network_programming_duration_secondssync_proxy_rules_duration_seconds 也存在,可以用来检查变更是否被及时处理,以及单个同步是否花费合理时间。

目前还不清楚哪些 nftables 特定的指标会有趣。例如,在 iptables 后端中我们有 sync_proxy_rules_iptables_total,它告诉你 kube-proxy 已编程的 iptables 规则总数。但在 nftables 后端中,等效的指标不会那么有趣,因为在 iptables 后端用规则完成的许多事情在 nftables 后端将用映射和集合完成。同样,仅统计“规则和集合/映射元素的总数”可能没有用,因为集合和映射的整个意义在于它们具有大致 O(1) 的行为,所以知道元素的数量不会给你很多关于系统性能好坏的信息。

(在升级到 Beta 版本时的更新:仍然不清楚。)

  • 指标
    • 指标名称:
      • network_programming_duration_seconds(已存在)
      • sync_proxy_rules_last_queued_timestamp_seconds(已存在)
      • sync_proxy_rules_last_timestamp_seconds(已存在)
      • sync_proxy_rules_duration_seconds(已存在)
      • sync_proxy_rules_nftables_sync_failures_total
      • sync_proxy_rules_nftables_cleanup_failures_total
    • 暴露指标的组件:
      • kube-proxy

是否有任何缺失的指标可以提高该特性的可观测性?

我们现在在 iptables 模式中添加了一些指标(例如,kubeproxy_iptables_ct_state_invalid_dropped_packets_total),使用户能够了解他们是否依赖于在 nftables 后端中工作方式不同的特性,帮助用户决定他们是否可以迁移到 nftables,以及是否需要任何非标准配置来实现迁移。

依赖关系

该特性是否依赖于集群中运行的任何特定服务?

它可能需要一些用户当前没有的较新内核。它不依赖于集群中的其他任何东西。

可扩展性

启用/使用此特性是否会导致任何新的 API 调用?

不会。kube-proxy 仍在使用相同的 Service/EndpointSlice 监控代码,只是本地对结果进行了不同的处理。

启用/使用此特性是否会引入新的 API 类型?

不会。

启用/使用此特性是否会导致任何新的云提供商调用?

不会。

启用/使用此特性是否会增加现有 API 对象的大小或数量?

不会。

启用/使用此特性是否会增加现有 SLI/SLO 覆盖的任何操作所需的时间?

不会。

启用/使用此特性是否会导致任何组件的资源使用(CPU、RAM、磁盘、IO 等)显著增加?

预期不会…

我们目前没有完全一致的对比;nftables 性能作业使用的 CPU 比相应的 iptables 作业多,但这是因为它不像 iptables 作业那样运行 minSyncPeriod: 10s,因此它更频繁地同步规则更改。(然而,它能够在不导致集群崩溃的情况下做到这一点,这强烈表明它确实更高效。)

故障排除

如果 API 服务器和/或 etcd 不可用,该特性会如何反应?

与当前的 kube-proxy 一样;更新会停止处理,直到 apiserver 再次可用。

其他已知的故障模式是什么?

如果未达到 SLO,应该采取哪些步骤来确定问题?

实施历史

  • 初始提案:2023-02-01
  • 合并:2023-10-06
  • Beta 更新:2024-05-24

缺点

添加一个新的官方支持的 kube-proxy 实现意味着 SIG Network 需要更多的工作(特别是如果我们不能很快弃用任何现有的后端)。

替换默认的 kube-proxy 实现会影响许多用户。

然而,不采取任何行动将导致最终许多用户无法使用默认的代理实现。

替代方案

继续改进 iptables 模式

我们已经对 iptables 模式进行了许多改进,并且可以进行更多改进。特别是,我们可以让 iptables 模式像 ipvs 模式那样使用 IP 集。

然而,即使我们能够解决 iptables 模式的所有性能问题,仍然存在迫在眉睫的弃用问题。

(另见 “iptables 内核子系统存在无法解决的性能问题”。)

修复 ipvs 模式

与其实现一个全新的 nftables kube-proxy 模式,我们可以尝试修复现有的 ipvs 模式。

然而,ipvs 模式除了使用 IPVS API 外,还广泛使用了 iptables API。所以虽然它解决了 iptables 模式的性能问题,但并没有解决弃用问题。因此我们至少需要将其重写为 IPVS+nftables 而不是 IPVS+iptables。

(另见 “kube-proxy 的 ipvs 模式不会拯救我们”。)

使用现有的基于 nftables 的 kube-proxy 实现

注释/约束/警告 中讨论。

创建基于 eBPF 的代理实现

另一种可能性是尝试用基于 eBPF 的代理后端来替换 iptablesipvs 模式,而不是基于 nftables 的模式。eBPF 非常流行,但众所周知,它也非常难以使用。

这种方法的一个问题是,从 eBPF 程序访问 conntrack 信息的 API 仅存在于最新的内核中。特别是,从 eBPF 进行 NAT 的 API 仅在最近发布的 6.1 内核中添加。大多数 Kubernetes 用户拥有足够新内核的时间还很长,我们无法依赖该 API。

因此,基于 eBPF 的 kube-proxy 实现最初将需要许多缺失功能的变通方法,增加其复杂性(并可能强迫进行一些架构选择,这些选择本来是无需进行的,以支持这些变通方法)。

一种有趣的基于 eBPF 的服务代理方法是使用 eBPF 截获 pod 中的 connect() 调用,并在数据包发送之前重写目标 IP。在这种情况下,不需要 eBPF conntrack 支持(尽管对于非本地服务连接,如通过 NodePorts 的连接,仍然需要)。这种方法的一个优点是,它可以很好地与未来可能的“多网络服务”想法集成,其中一个 pod 可能连接到解析为次级网络 IP 的服务 IP,而这个次级网络只有某些 pod 可以访问。在主网络命名空间中进行目标 IP 重写的“正常”服务代理的情况下,这将导致数据包不可传送(因为主网络命名空间没有到隔离次级 pod 网络的路由),但是在 connect() 时进行重写的服务代理会在连接离开 pod 网络命名空间之前重写连接,从而允许连接继续进行。

多网络工作仍处于非常早期的阶段,目前尚不清楚它是否会真正采用这种方式工作的多网络服务模型。(实际上也 有可能 使这种模型与主要基于主网络的代理实现一起工作;只是更复杂一些。)