Kubernetes 基础体系 · 第 27/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

kube-proxy 与服务转发:iptables、IPVS、nftables、Session 和诊断

1. 先建立正确的模型:Service 不是一个监听进程

Kubernetes Service 是一个稳定的访问抽象,通常提供一个虚拟 IP、一个端口和一组后端 Pod。以典型的 ClusterIP 为例:

客户端 Pod
   |
   | 访问 10.96.10.20:80
   v
节点内核网络栈
   |
   | kube-proxy 写入的规则或虚拟服务
   v
选择一个后端 Pod
   |
   | 可能发生 DNAT、SNAT、路由
   v
Pod IP:8080

这里容易产生一个错误认识:kube-proxy 不是一个一直监听 ClusterIP:80 的普通 TCP 代理。它通常不在用户态接收每个连接,而是监听 Service、EndpointSlice 等 Kubernetes 对象的变化,然后把转发规则写入 Linux 内核的 iptablesIPVSnftables。真正处理数据包的是内核网络栈。

因此需要区分两条路径:

  1. 控制路径:kube-proxy 从 API Server 获取 Service 和 EndpointSlice,计算规则并写入节点。
  2. 数据路径:数据包进入节点后,由内核中的 netfilter、IPVS、路由和 conntrack 完成转发。

kube-proxy 的故障通常首先表现为控制路径没有正确收敛;但即使规则已经存在,数据路径仍可能被 CNI、路由、MTU、防火墙、conntrack 或 Pod 本身的问题阻断。


2. Service、EndpointSlice 与 kube-proxy 的关系

2.1 Service 只描述“如何访问”,EndpointSlice 描述“转发到哪里”

一个 Service 可能类似于:

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: default
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080
  type: ClusterIP
  sessionAffinity: None

各字段的含义是:

  • port: 80:客户端访问 Service 时使用的端口。
  • targetPort: 8080:后端 Pod 接收流量的端口。
  • selector:Service 控制器根据标签选择 Pod。
  • ClusterIP:Service 的虚拟地址,通常由控制面分配。
  • sessionAffinity:是否尝试让同一个客户端 IP 持续使用同一个后端。

Service 本身不保存完整的后端地址列表。现代 Kubernetes 主要通过 EndpointSlice 表示后端:

kubectl get endpointslice \
  -l kubernetes.io/service-name=web \
  -o wide

典型输出会包含:

NAME        ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
web-abc12   IPv4           8080    10.244.1.12,10.244.2.7   5m

实际输出格式会因 Kubernetes 版本和 kubectl 版本略有差异,但需要关注以下状态:

  • addresses:后端地址。
  • conditions.ready:是否可接收正常流量。
  • conditions.serving:终止过程中的 Pod 是否仍在提供服务。
  • conditions.terminating:后端是否正在终止。
  • nodeName:Endpoint 所在节点。
  • hints:拓扑感知路由可能使用的提示。

Service 控制器负责维护 EndpointSlice,kube-proxy 再根据 EndpointSlice 生成本节点上的转发状态。也就是说:

Pod 标签变化
  -> Service 控制器更新 EndpointSlice
  -> kube-proxy 观察到对象变化
  -> kube-proxy 重新计算节点规则
  -> 新连接使用新的后端集合

这条链路中任何一环延迟或失败,都可能造成“Service 对象看起来正确,但节点实际仍转发到旧 Pod”的现象。

2.2 端口不等于监听成功

Service 中写了 targetPort: 8080,并不代表 Pod 内一定有进程监听 8080。例如:

kubectl exec deploy/web -- ss -lntp

如果容器没有监听目标端口,Service 规则仍然可能正确,数据包也可能成功 DNAT 到 Pod IP,但最终收到 connection refused 或超时。

因此应分别验证:

kubectl get svc web -o yaml
kubectl get endpointslice \
  -l kubernetes.io/service-name=web \
  -o yaml
kubectl exec deploy/web -- ss -lntp

这三步分别验证 Service 配置、后端集合和应用监听状态,不能用其中一项替代另外两项。


3. 一次 ClusterIP 请求究竟发生了什么

假设:

  • 客户端 Pod:10.244.1.10
  • Service ClusterIP:10.96.10.20
  • Service 端口:80
  • 后端 Pod:10.244.2.7:8080
  • 客户端访问:10.96.10.20:80

一个常见的数据路径如下:

10.244.1.10:45000
        |
        | 目的地址 10.96.10.20:80
        v
节点 netfilter / kube-proxy 规则
        |
        | DNAT
        v
10.244.2.7:8080
        |
        | 路由到后端节点或本机 Pod
        v
后端进程

3.1 DNAT 改变目的地址

Destination NAT,即 DNAT,会把报文目的地址从:

10.96.10.20:80

改为:

10.244.2.7:8080

内核必须记录这个连接的 NAT 状态,否则返回包无法恢复为客户端眼中的:

源地址:10.96.10.20:80

