Service 的 ClusterIP 是一个不存在于任何网卡的虚拟 IP。在节点上运行 ip addr,看不到它;用 ping 测试,收不到回应;但对它发起 TCP 连接,数据却能稳定到达某个后端 Pod。这个"幽灵 IP"能够工作,完全依赖 kube-proxy 写入每个节点内核的 iptables 或 IPVS 规则——规则在,转发在;规则消失,连接消失。

理解这一机制的起点是区分两种截然不同的角色:kube-proxy 是"规则维护者",内核的 netfilter/IPVS 子系统才是"数据面执行者"。kube-proxy 进程本身从不处理任何用户请求,它只监听 kube-apiserver 上 Service 和 EndpointSlice 的变化,把这些变化翻译成内核规则,然后等待下一次变化。真正处理数据包的是内核——每个到达某 ClusterIP:port 的数据包,在经过 iptables nat 表的 PREROUTING 链时,被 DNAT 规则改写目标地址,转向某个真实的 Pod IP:port,整个过程对应用完全透明。

这种设计的优雅之处在于分层解耦:控制面(kube-proxy watch + 规则同步)与数据面(内核包转发)完全解耦。数据面没有单点,没有用户态瓶颈,每个节点独立维护自己的转发规则;kube-proxy 进程崩溃后,已有规则继续生效,已建立的 TCP 连接不受影响,只有新的 Service/Pod 变更不再被同步。代价是规则同步本身的复杂性,以及 iptables 在大规模下的线性遍历开销,这正是 IPVS 模式和 Cilium eBPF 替代方案要解决的问题。

转发路径全景

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
Pod AClusterIP:80 (e.g. 10.96.0.10:80)


内核 netfilter PREROUTING
├─ KUBE-SERVICES 链 (匹配 ClusterIP:port)
│ └─ KUBE-SVC-XXXXXXXX 链 (该 Service 的规则链)
│ ├─ statistic mode random prob 0.333 → KUBE-SEP-P1
│ │ DNAT → 10.244.1.2:80
│ ├─ statistic mode random prob 0.500 → KUBE-SEP-P2
│ │ DNAT → 10.244.1.3:80
│ └─ (remaining) → KUBE-SEP-P3
│ DNAT → 10.244.2.4:80

▼ (DNAT 完成,目标 IP 已改写为真实 Pod IP)
路由决策 → 本地或跨节点发送


目标 Pod (真实 Pod IP:port)

IPVS 模式对比:
ip_vs virtual server: 10.96.0.10:80
→ hash table 查找 O(1)
→ real server 选择 (rr/lc/wrr/sh)
→ 包转发(内核 ip_vs 模块)

kube-proxy 控制循环:
Service 变化 → 创建/删除 KUBE-SVC 链 / IPVS virtual server
EndpointSlice 变化 → 增删 KUBE-SEP 规则 / IPVS real server
readinessProbe 失败 → EndpointSlice ready:false → 规则摘除 → 新连接不再路由到该 Pod

Service 的本质:稳定的虚拟入口

Service 是 Kubernetes 对"服务发现 + 负载均衡"这个经典问题的声明式解答。Pod IP 不稳定(重建即变),一组 Pod 需要负载均衡访问,Service 同时解决这两个问题:用一个固定的 ClusterIP 作为稳定的访问入口,用 kube-proxy 维护的内核规则把流量分发到当前健康的 Pod 集合。

ClusterIP 由 kube-apiserver 在 Service 创建时从 --service-cluster-ip-range(通常是 10.96.0.0/12)中分配,写入 etcd 的 Service 对象。只要 Service 不被删除,这个 IP 就不会变更——即使后端 Pod 全部重建、迁移到不同节点,ClusterIP 保持不变,调用方的配置无需更新。这正是 Service 提供的核心抽象价值:把不稳定的 Pod 集合封装在一个稳定的虚拟地址后面。

ClusterIP 没有绑定到任何网络接口,它不是一个真实存在的 IP 地址,而是 iptables/IPVS 规则里的一个匹配键。ping 之所以不通,是因为 ICMP echo 不经过 iptables nat 表的 DNAT 规则(DNAT 只处理 TCP/UDP 的有连接协议),ping 包到达"ClusterIP"这个地址时,没有任何接口应答,包被内核丢弃。curl 发起 TCP SYN 包,经过 PREROUTING 链,被 DNAT 改写到真实 Pod IP,SYN 包成功到达 Pod,连接建立。这是最常见的"Service 可用但不可 ping"的根本原因,理解这一点可以避免很多无谓的网络排查。

ClusterIP 的地址范围不能与 Pod 网络(PodCIDR)或节点网络重叠,三个地址空间必须互相独立。规划集群网络时,需要提前确定这三个 CIDR 范围:节点 IP 段(物理网络决定)、PodCIDR(由 --pod-cidr 配置)、ServiceCIDR(由 --service-cluster-ip-range 配置)。一个常见的规划方案是节点用 192.168.0.0/16,PodCIDR 用 10.244.0.0/16,ServiceCIDR 用 10.96.0.0/12,三者完全不重叠。

Service 的五种类型

ClusterIP 是所有 Service 类型的基础,其他类型都是在 ClusterIP 之上叠加额外能力。

ClusterIP 分配集群内部虚拟 IP,只在集群内可访问。这是绝大多数内部服务(数据库、缓存、内部 API)的标准形态。外部流量无法直接访问 ClusterIP,需要通过 Ingress 或 LoadBalancer 转发进来。可以在 Service 的 spec 里通过 sessionAffinity: ClientIP 开启基于客户端 IP 的会话亲和性——kube-proxy 会在 iptables 里写入 recent 模块规则(或 IPVS 的 sh 调度算法),让同一客户端 IP 的请求始终路由到同一后端 Pod。会话亲和性的有效期由 sessionAffinityConfig.clientIP.timeoutSeconds 控制,默认 10800 秒(3小时)。

NodePort 在每个节点上开放一个固定端口(默认范围 30000-32767,由 --service-node-port-range 配置),外部请求打到任意节点 IP:NodePort,被 iptables 规则 DNAT 到 ClusterIP,再路由到 Pod。NodePort 包含 ClusterIP 的全部能力,集群内部仍然可以用 ClusterIP 访问。

外部访问时,externalTrafficPolicy 的取值决定了流量行为。externalTrafficPolicy: Cluster(默认值):即使请求打到一个没有对应 Pod 的节点,规则也会把流量转发到有 Pod 的节点(跨节点转发),但这个过程需要 SNAT——因为跨节点转发后,如果目标 Pod 直接回包给原始客户端 IP,回包不会经过入口节点,连接无法建立(非对称路由)。SNAT 解决了路由对称性,但掩盖了真实客户端 IP。externalTrafficPolicy: Local:只有本节点有 Pod 时才接受并处理请求,没有 Pod 的节点会直接丢弃该端口的流量;因为流量不需要跨节点转发,不需要 SNAT,Pod 看到的来源 IP 就是真实客户端 IP。代价是流量分布不均——如果某个节点有 3 个 Pod,某个节点有 1 个 Pod,前者实际承受的是后者的 3 倍流量。

