当前位置:首页 > 技术 > 正文内容

剖析 kube-proxy IPVS 模式下的服务流量调度

访客 技术 2026年10月28日 1

在 Kubernetes 平台中,Service 抽象是微服务架构的核心,它为一组具有相同标签的 Pod 提供了稳定的网络端点和负载均衡能力。而 kube-proxy 则是实现这一 Service 机制的关键组件。深入理解 kube-proxy 的运作方式,对于掌握集群内部服务通信流程以及排查网络故障至关重要。

kube-proxy 提供了多种工作模式,包括 userspace、iptables 和 IPVS。其中,userspace 模式因性能问题已较少使用。传统的 iptables 模式在服务数量庞大时,会生成大量 iptables 规则链,这不仅导致规则更新时延增加,还会在大规模集群中引发显著的性能瓶颈。为了解决 iptables 模式的性能局限性,Kubernetes 从 v1.8 版本开始引入了 IPVS (IP Virtual Server) 模式的测试支持,并于 v1.11 版本达到通用可用 (GA) 状态。

IPVS 模式利用 Linux 内核中更高效的负载均衡技术,实现了规则的增量式更新,并且能够在服务更新过程中保持现有连接不中断,从而显著提升了在大规模集群中的性能和稳定性。

当前关于 kube-proxy 工作原理的文献,多以 iptables 模式为例,对 IPVS 模式的详细解析相对较少。本文将专注于揭示 kube-proxy IPVS 模式的内部机制。为了更彻底地理解其运作,我们将避免直接使用 Docker 或 Kubernetes 环境,而是通过 Linux 底层的网络工具进行模拟和演示。

Kubernetes 为每个 Pod 分配一个独立的网络命名空间 (Network Namespace)。在接下来的实验中,我们将手动创建网络命名空间,并启动简单的 HTTP 服务来模拟 Kubernetes 中的 Pod 实例。

本文的目标是模拟以下 Kubernetes Service 定义,并通过 IPVS 和 ipset 工具探索其流量转发原理:

apiVersion: v1
kind: Service
metadata:
  name: app-svc-test
spec:
  clusterIP: 192.168.10.100
  selector:
    app: webserver
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

按照本文的步骤,最终您将能够通过执行 curl 192.168.10.100:8080 命令,访问到我们手动创建的网络命名空间中的 HTTP 服务。

实验环境准备

请确保您的 Linux 系统(本文基于 Ubuntu 20.04 进行测试)已开启 IP 转发功能:

$ sysctl -w net.ipv4.ip_forward=1

接下来,我们将配置一个模拟 Kubernetes Pod 网络的实验环境:

  • 创建一个名为 k8s-bridge 的虚拟网桥。
  • 创建两个网络命名空间 ns-app-a 和 ns-app-b,它们将模拟 Kubernetes 中的两个 Pod。
  • 为每个网络命名空间配置 DNS 解析。
  • 创建两个 veth pair (虚拟以太网对),其一端连接到宿主机上的 k8s-bridge,另一端连接到各自的网络命名空间。
  • 为 ns-app-a 中的 veth 设备分配 IP 地址 172.18.0.101。
  • 为 ns-app-b 中的 veth 设备分配 IP 地址 172.18.0.102。
  • 为每个网络命名空间设置默认路由。
  • 添加 iptables 规则,允许流量通过 k8s-bridge 接口转发。
  • 添加 iptables 规则,对来自 172.18.0.0/24 网段的流量进行 IP 伪装 (MASQUERADE)。
$ ip link add dev k8s-bridge type bridge
$ ip address add 172.18.0.1/24 dev k8s-bridge

$ ip netns add ns-app-a
$ mkdir -p /etc/netns/ns-app-a
$ echo "nameserver 8.8.8.8" | tee -a /etc/netns/ns-app-a/resolv.conf
$ ip netns exec ns-app-a ip link set dev lo up
$ ip link add dev veth-host-a type veth peer name veth-ns-a
$ ip link set dev veth-host-a master k8s-bridge
$ ip link set dev veth-host-a up
$ ip link set dev veth-ns-a netns ns-app-a
$ ip netns exec ns-app-a ip link set dev veth-ns-a up
$ ip netns exec ns-app-a ip address add 172.18.0.101/24 dev veth-ns-a

$ ip netns add ns-app-b
$ mkdir -p /etc/netns/ns-app-b
$ echo "nameserver 8.8.8.8" | tee -a /etc/netns/ns-app-b/resolv.conf
$ ip netns exec ns-app-b ip link set dev lo up
$ ip link add dev veth-host-b type veth peer name veth-ns-b
$ ip link set dev veth-host-b master k8s-bridge
$ ip link set dev veth-host-b up
$ ip link set dev veth-ns-b netns ns-app-b
$ ip netns exec ns-app-b ip link set dev veth-ns-b up
$ ip netns exec ns-app-b ip address add 172.18.0.102/24 dev veth-ns-b