这正是 conntrack 的作用之一。对于 TCP 连接,后续数据包通常不会每次都重新执行一次“选择后端”的逻辑;已建立连接由连接跟踪状态关联到之前选定的 NAT 映射。

因此,“修改 Service 后新连接已经使用新 Pod,但旧连接仍然进入旧 Pod”并不一定是 kube-proxy 失效,而可能是已有连接和 conntrack 状态仍然有效。

3.2 是否需要 SNAT 取决于返回路径

如果客户端和后端位于不同节点,后端 Pod 返回客户端时必须有一条可达且对称的路径。CNI 插件可能通过 Pod 路由、隧道或其他方式实现这一点。

在某些场景中,kube-proxy 会对源地址做 SNAT,将客户端地址改为节点地址。这样后端只需把响应返回给节点,节点再通过 conntrack 还原给客户端。SNAT 提高了路径成功率,但代价是后端看不到原始客户端 IP。

这解释了两个常见现象:

  • 后端日志中看到的是节点 IP,而不是客户端 Pod IP。
  • 禁用 SNAT 或设置 externalTrafficPolicy: Local 后,源 IP 得以保留,但跨节点转发和负载均衡约束增加。

4. kube-proxy 的三种主要实现模式

kube-proxy 的 --proxy-mode 常见取值包括:

  • iptables
  • ipvs
  • nftables

另有历史上的 userspace 模式,但它已经不是现代生产部署的选择。具体默认值和可用模式取决于 Kubernetes 版本、节点内核、发行版和 kube-proxy 配置。

需要特别区分两件事:

  1. kube-proxy 模式:决定它如何把 Service 转发逻辑写入内核。
  2. iptables 后端实现:系统中的 iptables 命令可能通过传统 xtables 或 nftables 兼容层实现。

因此,iptables 模式不一定意味着内核底层完全没有 nftables;反过来,nftables kube-proxy 模式也不是简单地调用 iptables 命令。


5. iptables 模式:链式规则与 conntrack

5.1 基本结构

iptables 模式下,kube-proxy 通常在 nat 表中建立若干自定义链。不同版本的链细节可能变化,但常见结构包含:

  • KUBE-SERVICES:根据 Service 的 ClusterIP、端口和协议匹配。
  • KUBE-NODEPORTS:处理 NodePort。
  • KUBE-SVC-*:对应某个 Service 的后端选择逻辑。
  • KUBE-SEP-*:对应某个具体 Endpoint。
  • KUBE-MARK-MASQ:标记需要 SNAT 的流量。
  • KUBE-POSTROUTING:在 POSTROUTING 阶段执行伪装。

可以把一次选择抽象为:

KUBE-SERVICES
    |
    +-- Service A -> KUBE-SVC-A
                         |
                         +-- KUBE-SEP-1 -> DNAT Pod 1
                         +-- KUBE-SEP-2 -> DNAT Pod 2
                         +-- KUBE-SEP-3 -> DNAT Pod 3

较早版本常用一组概率规则近似实现随机选择。例如有三个后端时,逻辑上可以表示为:

第一次规则:以 1/3 概率选择 Endpoint 1
否则进入下一条
第二次规则:以 1/2 概率选择 Endpoint 2
否则选择 Endpoint 3

完整概率为:

P(Endpoint 1) = 1/3
P(Endpoint 2) = (2/3) × (1/2) = 1/3
P(Endpoint 3) = (2/3) × (1/2) = 1/3

这不是按请求数精确轮询,而是对符合条件的新连接执行概率选择。短时间内的连接数可能明显不均匀,尤其是在连接数很少、连接持续时间差异很大或部分客户端反复复用连接时。

现代 Kubernetes 版本会持续优化规则结构,不能依赖某个具体链名或规则顺序作为稳定 API。

5.2 iptables 的优点与代价

iptables 模式的优点是:

  • 成熟、普遍可用。
  • 依赖的内核能力广泛存在。
  • 规则可以直接通过 iptables-save 检查。
  • 与大量 Linux 网络工具和排障经验兼容。

代价是:

  • Service 和 Endpoint 数量很大时,规则数量可能增长明显。
  • 规则更新可能需要较多内核规则操作。
  • 排查时链路较长,容易混淆 Service 链、Endpoint 链和 SNAT 链。
  • 规则中的“选择后端”不等于应用层负载均衡。

5.3 查看 iptables 规则

在节点上执行:

sudo iptables-save -t nat | less

可以筛选某个 ClusterIP:

sudo iptables-save -t nat | grep '10\.96\.10\.20'

也可以看计数器:

sudo iptables -t nat -L KUBE-SERVICES -n -v --line-numbers

输出中的 pktsbytes 是规则命中计数。它们只能证明数据包经过某条规则,不能单独证明:

  • 后端应用成功处理了请求;
  • 返回路径正常;
  • TCP 三次握手完成;
  • 规则一定属于当前生效的 kube-proxy 状态。