LoadBalancer 在 NodePort 之上,向云厂商申请一个外部负载均衡器(AWS NLB/ALB、GCP Cloud LB、Azure LB 等),把集群节点的 NodePort 注册为后端,外部 IP 绑定到 Service 的 status.loadBalancer.ingress。这个过程由 cloud-controller-manager 中的 cloud provider 实现完成——它 watch LoadBalancer 类型的 Service,调用云 API 创建 LB,把节点 IP:NodePort 注册为健康检查目标。云 LB 的健康检查探测 NodePort,只有健康的节点才会接收来自云 LB 的流量。在裸金属集群上没有云厂商支持时,可以用 MetalLB 或 Cilium 的 LB-IPAM 提供类似能力:MetalLB 通过 ARP(L2 模式)或 BGP(L3 模式)把 Service 的外部 IP 通告给网络,实现外部流量的接入。

ExternalName 不分配 ClusterIP,也不使用 kube-proxy 规则。CoreDNS 把该 Service 的 DNS 查询做 CNAME 解析到 spec.externalName 指定的外部域名。应用代码访问 db.default.svc.cluster.local,CoreDNS 返回一个 CNAME 指向 my-rds-instance.xxxxx.rds.amazonaws.com,应用实际连接的是 RDS 实例。迁移时只需修改 Service 的 externalName 字段,不需要改应用代码。ExternalName 的使用场景是把集群外部的服务(RDS、ElastiCache、第三方 API 端点)包装成集群内的 Service 名,统一应用的寻址方式,方便在 staging 和 production 环境切换不同的外部服务,而应用代码始终用同一个 Service DNS 名。

Headless Service(clusterIP: None)不分配 ClusterIP,CoreDNS 直接返回所有就绪 Pod 的 IP 列表(多条 A 记录),不经过任何 DNAT。StatefulSet 的 Pod 通过 Headless Service 实现 Pod 级别的 DNS 名(web-0.web.default.svc.cluster.local → Pod web-0 的 IP),使有状态应用可以直接寻址到特定实例,是 Kafka broker 相互通信、MySQL replica 配置主从、Elasticsearch 集群自发现的标准解决方案。Headless Service 在创建时不会触发 kube-proxy 写任何 iptables 规则,因为它不需要负载均衡转发。

Endpoints 与 EndpointSlice

Endpoints 是 Service 后端的第一代表示,每个 Service 对应一个同名的 Endpoints 对象,包含所有就绪后端 Pod 的 IP 和端口列表。endpoints-controller(kube-controller-manager 的一部分)持续监控 Pod 的就绪状态:readinessProbe 通过的 Pod 被加入 Endpoints,readinessProbe 失败的 Pod 被从 Endpoints 移除。kube-proxy watch 到 Endpoints 变化后立即更新内核规则,新连接就不会再路由到不健康的 Pod。

Endpoints 在大规模集群下暴露了一个严重的扩展性问题:单个 Endpoints 对象包含所有后端 Pod,任何一个 Pod 变化(哪怕是后端有 1000 个 Pod 的 Service 里只有 1 个 Pod 重启),整个 Endpoints 对象都需要全量更新、序列化、写入 etcd、推送给所有节点的 kube-proxy。在 1000 个节点、每个节点都有 kube-proxy 的集群里,一次 Endpoints 更新可能产生 1000 次全量对象推送,对 apiserver、etcd、网络带宽都是压力。

EndpointSlice(GA 自 Kubernetes 1.21)把后端 Pod 分片存储,每片默认最多 100 个端点(由 maxEndpointsPerSlice 控制)。当 1 个 Pod 在 1000 个后端里发生变化,只有包含它的那一个 Slice 对象需要更新,其他 Slice 不受影响,kube-proxy 的 watch 事件量从 O(total_pods) 降到 O(changed_slice_size)。更少的数据传输、更低的 etcd 写压力、更快的规则同步速度,在后端 Pod 数量大时效果非常显著。kube-proxy 1.19 版本开始默认使用 EndpointSlice,1.21 版本 EndpointSlice GA 后成为生产推荐。

EndpointSlice 的数据结构比 Endpoints 更丰富。每个端点(endpoint)字段包含:addresses(Pod IP 列表,通常是一个)、conditionsreadyservingterminating 三个布尔值)、targetRef(对应的 Pod 对象引用)、nodeName(Pod 所在节点)、zone(可用区)。conditions.servingconditions.terminating 的引入解决了 Pod 优雅退出期间的流量处理问题——Pod 进入 Terminating 状态后,terminating: true,kube-proxy 可以根据这个字段决定是否继续向该 Pod 转发(配合 preStop hook 等优雅退出机制)。

EndpointSlice 还引入了 topology 字段支持拓扑感知路由(Topology Aware Routing,1.27 GA):kube-proxy 优先把流量路由到与请求方在同一可用区的后端 Pod,减少跨区流量,降低延迟和跨区数据传输费用。这个能力在多 AZ 部署的云环境里价值显著,Endpoints 对象没有这个字段。拓扑感知路由通过在 Service 上设置 spec.trafficDistribution: PreferClose 启用(1.31 GA),kube-proxy 根据 EndpointSlice 里的 zone 字段和节点的 zone 标签,优先选择同 zone 的端点。

kube-proxy iptables 模式详解

iptables 模式在 kube-proxy 启动时(或 Service/EndpointSlice 变化时)把 Service 的转发规则写入 Linux netfilter 的 nat 表。规则结构采用链式组织,有几个关键的自定义链:

KUBE-SERVICES 链是所有 Service 规则的入口,在系统的 PREROUTING 和 OUTPUT 链里都有跳转到 KUBE-SERVICES 的规则(前者处理从外部进来的包,后者处理本地发出的包)。每个 Service 在这里有一条跳转规则,匹配条件是目标 IP(ClusterIP)和端口,动作是跳转到该 Service 专属的 KUBE-SVC-XXXXXXXX 链(XXXXXXXX 是 Service ClusterIP+port 的哈希)。KUBE-SERVICES 链还在末尾有一个 KUBE-NODEPORTS 规则,处理 NodePort 流量。