$ ip link set k8s-bridge up
$ ip netns exec ns-app-a ip route add default via 172.18.0.1
$ ip netns exec ns-app-b ip route add default via 172.18.0.1

$ iptables -t filter -A FORWARD -i k8s-bridge -j ACCEPT
$ iptables -t filter -A FORWARD -o k8s-bridge -j ACCEPT

$ iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -j MASQUERADE

现在,我们将在网络命名空间中启动简单的 HTTP 服务器。请在不同的终端窗口中执行以下命令:

在第一个终端中,为 ns-app-a 启动 HTTP 服务:

$ ip netns exec ns-app-a python3 -m http.server 8080 --bind 172.18.0.101

在第二个终端中,为 ns-app-b 启动 HTTP 服务:

$ ip netns exec ns-app-b python3 -m http.server 8080 --bind 172.18.0.102

测试各个网络命名空间之间是否能正常通信:

$ curl 172.18.0.101:8080
$ curl 172.18.0.102:8080
$ ip netns exec ns-app-a curl 172.18.0.102:8080
$ ip netns exec ns-app-b curl 172.18.0.101:8080

上述测试均应成功返回 HTTP 服务的内容。至此,我们的模拟网络环境已准备就绪。

安装必要工具

为了便于调试和管理 IPVS 规则与 IP 集合,我们需要安装 ipset 和 ipvsadm 工具:

$ apt install ipset ipvsadm --yes

通过 IPVS 模拟 Service

现在,我们将使用 ipvsadm 工具创建一个虚拟服务 (Virtual Service) 来模拟 Kubernetes 中的 Service:

$ ipvsadm -A -t 192.168.10.100:8080 -s rr
  • -A 或 --add-service:添加一个虚拟服务。
  • -t 192.168.10.100:8080 或 --tcp-service 192.168.10.100:8080:指定 TCP 协议的虚拟服务 IP 和端口。
  • -s rr 或 --scheduler rr:选择轮询 (Round-Robin) 调度算法。这是 IPVS 相较于 iptables 的一个优势,能够灵活选择多种调度算法。

创建虚拟服务后,还需要为它指定一个后端真实服务器 (Real Server),即 ns-app-a 中的 HTTP 服务:

$ ipvsadm -a -t 192.168.10.100:8080 -r 172.18.0.101:8080 -m
  • -a 或 --add-server:为指定的虚拟服务添加一个真实服务器。
  • -r 172.18.0.101:8080 或 --real-server 172.18.0.101:8080:指定真实服务器的 IP 地址和端口。
  • -m 或 --masquerading:启用 IP 伪装模式。这意味着当数据包从真实服务器返回客户端时,源 IP 将被修改为虚拟服务的 IP。如果禁用此选项,IPVS 会尝试使用路由表进行转发,但通常在 Docker/Kubernetes 等场景下,这无法正常工作,因为回程流量需要进行 NAT 反转。由于 IPVS 本身没有 POST_ROUTING 钩子点,它需要依赖 iptables 来完成 IP 伪装等功能。

现在,从宿主机上测试虚拟服务是否能正常工作:

$ curl 192.168.10.100:8080

如果一切顺利,您应该能收到 ns-app-a HTTP 服务返回的内容。

在网络命名空间中访问虚拟服务

刚才的测试是在宿主机网络命名空间中进行的。现在,我们进入 ns-app-b 网络命名空间中进行测试:

$ ip netns exec ns-app-b curl 192.168.10.100:8080

您会发现访问失败了!这是因为 192.168.10.100 这个虚拟 IP 尚未被宿主机的任何网络接口所拥有,内核无法将其识别为本地地址。当数据包到达宿主机时,Linux 内核的 Netfilter 框架会对其进行处理。IPVS 在 Netfilter 中的实现主要依赖于 LOCAL_IN、LOCAL_OUT、FORWARD 这些钩子点,但没有 PRE_ROUTING 钩子。这意味着数据包在进行路由判断之前,IPVS 无法对其目标 IP 进行转换。如果数据包的目标 IP (即 192.168.10.100) 不属于本地接口,在经过路由判断后,它将不会被送往 LOCAL_IN 钩子,而是被认为是需要转发到其他机器,或者直接被丢弃。因此,我们需要将这个虚拟 IP 分配给一个本地的网络接口,使内核认为这个 IP 地址是属于本机的,从而让 IPVS 在 LOCAL_IN 钩子中能够处理它。

使用 Dummy 接口