此外,某些发行版的 iptables 命令是 nftables 兼容层。诊断时应确认后端:

iptables --version
nft --version

不要在生产节点直接执行:

iptables -F
iptables -t nat -F

这会破坏 kube-proxy、容器网络和主机防火墙的现有状态,可能造成整台节点的大范围中断。


6. IPVS 模式:虚拟服务、连接调度与现实边界

6.1 IPVS 如何表示一个 Service

IPVS,即 IP Virtual Server,是 Linux 内核中的四层负载均衡机制。kube-proxy 在 IPVS 模式下,会为 Service 创建一个虚拟服务,并为其挂载多个真实服务器。

抽象表示为:

虚拟服务:10.96.10.20:80/TCP
    |
    +-- 10.244.2.7:8080
    +-- 10.244.3.9:8080
    +-- 10.244.4.5:8080

IPVS 支持多种调度算法,例如:

  • rr:轮询。
  • lc:最少连接。
  • dh:目标地址哈希。
  • sh:源地址哈希。

但需要注意:IPVS 选择的是连接或流的后端,而不是 HTTP 请求。一个 HTTP/1.1 keep-alive 连接上的多个请求通常都会继续到同一个 Pod;一个 HTTP/2 连接也不会因为每个请求不同而重新经过 Service 层调度。

6.2 IPVS 的检查方式

sudo ipvsadm -Ln

典型结果类似:

TCP  10.96.10.20:80 rr
  -> 10.244.2.7:8080       Masq    3      12
  -> 10.244.3.9:8080       Masq    2       8

字段通常表示:

  • 虚拟地址和端口。
  • 调度算法。
  • 真实服务器地址和端口。
  • 转发方式。
  • 当前连接等计数。

实际列名和计数含义由 ipvsadm 版本决定。这里的 312 不能直接解释为请求数;它们通常与连接或报文统计相关。

IPVS 状态也可能通过 /proc 观察:

sudo cat /proc/net/ip_vs
sudo cat /proc/net/ip_vs_conn

6.3 IPVS 不会自动解决所有问题

IPVS 只负责其参与的四层转发逻辑,以下部分仍可能由 iptables、路由和 conntrack 完成:

  • NodePort 入口规则。
  • SNAT 和 Masquerade。
  • 某些防火墙和流量标记逻辑。
  • 本地或外部流量的特殊处理。
  • 容器网络到 Pod 网络的路由。

因此,看到 ipvsadm -Ln 中存在虚拟服务,并不能证明请求一定能到达 Pod。仍需检查:

sudo ipvsadm -Ln --stats
ip route
sudo conntrack -L

还要检查节点到 Pod IP 的实际可达性,以及 Pod 是否监听目标端口。

6.4 IPVS 的版本和维护风险

IPVS 曾经被广泛用于大规模 Service 场景,但它依赖内核 IPVS 模块和额外的状态管理。Kubernetes 社区近年来更重视 nftables 模式,并在较新的版本中对 IPVS 的维护状态和弃用计划作出调整。具体弃用阶段必须以所使用 Kubernetes 版本的官方发布说明和 kube-proxy 配置文档为准,不能因为某个集群仍能启动 IPVS 就认为它是长期推荐方案。

生产环境迁移模式时,需要验证:

  • 节点内核是否具备所需模块;
  • CNI 与 kube-proxy 的兼容性;
  • NodePort、LoadBalancer、双栈和本地流量策略;
  • 现有监控是否依赖 ipvsadm
  • 连接迁移期间的中断和回滚方式。

7. nftables 模式:集合、映射和更适合扩展的规则表达

7.1 nftables 与 iptables 的关系

nftables 是 Linux 新一代数据包过滤和分类框架。它提供集合、映射、原子规则更新等能力。kube-proxy 的 nftables 模式直接使用 nftables 规则表达 Service 转发逻辑,而不是通过传统 iptables 规则模型间接完成。

这与“iptables 命令使用 nftables 后端”不同:

iptables 模式
  kube-proxy -> iptables 规则接口 -> 可能由 xtables-nft 兼容层实现

nftables 模式
  kube-proxy -> nftables 规则模型

两者不能仅凭 nft list ruleset 是否有输出就判断为同一种模式。

7.2 nftables 的数据结构优势

Service 转发经常需要表达这样的关系:

(Service IP, port, protocol)
    -> Service 后端集合或映射

nftables 的集合和映射可以更自然地承载这类关系,规则更新也可以采用更适合批量变更的方式。与大量线性 iptables 规则相比,在 Service 和 Endpoint 数量较多的节点上,nftables 模式通常具有更好的扩展潜力。

但“扩展潜力更好”不是任何工作负载下的固定性能保证。实际结果仍受到以下因素影响:

  • 内核版本;
  • nftables 实现;
  • CNI 数据路径;
  • Service 和 Endpoint 数量;
  • 连接建立速率;
  • conntrack 表压力;
  • 是否启用双栈;
  • 节点 CPU 和规则同步频率。