KUBE-SVC-XXXXXXXX 链是某个 Service 的负载均衡规则链,包含 N 条规则(N 是就绪后端 Pod 数量)。规则使用 iptables 的 statistic 模块做概率分配:第 1 条规则匹配概率是 1/N,第 2 条是 1/(N-1),以此类推,最后一条是 1(兜底)。这种链式概率设计使每个 Pod 被选中的期望概率相等(均为 1/N)——虽然每条规则的写法看起来不同,但组合起来是均匀分布。以 3 个后端为例:第一条规则有 1/3 的概率命中并跳转到 KUBE-SEP-P1;不命中(2/3 概率)的流量进入第二条规则,第二条有 1/2 的概率命中 KUBE-SEP-P2(即总概率 2/3 × 1/2 = 1/3);剩余流量(1/3)全部走 KUBE-SEP-P3。三个 Pod 期望被选中概率均等。

KUBE-SEP-YYYYYYYY 链(SEP = Service EndPoint)对应一个具体的后端 Pod,包含一条 DNAT 规则把 ClusterIP:port 改写为 Pod IP:port,以及一条 MASQUERADE 规则处理 hairpin 场景(Pod 访问自身所属 Service 的 ClusterIP 时,需要 MASQUERADE 确保回包能正确路由)。MASQUERADE 是 SNAT 的动态版本,自动把源 IP 改写为出口网卡的 IP,不需要指定具体的 SNAT 地址。

iptables 模式的两个已知扩展性瓶颈值得理解清楚。第一,规则遍历是线性的:每个新 TCP 连接的 SYN 包(以及每个无连接 UDP 包)都从 KUBE-SERVICES 链头开始遍历,匹配到对应 Service 的链;Service 数量增加意味着平均遍历长度增加,10 个 Service 时每次遍历约 10 条规则,1000 个 Service 时平均约 500 条规则。第二,规则同步是全量的:kube-proxy 调用 iptables-restore 写入整个规则集,任何 Service 或 Pod 变化都触发这个全量操作;在 Service 数量多时,一次同步可能需要锁住 iptables 锁(/run/xtables.lock)数秒,期间其他 iptables 操作会被阻塞。在 5000 个 Service 的集群里,一次 iptables-restore 可能写入数万条规则,耗时 10 秒以上,这期间内核态的 iptables 锁被持有,任何需要修改 iptables 的操作都会排队等待。

已建立的 TCP 连接不受 iptables 规则变化影响,因为 conntrack(连接追踪)模块缓存了第一个 SYN 包的 DNAT 结果,后续同一连接的包直接命中 conntrack 缓存,跳过整个 iptables 链路。这个特性既是性能优化(避免每个包都遍历规则链),也是会话一致性的保证(同一连接的所有包到达同一后端 Pod)。

kube-proxy IPVS 模式详解

IPVS(IP Virtual Server)是 Linux 内核的四层负载均衡模块,由章文嵩博士主导开发,是 LVS(Linux Virtual Server)项目的核心组件。kube-proxy 的 IPVS 模式把 Service 映射为 IPVS 虚拟服务器(virtual server),把每个后端 Pod 注册为对应的真实服务器(real server),由内核的 ip_vs 模块完成包转发。

IPVS 用哈希表存储 virtual server 和 real server 的映射,规则查找时间 O(1),不随 Service 数量增长。规则同步是增量的:Service 变化时用 netlink 接口(ipvs.UpdateService)只更新对应的 virtual server,不触及其他 Service 的规则;EndpointSlice 变化时只增减对应的 real server。这两点使 IPVS 在 Service 数量超过 1000 时性能明显优于 iptables。

IPVS 支持多种负载均衡调度算法,通过 kube-proxy 的 --ipvs-scheduler 参数或 KubeProxyConfiguration ConfigMap 配置:

  • rr(round robin):轮询,逐个分配,默认值,适合后端处理能力相近的场景
  • lc(least connection):最少活跃连接优先,把新连接路由到当前活跃连接数最少的后端,适合长连接或处理时间差异大的场景
  • wrr(weighted round robin):加权轮询,可以给不同 Pod 设置不同权重,适合 Pod 配置不均等的场景
  • dh(destination hashing):目标地址哈希,相同目标 IP 的请求路由到同一后端,适合反向代理缓存场景
  • sh(source hashing):源地址哈希,相同源 IP 的请求路由到同一后端,提供会话亲和性(Session Affinity)

Kubernetes 的 Service 里设置 sessionAffinity: ClientIP 时,kube-proxy IPVS 模式会自动选择 sh 调度算法,使同一客户端 IP 的请求始终被路由到同一后端 Pod,比 iptables 模式的 recent 模块实现更高效。

IPVS 模式还需要节点上预装对应的内核模块(ip_vsip_vs_rrip_vs_wrrip_vs_sh)以及 nf_conntrack 模块用于连接追踪。在现代 Linux 发行版(CentOS 7.4+、Ubuntu 16.04+)上,这些模块通常已经包含在内核里,只需要 modprobe ip_vs 加载即可。kube-proxy 在启动时会检查这些模块是否存在,如果缺失则回退到 iptables 模式并打出警告日志。

IPVS 模式要求 ClusterIP 绑定到一个虚拟网络接口(通常是 kube-ipvs0,一个 dummy 接口),这样内核才能接受发往 ClusterIP 的流量而不丢包。kube-proxy 在 IPVS 模式启动时会创建这个 dummy 接口,并把所有 Service 的 ClusterIP 以 /32 地址的形式 assign 到这个接口上。

值得注意的是:IPVS 模式仍然使用少量 iptables 规则处理 IPVS 本身覆盖不到的场景(如 NodePort 流量的 MASQUERADE、hairpin 流量的 SNAT、ClusterIP 的 localhost 访问)。切换到 IPVS 模式并不意味着完全不用 iptables,但 iptables 规则数量从数百/数千条降到数十条,开销大幅减少。

Cilium kube-proxy 完全替代

Cilium 提供了一种彻底不同的 Service 实现路径:用 eBPF 程序在 socket 层(connect() 系统调用时)拦截流量,在内核数据路径中直接完成 Service 到 Pod IP 的映射,完全绕过 iptables 和 netfilter,也不需要 conntrack 做连接追踪。

iptables/IPVS 在 IP 包转发层(L3/L4)拦截:每个数据包,包括同一 TCP 连接的第 N 个包,都经过 netfilter 处理(虽然 conntrack 缓存使已建立连接的处理很快);新建连接的 SYN 包需要经过完整的规则匹配。Cilium 的 socket 级负载均衡在 connect() 系统调用时拦截:应用调用 connect(ClusterIP, port) 时,eBPF 程序把目标 IP 改写为某个 Pod IP,后续该连接的所有包直接发往 Pod IP,无需任何 netfilter 处理。把每个连接的 DNAT 开销从"每个包一次 conntrack 查找"降到"建立连接时一次 eBPF Map 查找"。