在 Kubernetes 中,为了实现 Service ClusterIP,通常会创建一个 dummy 虚拟网络接口。dummy 接口类似于 loopback 接口,但您可以创建多个,它提供了一种将 IP 地址"虚拟化"绑定到主机的方式,但实际上并不进行数据转发。它有两个主要用途:

  • 用于主机内部程序间的通信。
  • 由于 dummy 接口总是处于"up"状态,将其绑定 Service ClusterIP 可以确保 Service 地址的可用性不受物理网络接口状态的影响。

因此,我们创建一个 dummy 接口,并将其命名为 k8s-dummy0:

$ ip link add name k8s-dummy0 type dummy

然后,将虚拟 IP 192.168.10.100 分配给 k8s-dummy0 接口,并启用它:

$ ip addr add 192.168.10.100/32 dev k8s-dummy0
$ ip link set dev k8s-dummy0 up

此时再次尝试从 ns-app-b 访问 HTTP 服务:

$ ip netns exec ns-app-b curl 192.168.10.100:8080

您可能会发现仍然无法成功。为了解决这个问题,我们还需要启用一个重要的内核参数:net.bridge.bridge-nf-call-iptables。

bridge-nf-call-iptables 参数详解

Kubernetes 集群网络通常基于 Linux 网桥实现。每个 Pod 的虚拟网卡 (veth 设备) 都连接到宿主机上的网桥。同一节点上的 Pod 之间通信通常通过网桥进行二层转发,而跨节点通信则需要经过宿主机的物理网卡。

无论是 iptables 还是 IPVS 模式,Kubernetes Service 都会进行 DNAT (目的网络地址转换),将访问 ClusterIP:Port 的数据包转换成 Service 的某个后端 PodIP:Port。内核会将连接信息记录到 conntrack 表中。当后端 Pod 回复数据时,内核会根据 conntrack 表进行反向 NAT,确保数据包返回到正确的客户端。

然而,当 Pod 访问位于同一节点的 Service 时,可能出现问题:

  1. 客户端 Pod 发送数据包到 Service ClusterIP。
  2. 数据包经过 DNAT,目标 IP 被修改为同节点上的后端 Pod IP。
  3. 后端 Pod 收到数据包并回复。此时,由于客户端 Pod 和后端 Pod 都连接到同一个网桥,回复数据包可能直接通过二层转发返回给客户端 Pod,而跳过了 Netfilter 的三层处理 (包括 conntrack 表的反向查询)。
  4. 由于回复数据包没有经过 conntrack,客户端 Pod 接收到的数据包的源 IP 仍是后端 Pod IP,而不是 Service ClusterIP。这导致客户端认为这不是它发起的连接的回复,从而无法正常通信。

为了解决这种"同节点服务访问"问题,net.bridge.bridge-nf-call-iptables 参数必须设置为 1。当此参数启用时,Linux 网桥在进行二层转发时,也会调用 Netfilter/iptables 的三层规则,包括 conntrack 模块。这样就能确保即使是同网桥内的流量,也能被 iptables 规则和 conntrack 跟踪并正确处理。

因此,我们需要加载 br_netfilter 模块并启用此内核参数:

$ modprobe br_netfilter
$ sysctl -w net.bridge.bridge-nf-call-iptables=1

现在,再次测试从 ns-app-b 访问虚拟服务:

$ ip netns exec ns-app-b curl 192.168.10.100:8080

这次,您应该能成功收到 ns-app-a HTTP 服务返回的内容了!

开启 Hairpin(发夹)模式

虽然现在 ns-app-b 可以通过虚拟服务访问 ns-app-a,但我们还需要测试一个特殊的场景:当一个 Pod 通过 Service IP 访问它自己时(即发夹模式,Hairpin Mode)。让我们从 ns-app-a 中尝试通过虚拟服务访问 192.168.10.100:8080:

$ ip netns exec ns-app-a curl 192.168.10.100:8080

不出所料,访问再次失败。在这种情况下,数据包从 ns-app-a 发出,经过 DNAT 再次指向 ns-app-a 本身。为了让这种"自环"流量能够正常工作,我们需要为连接 Pod 的 veth 接口开启"发夹"模式。发夹模式允许数据包从一个端口进入网桥后,在不离开网桥的情况下,再次从同一个端口流出。

我们可以通过 brctl 命令为 veth-host-a 接口开启发夹模式:

$ brctl hairpin k8s-bridge veth-host-a on

再次进行测试:

$ ip netns exec ns-app-a curl 192.168.10.100:8080

仍然失败了!除了启用发夹模式,对于 IPVS 模式下的自环流量,我们还需要确保连接跟踪(conntrack)能与 IPVS 协同工作。这需要设置另一个内核参数:

$ sysctl -w net.ipv4.vs.conntrack=1