7.3 检查 nftables 模式

sudo nft list ruleset

kube-proxy 的规则通常位于带有 Kubernetes 命名特征的 table 或 chain 中,但具体名称不能作为跨版本 API。更可靠的判断方式是检查 kube-proxy 配置:

kubectl -n kube-system get configmap kube-proxy -o yaml

如果集群使用 KubeProxyConfiguration,重点查看:

mode: nftables

也可以查看 kube-proxy 日志:

kubectl -n kube-system logs ds/kube-proxy --tail=200

不同发行版可能把 kube-proxy 配置放在本地文件或启动参数中,不能假设所有集群都使用同一个 ConfigMap。

7.4 nftables 模式的版本边界

nftables kube-proxy 模式是较新的能力,支持级别会随 Kubernetes 版本变化。某个版本可能处于 alpha、beta 或 stable,默认模式也可能不同。部署前必须同时核对:

  • Kubernetes 版本对应的官方 kube-proxy 文档;
  • 特性门控状态;
  • 节点内核版本;
  • 集群安装器是否覆盖了 kube-proxy 配置;
  • 云厂商托管控制面是否允许自定义 kube-proxy 模式。

不能把较新版本文档中的 nftables 配置直接复制到旧集群,也不能把某个发行版的默认值当作 Kubernetes API 的规范保证。


8. 三种模式的共同边界:调度的是连接,不是业务请求

无论使用 iptables、IPVS 还是 nftables,kube-proxy 通常工作在 L3/L4 层。它能识别的主要是:

  • 源和目的 IP;
  • TCP、UDP、SCTP 等协议;
  • 端口;
  • 连接跟踪状态。

它不知道以下应用层信息:

  • HTTP URL;
  • HTTP Host;
  • 用户身份;
  • 请求大小;
  • 请求是否幂等;
  • 一个连接中每个请求的业务负载。

因此下面这个算例很重要。

假设 Service 有两个后端:

Pod A:10.244.2.7:8080
Pod B:10.244.3.9:8080

客户端建立一条 TCP 连接:

10.244.1.10:45000 -> 10.96.10.20:80

第一次选择得到 Pod A。之后该连接上的 100 个 HTTP 请求通常都进入 Pod A,而不会变成 50 个请求到 A、50 个请求到 B。

如果希望按 HTTP 请求做负载均衡,应使用 Ingress、Gateway、服务网格或应用层代理等组件。Service 本身不提供 L7 请求级调度。


9. Session:SessionAffinity 的定义、算法边界与反例

9.1 ClientIP 是什么

Service 的 sessionAffinity 有:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600

含义是:对于同一个源 IP,尽量在指定时间内继续使用同一个后端 Endpoint。

默认通常是:

sessionAffinity: None

ClientIP 并不等价于:

  • 按用户登录状态绑定;
  • 按 Cookie 绑定;
  • 按 TCP 连接绑定;
  • 永久固定到某个 Pod;
  • 跨 Service 共享会话;
  • 在所有代理拓扑中识别真实终端 IP。

它依赖内核转发实现和客户端源地址。多个用户如果经过同一个 NAT 网关,在 Service 看来可能具有同一个源 IP,于是会被归为同一个客户端。

9.2 一个具体例子

有三个后端:

A、B、C

启用 ClientIP 后,来自:

源 IP 10.244.1.10

的连接可能被绑定到 B。只要相关会话状态仍在且未超过超时,后续新连接会尽量继续到 B。

但下面情况可能导致重新选择:

  1. SessionAffinity 超时。
  2. B 从 EndpointSlice 中移除。
  3. B 不再 Ready。
  4. kube-proxy 规则重新同步。
  5. conntrack 或 IPVS 会话状态丢失。
  6. 客户端源 IP 发生改变。
  7. 中间 NAT 设备改变了可见源地址。

所以它提供的是“粘性尝试”,不是强一致性会话保证。

9.3 SessionAffinity 与连接复用的叠加

如果客户端使用长连接,实际观察到的粘性可能来自两层机制:

TCP 连接保持
  + ClientIP 会话亲和
  + 后端集合未变化

即使关闭 ClientIP,已有 TCP 连接通常也不会自动迁移;即使启用 ClientIP,连接断开后也可能因为状态失效而选择新后端。

如果应用依赖登录会话,单纯启用 ClientIP 通常不够。更稳妥的设计是把状态放入共享存储,或者使用有明确语义的 Cookie/令牌策略。


10. Service 类型如何改变 kube-proxy 的路径

10.1 ClusterIP

ClusterIP 面向集群内部客户端:

Pod -> ClusterIP -> Endpoint

客户端访问的是虚拟 IP,kube-proxy 负责把它转换为后端地址。ClusterIP 通常只在集群内部有意义,外部主机不能仅凭路由就访问它。

10.2 NodePort

NodePort 在每个节点上暴露一个端口,例如:

spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080

外部客户端可以访问:

任意节点 IP:30080