socket 级负载均衡的实现利用了 BPF_CGROUP_INET4_CONNECT 这个 cgroup eBPF 钩子。内核在执行 connect() 系统调用时,会触发这个钩子,Cilium 挂载的 eBPF 程序读取目标地址,在 Service Map 里查找对应的后端 Pod IP,并在内核里修改 sockaddr 结构。整个过程对用户态应用完全透明,getsockname() 仍然返回 ClusterIP,但实际的内核 socket 已经连接到 Pod IP。

在每秒数万次新建连接的高并发短连接场景(如微服务的 HTTP/1.1 短连接、HTTP/2 多路复用下的大量流),Cilium 的 socket 级负载均衡延迟优势最为显著。conntrack 表的锁竞争在高并发下会成为瓶颈,Cilium 绕过 conntrack 的设计从根本上避免了这个问题。但 Cilium kube-proxy 替代需要 Linux 5.4+ 内核(socket 级 DNAT 需要 BPF_CGROUP_INET4_CONNECT 钩子),且 Cilium 自身的运维复杂度远高于 kube-proxy。

DNS 服务发现:CoreDNS 的角色

Service 的 IP 固定,但调用方如何知道这个 IP?硬编码 IP 违背了 Service 的设计意图(如果 Service 被重建,IP 可能变化),也不利于跨环境部署(不同集群的 ClusterIP 不同)。CoreDNS 提供了名称到 IP 的稳定映射。