这个参数强制 IPVS 配合连接跟踪表进行数据包处理。IPVS 的负载均衡决策只针对连接的第一个数据包,后续属于同一连接的数据包必须由 conntrack 表优先处理和反转,以维持连接的完整性。在发夹模式下,如果没有 conntrack 的协同,IPVS 对回来的数据包无法正确处理,导致通信中断。

最后,再次测试:

$ ip netns exec ns-app-a curl 192.168.10.100:8080

这次,ns-app-a 成功通过虚拟服务访问了它自己的 HTTP 服务!

开启网桥混杂模式

如果集群中有大量的 Pod,并且所有 Pod 都需要支持发夹模式(即能通过 Service IP 访问自己),为每个连接到网桥的 veth 接口单独开启 hairpin 模式会非常繁琐。有一种更简便的方法:直接开启网桥的混杂模式。

在普通模式下,网卡只接收发送给本机 MAC 地址的数据包(包括广播包)。而混杂模式下,网卡会接收所有经过它的数据包,无论其目标 MAC 地址是否是本机。如果一个网桥开启了混杂模式,它会自动为所有连接到它的端口启用 hairpin 模式。这样,您只需一条命令即可实现所有 Pod 的发夹功能:

$ ip link set k8s-bridge promisc on

现在,即使您尝试关闭之前为 veth-host-a 接口手动开启的发夹模式,测试仍然能够通过:

$ brctl hairpin k8s-bridge veth-host-a off
$ ip netns exec ns-app-a curl 192.168.10.100:8080

使用 ipset 优化 MASQUERADE

在实验环境准备阶段,我们添加了一条 iptables 规则,对所有来自 172.18.0.0/24 网段的流量进行伪装:

$ iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -j MASQUERADE

然而,Kubernetes 通常为了性能考虑,只会对来自特定 IP 的流量进行伪装,而不是整个子网。如果每个 Pod 都对应一条单独的 iptables 规则,在大规模集群中仍然会导致规则数量庞大,抵消 IPVS 的部分性能优势。为了更完美地模拟 Kubernetes 并解决这一问题,我们将引入 ipset 工具。

首先,删除之前宽泛的 MASQUERADE 规则:

$ iptables -t nat -D POSTROUTING -s 172.18.0.0/24 -j MASQUERADE

现在,我们使用 ipset 创建一个名为 K8S-IPVS-LOOPBACK 的集合:

$ ipset create K8S-IPVS-LOOPBACK hash:ip,port,ip

这条命令创建了一个哈希集合,其成员由三个字段组成:目的 IP、目的端口、源 IP。这三个字段对应了 hairpin 模式下需要进行 IP 伪装的流量特征。

接着,向集合中添加一个条目,表示当流量的目的 IP 是 172.18.0.101,端口是 8080,并且源 IP 也是 172.18.0.101 时(即自环流量,经过 DNAT 后回到自身),需要进行伪装:

$ ipset add K8S-IPVS-LOOPBACK 172.18.0.101,tcp:8080,172.18.0.101

现在,无论有多少个 Pod,我们都只需要一条 iptables 规则来引用这个 ipset 集合:

$ iptables -t nat -A POSTROUTING -m set --match-set K8S-IPVS-LOOPBACK dst,dst,src -j MASQUERADE
  • -m set:启用 set 匹配模块。
  • --match-set K8S-IPVS-LOOPBACK dst,dst,src:匹配 K8S-IPVS-LOOPBACK 集合中的条目。这里的 dst,dst,src 指示 iptables 依次将数据包的"目的 IP"、"目的端口"、"源 IP"与 ipset 集合中的三元组进行匹配。

再次进行网络连通性测试,包括宿主机、ns-app-b 和 ns-app-a 内部的访问,确保一切正常:

$ curl 192.168.10.100:8080
$ ip netns exec ns-app-b curl 192.168.10.100:8080
$ ip netns exec ns-app-a curl 192.168.10.100:8080

所有测试应该都能成功通过。

新增虚拟服务的后端

最后,我们将 ns-app-b 中的 HTTP 服务也添加为虚拟服务的后端:

$ ipvsadm -a -t 192.168.10.100:8080 -r 172.18.0.102:8080 -m

同时,向 K8S-IPVS-LOOPBACK 集合中添加 ns-app-b 的自环流量条目,以确保其发夹模式下的 MASQUERADE 也能生效:

$ ipset add K8S-IPVS-LOOPBACK 172.18.0.102,tcp:8080,172.18.0.102

现在,执行多次以下测试命令:

$ curl 192.168.10.100:8080

您会发现虚拟服务根据轮询调度算法,将请求交替转发到 ns-app-a 和 ns-app-b 的 HTTP 服务。这表明 IPVS 的负载均衡功能已正常运作。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。