路径通常为:

外部客户端
  -> 节点 IP:30080
  -> kube-proxy NodePort 规则
  -> Pod IP:8080

NodePort 是否能从节点本机访问、是否监听所有节点地址、是否受 --nodeport-addresses 限制,都取决于 kube-proxy 配置、内核行为和云环境。

10.3 LoadBalancer

type: LoadBalancer 主要是 Kubernetes API 对外部负载均衡器的抽象。云控制器或其他实现负责创建外部负载均衡器,kube-proxy 通常仍负责节点接收到流量后的 Service 到 Endpoint 转发。

因此:

LoadBalancer 创建成功

不等于:

节点上的 Service 转发正常

必须把外部负载均衡器、节点安全组、健康检查、NodePort 或直接 Pod 转发路径分别验证。

10.4 externalTrafficPolicy

常见配置:

spec:
  externalTrafficPolicy: Local

与默认的 Cluster 相比:

  • Cluster:节点可以把外部流量转发到其他节点的 Endpoint,通常更容易均匀利用后端,但可能需要 SNAT。
  • Local:节点只使用本节点上的 Endpoint。这样有机会保留外部客户端源 IP,但如果节点没有本地 Endpoint,流量可能失败或被健康检查排除。

Local 的关键不是“更快”,而是改变了可用后端集合:

Cluster:节点 N 可选择全集群 Endpoint
Local:节点 N 只能选择节点 N 上的 Endpoint

如果外部负载均衡器仍把流量发送到没有本地 Pod 的节点,而该节点没有被正确健康检查排除,就会出现连接失败或黑洞。

与之相近但不同的是:

spec:
  internalTrafficPolicy: Local

它控制集群内部流量是否只选择本节点 Endpoint。不要把 internalTrafficPolicyexternalTrafficPolicy 混为一谈。


11. SNAT、源地址保留与 Hairpin

11.1 SNAT 的因果关系

假设客户端和后端在不同网络,后端返回路径不能直接到达客户端。kube-proxy 或 CNI 可能执行 SNAT:

原始请求:
10.244.1.10:45000 -> 10.96.10.20:80

DNAT 后:
10.244.1.10:45000 -> 10.244.2.7:8080

SNAT 后:
节点IP:45000 -> 10.244.2.7:8080

后端看到的源地址变为节点 IP。conntrack 保存这组转换关系,使响应能够还原为客户端访问的 Service 地址。

如果业务需要真实客户端 IP,应确认:

  • Service 的 externalTrafficPolicy
  • 云负载均衡器是否使用代理协议;
  • Ingress 是否写入 X-Forwarded-For
  • CNI 是否改变源地址;
  • 后端日志记录的是 TCP 对端地址还是 HTTP 头。

不能仅凭“Service 使用了 Local”就断言业务一定获得真实终端 IP。

11.2 Hairpin 流量

Hairpin,也叫 hairpin NAT 或 hairpin traffic,指后端 Pod 通过 Service 访问自己,或者访问同一 Service 后又被选回同一个节点/Pod 的情况。

例如:

Pod A -> ClusterIP -> Pod A

某些网络路径中,如果源地址不做适当转换,Pod A 发出的请求到达 Pod A 后,响应可能绕过预期的 conntrack 路径,导致连接失败。kube-proxy 和 CNI 会针对常见场景处理 hairpin,但实现方式与容器运行时、桥接配置和 CNI 有关。

诊断时可以测试:

kubectl exec deploy/web -- \
  curl -v --connect-timeout 3 http://web.default.svc.cluster.local/

如果 Pod 通过 Service 访问自己失败,但其他 Pod 访问成功,应检查:

  • kube-proxy 的 Service 规则;
  • CNI bridge 的 hairpin 模式;
  • Pod 网卡和宿主机 veth;
  • conntrack;
  • 是否存在 NetworkPolicy 或主机防火墙阻断。

12. kube-proxy 的状态同步与并发行为

kube-proxy 并不是每收到一个数据包就向 API Server 查询后端。它通常维护本地缓存,监听对象事件,再异步同步内核状态。

因此从 Pod 删除到规则消失之间可能存在短暂窗口:

Pod 被删除
  -> EndpointSlice 更新
  -> kube-proxy 收到事件
  -> 计算新规则
  -> 写入内核
  -> 新连接使用新集合

这个过程受以下因素影响:

  • API Server 延迟;
  • kube-proxy 事件处理;
  • 规则计算耗时;
  • 内核规则更新;
  • EndpointSlice 数量;
  • 节点 CPU 和 I/O;
  • kube-proxy 与 API Server 的连接状态。

旧连接不一定会被主动迁移。即使后端已经从 EndpointSlice 删除,已有 conntrack、IPVS 连接或应用长连接仍可能继续存在,具体行为取决于协议状态和实现。

终止中的 Pod 还涉及连接排空。EndpointSlice 的 readyservingterminating 状态用于表达后端生命周期,但应用是否能正确处理 SIGTERM、停止接收新请求并等待旧请求完成,仍是应用自身责任。