CoreDNS 以 Deployment 形式部署在 kube-system 命名空间,由 --dns-service-ip 参数(通常是 10.96.0.10)指定其 Service ClusterIP。每个 Service 自动获得一条 DNS A 记录:{service-name}.{namespace}.svc.cluster.local → ClusterIP。同命名空间内的调用可以省略命名空间和域后缀,直接用服务名访问(curl http://nginx,CoreDNS 的 search 域自动补全)。

Pod 的 /etc/resolv.conf 由 kubelet 在创建时注入,nameserver 字段指向 CoreDNS 的 ClusterIP,search 字段列出搜索域(default.svc.cluster.local svc.cluster.local cluster.local),options ndots:5 控制短域名查询的搜索行为。

ndots:5 的语义:如果查询的域名里点的数量少于 5 个,先依次在 search 域列表里重试,全部失败后才尝试根域名。github.com 只有 1 个点,少于 5,所以会先尝试 github.com.default.svc.cluster.local(失败)→ github.com.svc.cluster.local(失败)→ github.com.cluster.local(失败)→ 再尝试根域名 github.com.。这个机制在访问公网域名时产生额外的 DNS 查询开销(通常是 3 次额外查询),在 DNS 密集场景(每次 HTTP 请求前都做 DNS 解析)里可以用 ndots:2 或在域名末尾加 .(强制根域名查询)来优化。

CoreDNS 的 SRV 记录也是服务发现的一部分,格式是 _port-name._proto.{service-name}.{namespace}.svc.cluster.local,包含 Service 的端口号和协议类型。部分服务发现框架(如 etcd 的成员发现)依赖 SRV 记录获取端口信息,不需要客户端提前知道端口号。

Headless Service 的 DNS 行为不同:CoreDNS 直接返回 Pod IP 列表(多条 A 记录),客户端可以用 DNS 轮询或自定义负载均衡。StatefulSet 的 Pod 还额外获得 {pod-name}.{headless-svc}.{namespace}.svc.cluster.local 格式的 DNS 记录,指向该 Pod 的具体 IP,是有状态集群内部通信的标准寻址方式。

CoreDNS 本身是一个插件化的 DNS 服务器,核心功能由 Corefile 配置文件里的插件声明。Kubernetes 集群里常用的插件有:kubernetes(从 kube-apiserver 同步 Service/Pod 的 DNS 记录)、forward(把非集群域名的查询转发给上游 DNS)、cache(DNS 响应缓存,默认 30 秒 TTL)、health(健康检查端点)。CoreDNS 通过 watch apiserver 的 Service 和 Pod 对象实时更新 DNS 记录,Service 创建后几秒内 DNS 就可以解析,删除后同样快速失效。

可运行实验

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
# 创建测试用的 Deployment 和 ClusterIP Service
kubectl create deployment nginx --image=nginx --replicas=3
kubectl expose deployment nginx --port=80 --type=ClusterIP

# 查看 Service 的 ClusterIP 和 Endpoints
kubectl get svc nginx
kubectl get endpoints nginx
kubectl get endpointslices -l kubernetes.io/service-name=nginx

# 获取 ClusterIP
CLUSTER_IP=$(kubectl get svc nginx -o jsonpath='{.spec.clusterIP}')
echo "ClusterIP: $CLUSTER_IP"

# 验证 ClusterIP 不可 ping(icmp 不走 DNAT 规则)
docker exec kind-control-plane ping -c 2 "$CLUSTER_IP" 2>&1 || echo "ping 失败(预期行为)"

# 但 TCP 连接可以成功
kubectl run curl-test --image=curlimages/curl --restart=Never --rm -it -- \
curl -s "http://${CLUSTER_IP}/" | head -3

# 观察 iptables KUBE-SERVICES 链(找到 nginx Service 的规则)
docker exec kind-control-plane iptables -t nat -L KUBE-SERVICES -n --line-numbers | \
grep -i nginx

# 查看 KUBE-SVC 链内的概率分配规则(3 个后端,概率 0.333 / 0.500 / 1.0)
SVC_CHAIN=$(docker exec kind-control-plane iptables -t nat -S | \
grep "KUBE-SERVICES" | grep "$CLUSTER_IP" | grep -o "KUBE-SVC-[A-Z0-9]*")
echo "Service chain: $SVC_CHAIN"
docker exec kind-control-plane iptables -t nat -L "$SVC_CHAIN" -n 2>/dev/null

# 观察 conntrack 表(TCP 连接建立后的 DNAT 记录)
docker exec kind-control-plane conntrack -L -p tcp 2>/dev/null | grep "$CLUSTER_IP" | head -5

# DNS 解析验证
kubectl run dnstest --image=busybox --restart=Never --rm -it -- \
nslookup nginx.default.svc.cluster.local

# 查看 resolv.conf 注入
kubectl run dnstest2 --image=busybox --restart=Never --rm -it -- \
cat /etc/resolv.conf

# 验证 readinessProbe 联动:scale down 后观察 Endpoints 变化
kubectl scale deployment nginx --replicas=1
kubectl get endpoints nginx # 应该只有 1 个端点

# NodePort 测试
kubectl patch svc nginx -p '{"spec":{"type":"NodePort"}}'
NODE_PORT=$(kubectl get svc nginx -o jsonpath='{.spec.ports[0].nodePort}')
echo "NodePort: $NODE_PORT"
curl -s "http://localhost:${NODE_PORT}/" | head -3

# 清理
kubectl delete deployment nginx
kubectl delete svc nginx

实验结果映射

iptables -t nat -L KUBE-SERVICES -n 里,每个 Service 对应一条匹配规则,目标是 ClusterIP:port,动作是跳转到 KUBE-SVC-XXX 链。Service 越多,这条链越长,遍历开销越大。100 个 Service 时平均需要遍历约 50 条规则,1000 个 Service 时约 500 条,这是 iptables 模式线性开销的直接体现。

KUBE-SVC-XXX 链里看到的 statistic mode random probability 0.33333... 是 iptables 概率分配的直接体现。三个 Pod 对应三条规则,概率依次是 1/3、1/2(剩余两个里选一个)、1(兜底)。把副本数改为 5,可以观察到五条规则,概率依次是 0.2、0.25、0.333、0.5、1.0。理解这个"递减分母"设计是理解 iptables 负载均衡随机性的关键。

conntrack -L 显示内核的连接追踪表。对 ClusterIP 建立 TCP 连接后,conntrack 表里会有一条 DNAT 类型的记录,原始目标是 ClusterIP,DNAT 后目标是某个 Pod IP。同一连接的所有后续包通过 conntrack 命中缓存,直接发往 Pod IP,不再经过 KUBE-SVC 链的 statistic 规则——这保证了同一 TCP 连接的所有包始终到达同一个 Pod(连接级别的会话亲和性),而不会在连接中途被路由到不同 Pod。

EndpointSlice 的 conditions.ready: false 状态变化(Pod 缩容后触发)被 kube-proxy watch 到后,触发对应 KUBE-SEP 规则的删除。时序是:Pod 变为 Terminating → kubelet 停止 readinessProbe → controller 标记 EndpointSlice 端点为 ready: false → kube-proxy watch 到变化 → 删除 KUBE-SEP 规则。这个链路总延迟约为几秒到十几秒,期间新连接可能仍被路由到正在终止的 Pod。

模式提炼

Service 是"稳定虚拟入口 + 动态后端集合"的组合,三层各司其职:DNS(CoreDNS)提供名称到 ClusterIP 的映射,ClusterIP 提供稳定不变的虚拟地址,kube-proxy 维护的内核规则提供动态的流量分发。三层各自可以独立演进:DNS 层可以替换 CoreDNS 实现,规则层可以从 iptables 迁移到 IPVS 或 eBPF,后端管理层从 Endpoints 演进到 EndpointSlice,任何一层的变化不影响其他层。

kube-proxy 的核心价值不是"代理"(它不代理任何数据包),而是"规则同步器"——持续地把 Kubernetes 的声明式 Service 状态翻译成内核的命令式规则,是控制面与内核数据面之间的桥梁。这个角色模式在 Kubernetes 里反复出现:kubelet 是 Pod 规范到容器的转译器,cloud-controller-manager 是 Service/Node 状态到云资源的转译器,kube-proxy 是 Service/EndpointSlice 到内核规则的转译器。

iptables 还是 IPVS,不是功能差异,是扩展性差异:Service 数量小于几百时几乎无差别,超过 1000 时 IPVS 的 O(1) 查找和增量同步优势开始显现,超过 5000 时 iptables 同步延迟可能成为集群稳定性风险。生产集群规模到一定程度时,从 iptables 切换到 IPVS 是低风险、高收益的优化操作。

工程迁移表

传统技术 Kubernetes Service 对应物 差异说明
Nginx upstream 块 Service + EndpointSlice Nginx 手动配置,K8s 自动从 readinessProbe 维护端点列表
HAProxy backend Service + kube-proxy 规则 HAProxy 用户态代理,K8s 用内核 DNAT,无用户态数据路径开销
LVS 四层负载均衡 IPVS 模式 Service 技术完全相同,kube-proxy 把 K8s Service 模型翻译为 LVS 配置
DNS round-robin Headless Service 语义相近,K8s 额外支持 Pod 级 DNS 名(StatefulSet 用途)
手动 iptables DNAT iptables 模式 kube-proxy 手动规则无法自动感知后端变化,kube-proxy 实现声明式自动同步
Consul 服务注册与发现 Service + CoreDNS Consul 主动注册,K8s 由 controller 根据 Pod 就绪状态自动维护
VIP(F5 BigIP 虚拟服务器) ClusterIP 概念相同,K8s 的 VIP 在内核规则里,F5 的 VIP 在专用硬件上
主动健康检查 → 流量摘除 readinessProbe → EndpointSlice 机制完全对应,K8s 把健康检查集成进 Pod 生命周期

常见误解

误解一:ClusterIP 可以用 ping 测试连通性。

ClusterIP 不绑定任何网络接口,没有任何进程监听它,ICMP echo 不经过 iptables nat 表的 DNAT 规则(DNAT 只处理 TCP/UDP),ping 包抵达"ClusterIP"时没有接口应答,被内核丢弃。正确的测试方式是用 curlnc 发起 TCP 连接,或者用 kubectl run 临时启动一个 Pod 在集群内测试。从节点外(宿主机网络)也 ping 不通,因为 ClusterIP 的路由规则只存在于集群节点内部(iptables/IPVS 规则),外部网络完全不知道这个 IP 的存在。

误解二:kube-proxy 是流量代理,所有数据包经过 kube-proxy 进程处理。

kube-proxy 从不处理用户数据包。它的工作是 watch API Server 的变化,维护 iptables/IPVS 规则,然后等待下一次变化。数据面由内核的 netfilter/IPVS 子系统完全承担,不经过任何用户态进程。kube-proxy 进程崩溃后,已有的规则继续存在于内核,已建立的 TCP 连接完全不受影响,只有新的 Service/Pod 变更不再被同步到规则里,存量规则逐渐与实际状态偏离。这与 Nginx/HAProxy 等用户态代理的工作模式有根本区别——用户态代理进程崩溃即服务中断,kube-proxy 崩溃只影响规则同步,不影响已有转发。

误解三:iptables 模式和 IPVS 模式的负载均衡效果完全相同,只是性能不同。

两者的负载均衡算法能力有本质差异。iptables 只有 statistic random 概率分配,是纯随机 round-robin 的近似,不感知后端实际负载,也不跟踪后端连接数。IPVS 支持 lc(least connection)等感知后端连接数的算法:在后端 Pod 处理时间差异大的场景(比如部分请求是计算密集型任务、部分是轻量查询),lc 算法能更均衡地分配负载,避免某个 Pod 因为连接积压而成为瓶颈,而 iptables 的纯随机分配在这种场景下可能让重负载 Pod 继续接收新连接。

练习

练习一:在 kind 集群里创建 5 个副本的 Deployment,用 iptables -t nat -L KUBE-SVC-XXXX -n 查看五条概率规则(0.2、0.25、0.333、0.5、1.0),验证链式概率的数学原理——每条规则匹配到时,剩余请求的概率按比例分配给后续规则,保证每个 Pod 期望被选中的概率均等。用 hey -n 1000 -c 20 http://<ClusterIP> 发请求,统计每个 Pod 的日志访问次数,验证实际分布接近均等。

练习二:模拟 readinessProbe 失败的流量摘除过程。创建一个 Deployment,进入某个 Pod 并在其 80 端口返回 503,触发 readinessProbe 失败;用 watch kubectl get endpointslices -l kubernetes.io/service-name=nginxwatch iptables -t nat -L KUBE-SVC-XXXX -n 同时观察。记录从 readinessProbe 首次失败,到 EndpointSlice 端点变为 ready: false,到 KUBE-SEP 规则被删除的三个时间点,分析各阶段的延迟来源。

练习三:对比 iptables 模式和 IPVS 模式的规则同步速度。在集群里快速创建 200 个 Service,用 time 记录 kube-proxy 完成全部规则同步的耗时(观察 kube-proxy 日志里的 sync 时间)。切换到 IPVS 模式重复相同测试,对比"全量重写 iptables"和"增量更新 IPVS hash table"的耗时差异。

练习四:创建一个 Headless Service,部署 StatefulSet(3 个副本),用 nslookup nginx.default.svc.cluster.local(返回三个 Pod IP)和 nslookup nginx-0.nginx.default.svc.cluster.local(返回单个 Pod IP),理解 Headless Service 如何绕过 DNAT 支持 Pod 级别的直接寻址。再用 tcpdump 在某个 StatefulSet Pod 上抓包,验证来自其他 Pod 的直连请求源 IP 是发送方 Pod IP,而不是 ClusterIP 或节点 IP。

Service 的高级特性

Service 规范里有几个不常用但在特定场景极具价值的字段,值得单独说明。

spec.publishNotReadyAddresses:默认 false,意味着 Pod 的 readinessProbe 失败时,该 Pod 不出现在 EndpointSlice 的就绪列表里,kube-proxy 不会把流量路由过去。设为 true 时,即使 readinessProbe 失败,Pod IP 也出现在 EndpointSlice 里,流量仍然路由过来。这个字段的典型用途是 StatefulSet 里的主从数据库集群:主库在启动时需要连接其他节点做 leader election 或数据同步,如果其他节点的 readinessProbe 还没通过、Pod 不出现在 Endpoints 里,集群内部的互连无法建立,永远无法完成启动。publishNotReadyAddresses: true 允许 Pod 在 readiness 通过前就被其他 Pod 找到,专门用于有状态集群的自举(bootstrap)阶段。

spec.topologyKeys(已废弃,被 trafficDistribution 替代)用于拓扑感知路由的早期实现。现代 Kubernetes(1.31 GA)通过 spec.trafficDistribution: PreferClose 启用拓扑感知路由:kube-proxy 在计算转发权重时,优先增大与请求方同一可用区的后端 Pod 的权重,同时保证每个 Pod 至少有一定的基础权重(避免某个 AZ 的 Pod 完全不收到流量)。这个特性需要节点有 topology.kubernetes.io/zone 标签,EndpointSlice 的端点有 zone 字段,kube-proxy 版本在 1.27+。

spec.allocateLoadBalancerNodePorts:对 LoadBalancer 类型的 Service,默认 true,cloud-controller-manager 会同时分配 NodePort(云 LB 的健康检查和流量都通过 NodePort 进入节点)。设为 false 时跳过 NodePort 分配,适用于支持直接路由到 Pod IP 的云 LB(如 AWS NLB 的 IP 目标组模式),流量不经过 NodePort,减少一跳,客户端 IP 保留更可靠。

spec.externalTrafficPolicyspec.internalTrafficPolicy 分别控制外部流量和集群内部流量的路由策略。internalTrafficPolicy: Local(1.26 GA)让集群内部的 Pod 在访问 Service 时也优先选择本节点的后端 Pod(如果本节点有就绪 Pod 的话),没有本地 Pod 时回退到正常路由。这对同节点通信特别有价值——节点内的 Pod 间通信不需要离开节点,延迟最低,也避免了跨节点的带宽消耗。

kube-proxy 模式切换与调优

生产环境从 iptables 模式切换到 IPVS 模式的操作步骤相对简单,但有几个细节需要注意。

切换前提:确认内核模块已加载(lsmod | grep ip_vs),未加载则执行:

1
2
3
4
5
modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack

切换方式:修改 kube-proxy 的 ConfigMap(kubectl edit cm kube-proxy -n kube-system),把 mode: "" 改为 mode: "ipvs",同时可以设置 ipvs.scheduler: "lc"(最少连接)。改完后 kubectl rollout restart daemonset kube-proxy -n kube-system 触发重启。

切换后验证:ipvsadm -L -n 查看 IPVS 虚拟服务器列表,每个 Service 对应一个 virtual server,每个就绪 Pod 对应一个 real server。ipvsadm -L -n --stats 查看每个 real server 的连接数和流量统计,验证负载均衡的实际分布。

调优参数:ipvs.tcpTimeout(TCP 连接的 IPVS 超时,默认 900 秒,长连接场景可适当调大)、ipvs.tcpFinTimeout(TCP FIN 后的超时,默认 16 秒)、ipvs.udpTimeout(UDP 超时,默认 300 秒)。这些超时控制 IPVS 连接追踪表的老化时间,设置过小会导致长连接被意外断开,设置过大会占用内核内存。

conntrack 调优:IPVS 模式仍然使用 nf_conntrack 做连接追踪。高并发场景下 conntrack 表可能耗尽(nf_conntrack: table full, dropping packet 错误),需要调整 nf_conntrack_maxsysctl -w net.netfilter.nf_conntrack_max=1000000)和 nf_conntrack_buckets(哈希桶数量,通常设为 max/4)。conntrack 满载是 Kubernetes 集群网络问题中最难排查的根因之一,因为它以"随机丢包"的形式出现,没有明显的错误日志。

