Files
Qiniu/technical/xinfra-preview/multi-host-virtual-ports.md
T

11 KiB
Raw Blame History

tags, create time
tags create time
k8s
networking
kube-proxy
service
2026-07-07 14:47

K8s Service 与 kube-proxy 虚拟端口

概述

Kubernetes Service 通过虚拟 IP(ClusterIP)和 kube-proxy 的流量转发机制,为一组动态变化的 Pod 提供稳定的网络访问入口——这是理解集群内服务间通信的关键。

核心概念

ClusterIP 的本质:一个"不存在"的 IP

当创建一个 ClusterIP 类型的 Service 时,K8s 做了两件事:

  1. 在 etcd 中分配一个虚拟 IP(来自 service-cidr,如 10.43.0.0/16),这个 IP 不绑定在任何网卡上——你在任何节点执行 ip addr 都看不到它
  2. kube-proxy 在每个节点上写入转发规则,将发往该虚拟 IP + 端口的流量透明地分发到后端 Pod

[!info] 虚拟端口 vs 真实端口 Service 的 port(如 80)是虚拟端口——它不存在于任何进程监听列表中。真正监听的是 Pod 内的 targetPort(如 8080)。kube-proxy 负责在中间做"端口翻译"。

graph LR
    C[Client Pod] -->|"TCP to 10.43.0.100:80"| VIP["ClusterIP<br/>10.43.0.100:80"]
    VIP -->|"iptables/IPVS DNAT"| P1["Pod-A 10.42.1.5:8080"]
    VIP -->|"iptables/IPVS DNAT"| P2["Pod-B 10.42.2.8:8080"]
    VIP -->|"iptables/IPVS DNAT"| P3["Pod-C 10.42.3.2:8080"]

kube-proxy 三种模式对比

kube-proxy 是运行在每个节点上的网络代理,它 watch kube-apiserver 上 Service/Endpoints 的变化,然后在本机维护转发规则。

维度 iptables IPVS nftables(K8s 1.29+ alpha)
实现方式 内核 iptables 链规则 内核 IPVS 模块(LVS) nftables(iptables 的继任者)
匹配复杂度 O(n) 链表遍历 O(1) 哈希查找 O(n) → 未来可优化
大规模性能 Service 多时规则膨胀,延迟线性增长 万级 Service 仍保持稳定 介于两者之间,持续演进中
负载均衡算法 随机 / 概率(nth) rr / wrr / lc / sh 等丰富算法 随机 / 概率
连接跟踪 conntrack 表 IPVS 自己的连接表 conntrack
RKE2 默认 ✅ 是(传统默认) 需手动启用 实验阶段

启发问题:为什么 IPVS 用哈希表而非链表?——iptables 对每条规则做线性匹配,当 Service 数量上千时,一个新连接可能要遍历数千条规则才能命中;IPVS 在内核中维护哈希表,一次查找直接定位,这是本质区别。

一条请求的完整路径:Pod → Service → 目标 Pod

sequenceDiagram
    participant Client as Client Pod (10.42.1.3)
    participant KProxy as 本机 kube-proxy
    participant Kernel as 内核网络栈
    participant Target as Target Pod (10.42.2.8:8080)

    Client->>Kernel: ① connect(10.43.0.100:80)
    Note over Kernel: CNI 插件已配置路由<br/>10.43.0.0/16 → kube-proxy
    KProxy-->>Kernel: ② iptables/IPVS 规则持续 watch
    Note over Kernel: ③ DNAT: dst 10.43.0.100:80 → 10.42.2.8:8080
    Note over Kernel: ④ 如果目标 Pod 在其他节点:<br/>通过 CNI overlay 或 host-gw 封装转发
    Kernel->>Target: ⑤ 10.42.2.8:8080
    Target-->>Client: ⑥ 响应原路返回(reverse NAT)

关键步骤拆解:

  1. 客户端发出请求:Pod 内的应用调用 connect(10.43.0.100:80),对应用完全透明
  2. kube-proxy 规则生效:通过 iptables DNAT 或 IPVS 将目标地址改写为真实 Pod IP + 端口
  3. 跨节点转发:如果目标 Pod 在另一台机器上,由 CNI 插件(Flannel VXLAN / Calico BGP)封装后送达
  4. 响应返回:conntrack 记录了 NAT 映射关系,反向包自动做 SNAT 回到 ClusterIP

Service 类型扩展

类型 流量来源 kube-proxy 行为 典型场景
ClusterIP(默认) 集群内 Pod 仅在本机 iptables/IPVS 添加规则 服务间调用
NodePort 外部 → 任意节点 IP:Port 在 ClusterIP 基础上额外开放 30000-32767 端口 开发测试、快速验证
LoadBalancer 外部 → 云厂商 LB → NodePort → Pod 依赖云 controller 创建外部 LB 云上生产环境
ExternalName 集群内 DNS 查询 kube-proxy 不介入,CoreDNS 返回 CNAME 引用集群外服务(如 RDS)

[!tip] NodePort 的"隐藏代价" NodePort 会在集群所有节点上开放端口(即使是没有运行该 Service Pod 的节点)。这意味着外部流量可能先到达一个"空"节点,再经过一次内部转发才能到达目标 Pod——多了一跳延迟。用 externalTrafficPolicy: Local 可避免这个问题(但要注意流量不均)。

iptables vs IPVS 深入

iptables 的规则膨胀问题

每个 Service 会产生 N 条 iptables 规则(N = 后端 Pod 数):