13. 一个可运行的端到端实验

下面创建两个后端 Pod 和一个 ClusterIP Service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0
          args:
            - "-text=$(HOSTNAME)"
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  selector:
    app: echo
  ports:
    - name: http
      port: 80
      targetPort: 5678
  type: ClusterIP

应用:

kubectl apply -f echo.yaml
kubectl rollout status deployment/echo
kubectl get pod -l app=echo -o wide
kubectl get svc echo
kubectl get endpointslice \
  -l kubernetes.io/service-name=echo \
  -o wide

创建临时客户端:

kubectl run curl \
  --image=curlimages/curl \
  --restart=Never \
  -it --rm -- sh

在容器内执行:

for i in $(seq 1 10); do
  curl -s http://echo.default.svc.cluster.local/
  echo
done

预期可能看到两个不同的 Pod 名称,但不保证严格一半一半。原因包括:

  • 每个 curl 可能建立一个新连接;
  • kube-proxy 的选择是连接级别;
  • 调度算法可能是概率或实现相关;
  • 连接复用会减少重新选择次数;
  • Endpoint 变化会改变可选集合。

查看 DNS 解析:

getent hosts echo.default.svc.cluster.local

通常会得到 Service 的 ClusterIP。DNS 只负责把名称解析为 Service 地址,并不负责选择后端 Pod。后端选择发生在节点上的 Service 转发路径。

清理:

kubectl delete -f echo.yaml

这个实验验证了三层关系:

DNS 名称 -> ClusterIP
ClusterIP -> Endpoint
Endpoint -> Pod 进程端口

任何一层出错,表象都可能是“访问 Service 失败”,但修复方式完全不同。


14. 系统化诊断:从对象到数据包

诊断 Service 转发时,建议沿着实际数据路径逐层缩小范围,而不是直接修改 kube-proxy 规则。

14.1 第一步:确认 Service 对象

kubectl get svc echo -o yaml

重点检查:

  • clusterIP 是否为 None
  • ports.port 与客户端访问端口是否一致;
  • targetPort 是否为正确端口名或数字;
  • selector 是否匹配预期 Pod;
  • internalTrafficPolicy
  • externalTrafficPolicy
  • sessionAffinity

如果 clusterIP: None,这是 Headless Service。它通常不依赖 kube-proxy 为一个虚拟 ClusterIP 做传统转发,而是由 DNS 返回 Endpoint 地址。把 Headless Service 当作普通 ClusterIP 排查,会走错方向。

14.2 第二步:确认 EndpointSlice

kubectl get endpointslice \
  -l kubernetes.io/service-name=echo \
  -o yaml

检查:

  • 是否存在 EndpointSlice;
  • 地址是否属于预期 Pod;
  • 端口是否为实际监听端口;
  • ready 是否为 true
  • 是否因为 selector 不匹配而没有后端;
  • 是否存在多个地址族导致 IPv4/IPv6 访问不一致。

快速查看 Pod:

kubectl get pod -l app=echo -o wide
kubectl describe pod -l app=echo

如果 EndpointSlice 为空,先不要检查 iptables。kube-proxy 没有正确后端可编程,规则缺失通常只是结果。

14.3 第三步:区分 DNS 问题与 Service 转发问题

从客户端 Pod 中执行:

nslookup echo.default.svc.cluster.local
curl -v --connect-timeout 3 http://echo.default.svc.cluster.local/

再直接访问 ClusterIP:

kubectl get svc echo -o wide
curl -v --connect-timeout 3 http://<ClusterIP>/

解释方式:

  • DNS 名称失败、ClusterIP 访问成功:优先检查 CoreDNS、搜索域和 DNS Policy。
  • DNS 名称成功、ClusterIP 连接超时:优先检查 kube-proxy、CNI、路由、防火墙和 conntrack。
  • ClusterIP 连接被拒绝:可能已到达 Pod,但应用未监听目标端口。
  • Service 访问失败、直接 Pod IP 成功:Service 规则、端口、Endpoint 或转发路径可疑。
  • 直接 Pod IP 也失败:问题可能在应用、Pod 网络、NetworkPolicy 或 CNI。

直接测试 Endpoint:

kubectl get endpointslice \
  -l kubernetes.io/service-name=echo \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}'

然后从客户端访问:

curl -v http://10.244.2.7:5678/

不要把“Pod IP 可访问”简单推导为“Service 必然正常”。Service 还涉及虚拟地址匹配、端口转换、后端选择和可能的 SNAT。

14.4 第四步:确认 kube-proxy 模式和日志

kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
kubectl -n kube-system logs ds/kube-proxy --tail=200
kubectl -n kube-system get configmap kube-proxy -o yaml

注意:

  • kube-proxy DaemonSet 是否在故障节点运行;
  • 容器是否反复重启;
  • 是否无法连接 API Server;
  • 是否报告内核模块、iptables、IPVS 或 nftables 错误;
  • 配置中的 mode 是什么;
  • 实际节点是否使用了不同配置。