Service 网络故障排查

Service 网络问题有一套固定的分层排查路径,从最外层开始逐层向内缩小范围。

第一层,确认 Service 对象本身配置正确。kubectl get svc <name> -o yaml 查看完整配置:spec.selector 是否与 Pod 的标签匹配(标签不匹配是最常见的"Service 创建成功但无后端"原因)、spec.ports 里的 targetPort 是否与 Pod 监听的端口一致(容器端口名或号码)、spec.type 是否符合预期。kubectl get endpoints <name> 直接看后端 IP 列表,如果 ENDPOINTS 列是 <none>,说明没有 Pod 通过了 selector + readinessProbe 双重过滤。

第二层,确认后端 Pod 健康。kubectl get pod -l <selector> 列出被 Service 选中的 Pod,检查 STATUSREADY 列。kubectl describe pod <name> 查看 Events,readinessProbe 失败会在这里显示(Readiness probe failed: ...)。进入 Pod 内部 curl localhost:<port> 直接测试应用是否在监听。

第三层,确认 kube-proxy 规则已同步。在任意节点上 iptables -t nat -L KUBE-SERVICES -n | grep <clusterIP> 确认 KUBE-SERVICES 链里有对应 Service 的规则。如果规则缺失,检查 kube-proxy 日志(kubectl logs -n kube-system -l k8s-app=kube-proxy)是否有同步错误。IPVS 模式下用 ipvsadm -L -n | grep <clusterIP> 验证 virtual server 存在。