# 查看一个 3-Pod Service 生成的 iptables 规则
iptables -t nat -L KUBE-SERVICES -n | grep my-service
# KUBE-SVC-XXXX  tcp -- 0.0.0.0/0  10.43.0.100  tcp dpt:80
iptables -t nat -L KUBE-SVC-XXXX -n
# KUBE-SEP-AAA  statistic mode random probability 0.33333  → Pod-A
# KUBE-SEP-BBB  statistic mode random probability 0.50000  → Pod-B
# KUBE-SEP-CCC  /* default */                              → Pod-C

[!warning] 1000 个 Service × 3 Pod = 3000+ 规则 每次新建连接,内核从第一条开始逐条匹配。实测表明:当规则超过 5000 条时,单次连接的 DNAT 延迟会从微秒级劣化到毫秒级,对在线服务影响显著。

IPVS 的优势

# 查看 IPVS 虚拟服务
ipvsadm -Ln
# TCP  10.43.0.100:80 rr
#   → 10.42.1.5:8080     Masq    1      0
#   → 10.42.2.8:8080     Masq    1      0
#   → 10.42.3.2:8080     Masq    1      0
场景 iptables IPVS
100 Service ~300 条规则,性能无感知差异 ~100 个 hash entry,无感知差异
1000 Service ~3000+ 条规则,连接建立延迟可见劣化 ~1000 entry,几乎无变化
10000 Service 规则更新耗时数秒,iptables-restore 阻塞 独立于规则数量,毫秒级更新
负载均衡策略 随机/概率 支持 rr / wrr / lc / sh / sed / nq

[!tip] 如何在 RKE2 启用 IPVS 在 /etc/rancher/rke2/config.yaml 中配置:

kube-proxy-arg:
  - "proxy-mode=ipvs"
  - "ipvs-scheduler=rr"       # 轮询调度

需确保节点内核已加载 ip_vs 模块:modprobe ip_vs && lsmod | grep ip_vs

RKE2 中的实践

xinfra 基于 RKE2 集群,以下是与 Service / kube-proxy 相关的关键配置:

graph TB
    subgraph "RKE2 集群(xinfra)"
        subgraph "网络栈"
            Canal["Canal (Calico + Flannel)"]
            KProxy["kube-proxy (iptables 模式)"]
            CoreDNS["CoreDNS"]
        end
        subgraph "Service 层"
            SVC1["ClusterIP Service"]
            SVC2["NodePort Service"]
        end
        Canal -->|"Pod-to-Pod 网络"| KProxy
        KProxy -->|"Service 转发"| SVC1
        KProxy -->|"NodePort 转发"| SVC2
        CoreDNS -->|"DNS 解析"| SVC1
    end

默认配置要点:

配置项 RKE2 默认值 说明
cluster-cidr 10.42.0.0/16 Pod IP 地址范围
service-cidr 10.43.0.0/16 ClusterIP 地址范围
kube-proxy 模式 iptables 小规模集群足够
CNI 插件 Canal(Calico + Flannel) 提供 Pod 网络 + NetworkPolicy
CoreDNS 内置 Service DNS 自动解析

[!info] xinfra 七机房部署 xinfra 管理七机房的 RKE2 集群。每个集群独立的 service-cidr,跨集群服务访问不通过 ClusterIP,而是通过 Wayne 多集群管理平台统一调度——理解这点很重要:ClusterIP 的作用域是单集群内。

常见陷阱与最佳实践

1. Session Affinity(会话保持)

默认情况下,kube-proxy 对每个连接做随机/轮询分发。如果你的应用有状态(如 Session 存储在内存),需要开启:

spec:
  sessionAffinity: ClientIP        # 基于客户端 IP 做会话保持
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800        # 3 小时超时

[!warning] Session Affinity 不是银弹 如果后端 Pod 重启,连接会中断。有状态应用更推荐用 StatefulSet + Headless Service 做稳定的网络标识。

2. Headless Service(无头 Service)

当你需要直接获取 Pod IP 而不经过负载均衡时,使用 Headless Service:

spec:
  clusterIP: None                  # 关键:不分配 ClusterIP
  ports:
  - port: 80

DNS 查询返回的是所有 Pod 的 IP 列表,而非单个虚拟 IP。适用于:

  • StatefulSet(如 MySQL 主从、Redis Cluster)
  • 客户端自己实现负载均衡的场景

3. externalTrafficPolicy 的选择

spec:
  externalTrafficPolicy: Cluster   # 默认:跨节点转发,可能多一跳
  # externalTrafficPolicy: Local   # 只转发到本节点 Pod,保留源 IP
策略 优点 缺点
Cluster(默认) 流量均匀分布到所有 Pod 多一跳延迟,丢失源 IP
Local 保留源 IP,低延迟 流量分布不均(取决于 LB 分发)

4. Service 无法访问排查清单

当 curl <service-name>:<port> 超时或拒绝连接时:

flowchart TD
    A["Service 不通"] --> B{"kubectl get endpoints<br/>有 IP 吗?"}
    B -->|无| C["检查 selector 是否匹配 Pod label<br/>检查 Pod 是否 Ready"]
    B -->|有| D{"Pod 内 curl targetPort<br/>通吗?"}
    D -->|不通| E["应用未监听 / 端口错误 / readinessProbe 失败"]
    D -->|通| F{"CoreDNS 解析正确吗?<br/>nslookup service-name"}
    F -->|解析失败| G["检查 CoreDNS Pod 状态<br/>检查 resolv.conf"]
    F -->|解析成功| H["检查 kube-proxy 日志<br/>检查 iptables/IPVS 规则是否写入"]

5. 避免的常见错误

  • selector typo:Service 的 spec.selector 与 Pod 的 metadata.labels 不匹配,导致 Endpoints 为空
  • 端口不匹配:Service 的 targetPort 与容器实际监听端口不一致
  • NetworkPolicy 阻断:Calico NetworkPolicy 可能阻止了 Service 到 Pod 的流量

延伸阅读

关联笔记