一个常见误区是只查看控制面上的 kube-proxy 日志。Service 转发发生在客户端所在节点或 NodePort 接收流量的节点,因此应查看对应节点上的 kube-proxy。

14.5 第五步:按模式查看内核状态

iptables:

sudo iptables-save -t nat | grep -E 'KUBE|10\.96\.10\.20'
sudo iptables -t nat -L -n -v

IPVS:

sudo ipvsadm -Ln
sudo ipvsadm -Ln --stats

nftables:

sudo nft list ruleset

检查某个 ClusterIP 是否出现时,要同时核对协议和端口。下面两个 Service 即使 ClusterIP 不同,也可能因为端口错误导致完全不同的结果:

10.96.10.20:80/TCP
10.96.10.20:8080/TCP

此外,双栈环境必须分别检查 IPv4 和 IPv6。一个地址族工作而另一个失败,不能用“Service 正常”概括。

14.6 第六步:观察 conntrack 和抓包

查看连接跟踪表:

sudo conntrack -L | grep '10.96.10.20'

高连接数场景检查计数:

sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

如果 nf_conntrack_count 接近 nf_conntrack_max,新连接可能出现丢包、超时或 NAT 失败。扩大表容量不能替代分析连接泄漏和异常长连接,否则只是延后故障。

抓包时应选择正确接口。Pod 流量可能经过:

  • Pod veth;
  • Linux bridge;
  • CNI 隧道;
  • 节点物理网卡;
  • 云厂商虚拟网卡。

例如:

sudo tcpdump -ni any \
  'host 10.96.10.20 or host 10.244.2.7'

观察顺序:

  1. 客户端是否发出 SYN。
  2. Service 虚拟 IP 的 SYN 是否出现在节点。
  3. 是否看到目的地址改为 Endpoint IP。
  4. 后端是否返回 SYN-ACK。
  5. 返回包是否被正确还原并到达客户端。
  6. 是否发生重传、RST 或 ICMP 错误。

抓包显示的地址可能受抓包位置影响。不同 netfilter hook、veth 两端和隧道接口看到的 NAT 前后地址不一样,不能把某一个接口上的现象直接当成全链路事实。


15. 常见失败表现与对应推理

15.1 Service 没有 Endpoint

表现:

kubectl get endpointslice \
  -l kubernetes.io/service-name=echo

没有对象,或对象中没有地址。

优先推理:

  • Service selector 写错;
  • Pod 标签不匹配;
  • Pod 未 Ready;
  • 端口定义错误;
  • EndpointSlice 控制器异常;
  • 命名空间不一致。

这不是先切换 IPVS 或 nftables 就能解决的问题。

15.2 Endpoint 存在,但访问超时

可能路径:

ClusterIP
  -> kube-proxy 规则
  -> DNAT
  -> CNI 路由
  -> Pod
  -> 返回路径

应依次验证:

curl http://<PodIP>:<targetPort>
curl http://<ClusterIP>:<port>
ip route
conntrack -L
tcpdump

如果 Pod IP 直连成功而 ClusterIP 超时,重点看 Service 规则和 conntrack;如果两者都失败,重点看 Pod 网络和应用。

15.3 访问被拒绝

connection refused 往往说明某个网络实体主动返回了 RST,常见原因是:

  • Pod 内没有监听目标端口;
  • targetPort 错误;
  • NodePort 的节点地址上没有对应监听路径;
  • 主机或中间设备主动拒绝。

但不能仅凭错误字符串定位,需要结合抓包确定 RST 来源。

15.4 修改 Endpoint 后仍访问旧 Pod

可能原因:

  • 客户端复用了已有 TCP 连接;
  • conntrack 仍保存旧 NAT;
  • IPVS 仍有连接状态;
  • kube-proxy 尚未完成同步;
  • 应用或代理缓存了旧地址。

可比较:

kubectl get endpointslice \
  -l kubernetes.io/service-name=echo -o wide

sudo iptables-save -t nat
sudo ipvsadm -Ln
sudo conntrack -L

不要为了验证而随意清空全机 conntrack。清理连接跟踪会中断大量业务连接,必须限定范围、评估影响并准备恢复方案。

15.5 externalTrafficPolicy: Local 后部分节点失败

这是预期风险之一。假设:

节点 N1:没有后端 Pod
节点 N2:有后端 Pod

外部负载均衡器把流量发送到 N1,而 Service 使用 Local,N1 没有可用本地 Endpoint,那么 N1 不能像 Cluster 模式那样转发到 N2。

应检查:

kubectl get pod -l app=echo -o wide
kubectl get svc echo -o yaml

同时验证外部负载均衡器的健康检查是否能把 N1 排除。仅仅修改 Service 字段而不验证健康检查,可能把“源 IP 保留”换成“部分节点黑洞”。