第四层,测试 DNS 解析。kubectl run tmp --image=busybox --restart=Never --rm -it -- nslookup <service-name> 临时启动一个 Pod 测试 DNS。如果解析失败,先确认 CoreDNS Pod 运行正常(kubectl get pod -n kube-system -l k8s-app=kube-dns),再检查 Pod 的 /etc/resolv.conf 是否正确注入了 CoreDNS ClusterIP。

第五层,抓包验证。如果前四层都正常但连接仍然失败,在相关节点上 tcpdump -i any -n host <clusterIP> 抓包。能看到 SYN 包到达说明请求发出来了;能看到 SYN 包被 DNAT 改写后的目标 IP 说明 iptables 规则生效;如果 SYN 包到达 Pod 但没有 SYN-ACK,问题在 Pod 内部(应用未监听、防火墙拦截);如果 SYN-ACK 发出但发起方收不到,问题在回包路由(通常是 SNAT 配置问题)。

Service 拓扑与多集群

Kubernetes 的 Service 设计默认是单集群的,但大型系统往往需要跨集群的服务发现和流量路由。

单集群内,Service 的 ClusterIP 只在该集群内可达。两个集群的 Pod 之间通信不能直接用对方集群的 ClusterIP(IP 段可能冲突,即使不冲突也没有路由),需要借助专门的多集群方案。

多集群 DNS(如 Admiralty、Liqo):在每个集群里为远端服务创建镜像 Service,DNS 解析到本集群的镜像 Service,再通过隧道或 BGP 转发到远端集群。应用代码不需要感知多集群,访问 svc-name.namespace.svc.cluster.local 即可,底层处理跨集群转发。

Submariner:用 IPsec 或 WireGuard 隧道打通多个集群的 Pod 网络,集群间的 Pod IP 直接可路由,跨集群 Service 通过 serviceExportserviceImport CRD 声明。不需要修改 CoreDNS 或 kube-proxy,但需要管理隧道的建立和维护。

Istio 多集群(Primary-Remote 或 Multi-Primary):服务网格层面的多集群,通过 Istio control plane 同步服务注册表,sidecar 代理拦截流量并路由到跨集群的端点。适合已经引入 Istio 的场景,运维复杂度较高,但服务发现、流量管理、mTLS 等能力最完整。

这些方案各有适用场景,Service 在单集群内的工作方式不受影响,多集群层是叠加在 Service 之上的额外抽象层。选型时优先考虑是否已有 Istio(有则 Istio 多集群)、是否需要 Pod IP 直通(需则 Submariner)、还是只需要 DNS 级别的透明感知(DNS 镜像方案)。

CNCF 的 MultiCluster SIG 正在推进跨集群服务发现的标准化。serviceExportmulticluster.x-k8s.io/v1alpha1)允许在一个集群里把 Service 导出,serviceImport 在另一个集群里声明要使用对方导出的服务,两者配合完成跨集群的服务注册与发现。CoreDNS 的 multicluster 插件负责把跨集群的 DNS 查询路由到正确的 serviceImport,使应用代码仍然使用标准的 svc.cluster.local 域名,感知不到多集群的存在。这个标准还处于成熟期,但已有多个实现(Submariner、Liqo、Cilium Cluster Mesh)开始支持。

Cilium Cluster Mesh 是多集群方案里技术栈最统一的选择:如果所有集群都使用 Cilium,Cluster Mesh 可以直接打通多个集群的 Pod 网络,跨集群 Service 通过 annotation 标记为全局(service.cilium.io/global: "true"),Cilium 自动把请求路由到任意集群里的健康后端。跨集群流量走 Cilium 的加密隧道(WireGuard 或 IPsec),不需要单独的隧道管理工具。

conntrack 与连接跟踪调优

kube-proxy 的 iptables 模式依赖 Linux 内核的 conntrack(连接跟踪)模块,conntrack 表有容量上限,大规模集群下 conntrack 表满是一类真实的生产故障。

conntrack 表满的症状:dmesg 里出现 nf_conntrack: table full, dropping packet,同时节点上的新建连接开始被丢弃,但已有连接不受影响(已有条目不需要新建跟踪项)。conntrack -L | wc -l 查看当前条目数,/proc/sys/net/netfilter/nf_conntrack_max 查看上限,当前条目数超过上限的 80% 就应该考虑扩容。

调整方法:sysctl -w net.netfilter.nf_conntrack_max=1048576(根据节点内存调整,每条条目约 320 字节,100 万条约 320MB)、sysctl -w net.netfilter.nf_conntrack_buckets=262144(哈希表桶数,建议为 max 的 1/4)。conntrack 的 ESTABLISHED 状态超时默认 5 天,对短连接场景可适当缩短(net.netfilter.nf_conntrack_tcp_timeout_established=86400),减少表中废旧条目的占用。

IPVS 模式同样使用 conntrack,但 IPVS 本身有独立的连接表(ipvsadm -L --stats),IPVS 连接超时参数通过 ipvsadm --set <tcp> <tcp-fin> <udp> 配置,与 conntrack 超时是两套独立的机制,都需要根据业务流量特征调整。

高并发短连接场景(如 HTTP 长轮询、gRPC streaming 降级为短连接)下,conntrack 表消耗速度极快。此时可以考虑切换到 Cilium kube-proxy 替代模式——Cilium 的 socket 级别 DNAT 在 connect() 阶段完成地址转换,绕过了 conntrack,对短连接场景的性能提升显著。

Service 滚动发布与零停机更新

Service 与 Deployment 协同工作时,零停机更新依赖几个机制的配合,理解这些机制有助于在滚动发布时避免连接中断。

Deployment 的滚动更新策略(RollingUpdate)控制新旧 Pod 的替换节奏:maxUnavailable 决定允许同时不可用的 Pod 数量(通常设为 0,确保全程有足够副本),maxSurge 决定允许额外启动的 Pod 数量(通常设为 1,先起新 Pod 再删旧 Pod)。新 Pod 通过 readinessProbe 后,kubelet 更新 Pod 条件(Ready: true),EndpointSlice controller 把新 Pod IP 加入 EndpointSlice,kube-proxy 同步新规则,流量才开始路由到新 Pod。