16. 模式选择与生产取舍

16.1 iptables

适合:

  • 节点规模和 Service 数量中等;
  • 团队已有成熟 iptables 排障能力;
  • 发行版兼容性优先;
  • 不希望引入较新的 kube-proxy 模式。

风险主要是规则规模和更新成本,而不是基本语义不同。

16.2 IPVS

适合性不能只根据历史经验判断。它需要额外内核能力,且 Kubernetes 对其长期维护方向已经发生变化。新集群是否采用,应以当前 Kubernetes 版本官方状态和发行版支持矩阵为依据,而不是照搬旧教程。

16.3 nftables

在支持的 Kubernetes 版本、内核和发行版上,nftables 是值得评估的新路径,尤其是 Service 数量较大、希望使用更现代规则表达的场景。

迁移前必须进行真实流量验证,而不是只执行:

kubectl get pods

至少应测试:

  • ClusterIP;
  • NodePort;
  • LoadBalancer;
  • UDP Service;
  • 双栈;
  • SessionAffinity;
  • externalTrafficPolicy: Local
  • Pod 删除和滚动更新;
  • 节点重启;
  • kube-proxy 重启;
  • conntrack 接近上限时的行为。

性能数字不能脱离版本、内核、CNI 和工作负载直接引用。模式切换后应通过连接建立速率、规则同步耗时、CPU、丢包、P99 延迟和故障恢复时间进行基准测试。


17. 需要明确区分的规范、实现与经验

Kubernetes API 语义

以下属于 Service API 的语义范围:

  • ClusterIPNodePortLoadBalancer 等类型;
  • porttargetPort 的关系;
  • sessionAffinity: ClientIP
  • externalTrafficPolicy
  • internalTrafficPolicy
  • EndpointSlice 对后端状态的表达。

但 API 语义通常不会保证某个具体 iptables 链名、IPVS 调度实现细节或 nftables 表结构。

常见实现

以下是常见实现,不应当当作永远不变的接口:

  • kube-proxy 使用 KUBE-SVC-*KUBE-SEP-* 链;
  • IPVS 通过 ipvsadm 查看虚拟服务;
  • kube-proxy 在 nat 表中处理 SNAT;
  • 新连接根据当前 Endpoint 集合选择后端;
  • 已建立连接受 conntrack 或 IPVS 状态影响。

运维建议

以下属于生产经验,而不是 Kubernetes API 保证:

  • 规则规模很大时评估 nftables;
  • 对 SessionAffinity 做容量和故障测试;
  • 使用 Local 前验证本地 Endpoint 与健康检查;
  • 不直接 flush 节点上的防火墙规则;
  • 不用“规则存在”替代端到端验证;
  • 模式切换必须有回滚方案。

18. 最小诊断路径

当一个 Service 访问失败时,可以按下面顺序执行:

# 1. Service
kubectl get svc <service> -n <namespace> -o yaml

# 2. EndpointSlice
kubectl get endpointslice -n <namespace> \
  -l kubernetes.io/service-name=<service> -o wide

# 3. Pod 与应用端口
kubectl get pod -n <namespace> -l <selector>
kubectl exec -n <namespace> <pod> -- ss -lntup

# 4. 客户端内测试 DNS 和 Service
kubectl exec -n <namespace> <client-pod> -- \
  getent hosts <service>.<namespace>.svc.cluster.local

kubectl exec -n <namespace> <client-pod> -- \
  curl -v --connect-timeout 3 \
  http://<service>.<namespace>.svc.cluster.local:<port>/

# 5. 节点侧确认 kube-proxy
kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
kubectl -n kube-system logs <kube-proxy-pod> --tail=200

# 6. 按实际模式查看内核状态
sudo iptables-save -t nat
sudo ipvsadm -Ln
sudo nft list ruleset

# 7. 必要时检查路由、conntrack 和抓包
ip route
sudo conntrack -L
sudo tcpdump -ni any 'host <cluster-ip> or host <pod-ip>'

这条路径的核心是保持因果顺序:

对象是否正确
  -> 后端是否存在
  -> 应用是否监听
  -> kube-proxy 是否同步
  -> 内核是否有对应状态
  -> 数据包是否按预期经过各接口
  -> 返回路径和连接跟踪是否正常

只检查 kube-proxy 日志,无法排除 DNS、CNI、MTU、NetworkPolicy 和应用端口问题;只检查 EndpointSlice,也无法证明节点上的内核转发状态已经更新。

Service 转发的本质是:控制器对象描述意图,kube-proxy 将意图编译为节点状态,Linux 内核根据规则、路由和连接跟踪处理数据包。iptables、IPVS 和 nftables 的主要差异在于“如何表达和执行这组四层转发状态”,而不是改变 Service 的基本 API 语义。理解这一点后,Session、源地址、长连接、NodePort、LoadBalancer 和故障诊断就可以放回同一条数据路径中分析,而不必把每种现象当成彼此独立的功能。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。