旧 Pod 删除时,Kubernetes 先从 EndpointSlice 摘除该 Pod IP(EndpointSlice controller 监听 Pod 删除事件,把 conditions.terminating 置为 true,kube-proxy 停止向该端点发送新连接),然后发送 SIGTERM,等待 terminationGracePeriodSeconds(默认 30 秒),最后强制 SIGKILL。这个序列的关键点:摘除端点和发送 SIGTERM 几乎同时发生,但 kube-proxy 同步规则有延迟(通常 1-3 秒),在这个窗口期可能有新连接仍被路由到正在终止的 Pod。应用需要在 SIGTERM 处理里继续处理正在进行的请求,不能立即关闭 listener。preStop hook(在 SIGTERM 前执行)里加一个短暂的 sleep 5 可以给 kube-proxy 足够时间完成规则同步,是一个简单有效的零停机实践。

headless Service(clusterIP: None)与有状态应用的结合值得单独说明。StatefulSet 通常配合 headless Service 使用,原因是 StatefulSet 的 Pod 需要稳定的网络标识(pod-name.headless-svc.namespace.svc.cluster.local),而不是随机路由到某个副本。headless Service 不分配 ClusterIP,CoreDNS 为其生成多条 A 记录(每个就绪 Pod 一条),客户端通过 DNS 轮询或客户端负载均衡选择 Pod。Kafka、Elasticsearch、ZooKeeper 这类有状态集群,每个节点都有独立标识,节点间通过 Pod DNS 名称互相寻址,headless Service 是实现这一模式的标准 Kubernetes 原语。

Service 的 sessionAffinity: ClientIP 会话亲和性把同一客户端 IP 的请求固定路由到同一个后端 Pod,由 iptables 的 recent 模块(iptables 模式)或 IPVS 的 sh(source hash)调度算法实现。会话亲和性的有效期由 sessionAffinityConfig.clientIP.timeoutSeconds(默认 10800 秒,即 3 小时)控制。注意:来自同一客户端 IP 的请求在亲和性超时后会被重新分配到随机后端,不适合对会话极度敏感的场景;对于需要严格会话粘性的应用,应在应用层(如 JWT 携带会话 ID)或 Ingress 层(nginx.ingress.kubernetes.io/affinity: cookie)实现更可靠的会话管理。

NodePort Service 在使用 externalTrafficPolicy: Cluster(默认值)时,流量会被 SNAT 成节点 IP,后端 Pod 看到的源 IP 是节点 IP 而不是真实客户端 IP。这在需要记录客户端真实 IP(日志、限流、地理路由)的场景下是个问题。解决方案有两种:改为 externalTrafficPolicy: Local(保留源 IP,但只转发到本节点 Pod,其他节点必须通过云 LB 均衡流量);或者使用 Ingress/Gateway API,在 L7 层通过 X-Forwarded-For 头部传递真实 IP。LoadBalancer Service 配合 externalTrafficPolicy: Local 和云 LB 的健康检查端口(NodePort 33x-400 范围)一起使用,可以实现流量只进入有本地 Pod 的节点,同时保留源 IP。

Kubernetes 1.30 引入的 ClusterLoadBalancer 功能(当前 alpha)计划允许集群内部的客户端也能通过 LoadBalancer VIP 访问 Service,目前 LoadBalancer 的 ExternalIP 从集群内部访问行为是实现相关的(有些云提供商支持,有些不支持)。这是 Kubernetes 网络模型的一个长期遗留问题,ClusterLoadBalancer 是标准化尝试之一。

dual-stack(IPv4/IPv6 双栈)支持在 Kubernetes 1.21 升为 stable。双栈集群里,Service 可以同时有 IPv4 ClusterIP 和 IPv6 ClusterIP(spec.ipFamilyPolicy: RequireDualStack),Pod 同时分配 IPv4 和 IPv6 地址。kube-proxy 在双栈模式下同时维护 IPv4 和 IPv6 的 iptables/IPVS 规则。CoreDNS 为双栈 Service 同时返回 A 记录(IPv4)和 AAAA 记录(IPv6)。从单栈迁移到双栈需要 CNI 插件、kube-proxy、CoreDNS 同步升级配置,且集群 CIDR 需要同时指定 IPv4 和 IPv6 段——这是双栈最大的运维负担,建议在集群初始化时就规划好是否需要双栈,而不是事后迁移。

Service 对象的变更不会触发 Pod 重启,这是 Kubernetes 控制平面设计中的一个重要特性。修改 Service 的 selector、ports、type 等字段后,kube-proxy 异步同步规则,已有连接不受影响,新连接按新规则路由。这意味着 Service 配置变更是低风险操作,可以在不影响已有流量的前提下调整路由策略。但 ClusterIP 一旦分配就不能修改(需要删除重建),因为 ClusterIP 可能已经硬编码在应用配置或 DNS 缓存里,变更会导致找不到服务。

kube-proxy 本身的可观测性在新版本里有所改善。kube-proxy 在 10249 端口(--metrics-bind-address)暴露 Prometheus 指标,关键指标包括:kubeproxy_sync_proxy_rules_duration_seconds(规则同步耗时,高延迟说明 iptables 规则量过大或节点负载过高)、kubeproxy_sync_proxy_rules_last_timestamp_seconds(上次同步时间,用于告警规则同步是否卡住)、kubeproxy_network_programming_duration_seconds(从 EndpointSlice 变化到规则生效的端到端延迟)。将这些指标接入告警,可以提前发现 kube-proxy 积压导致的服务发现延迟问题,而不是等到应用层出现超时后才开始排查。

系列导航

参考资料

  • Kubernetes Services — Service 对象完整规范,包含四种类型、会话亲和性、topology 路由的官方说明
  • kube-proxy — kube-proxy 命令行参数参考,包含 --proxy-mode--ipvs-scheduler 等关键配置
  • EndpointSlices — EndpointSlice 设计动机、分片机制和拓扑感知路由说明
  • IPVS-Based In-Cluster Load Balancing Deep Dive — Kubernetes 官方博客对 IPVS 模式的深入分析,包含性能对比数据
  • Cilium kube-proxy Replacement — Cilium 完整替换 kube-proxy 的配置方式和 eBPF socket 负载均衡原理
  • Linux IPVS — LVS 项目官方文档,各调度算法的原始定义和适用场景
  • netfilter/iptables — iptables 和 netfilter 框架官方资料,了解 PREROUTING/OUTPUT 链和 nat 表的工作机制
  • kube-proxy Metrics — kube-proxy 暴露的 Prometheus 指标列表,包含规则同步延迟和网络编程耗时
  • Kubernetes Dual-Stack — IPv4/IPv6 双栈配置指南,包含 Service ipFamilyPolicy 和集群 CIDR 配置方式