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

242 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [k8s, networking, kube-proxy, service]
create time: 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 负责在中间做"端口翻译"。
```mermaid
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
```mermaid
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 数):
```bash
# 查看一个 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 的优势
```bash
# 查看 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` 中配置:
> ```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 相关的关键配置:
```mermaid
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 存储在内存),需要开启:
```yaml
spec:
sessionAffinity: ClientIP # 基于客户端 IP 做会话保持
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 小时超时
```
> [!warning] Session Affinity 不是银弹
> 如果后端 Pod 重启,连接会中断。有状态应用更推荐用 StatefulSet + Headless Service 做稳定的网络标识。
### 2. Headless Service(无头 Service)
当你需要**直接获取 Pod IP 而不经过负载均衡**时,使用 Headless Service:
```yaml
spec:
clusterIP: None # 关键:不分配 ClusterIP
ports:
- port: 80
```
DNS 查询返回的是**所有 Pod 的 IP 列表**,而非单个虚拟 IP。适用于:
- StatefulSet(如 MySQL 主从、Redis Cluster)
- 客户端自己实现负载均衡的场景
### 3. externalTrafficPolicy 的选择
```yaml
spec:
externalTrafficPolicy: Cluster # 默认:跨节点转发,可能多一跳
# externalTrafficPolicy: Local # 只转发到本节点 Pod,保留源 IP
```
| 策略 | 优点 | 缺点 |
|------|------|------|
| `Cluster`(默认) | 流量均匀分布到所有 Pod | 多一跳延迟,丢失源 IP |
| `Local` | 保留源 IP,低延迟 | 流量分布不均(取决于 LB 分发) |
### 4. Service 无法访问排查清单
当 `curl <service-name>:<port>` 超时或拒绝连接时:
```mermaid
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 的流量
## 延伸阅读
- [Kubernetes 官方文档 — Service](https://kubernetes.io/docs/concepts/services-networking/service/)
- [Kubernetes 官方文档 — IPVS-based service proxy](https://kubernetes.io/docs/reference/networking/virtual-ips/)
- [深入理解 kube-proxy 的三种模式(推荐)](https://www.qikqiak.com/post/kube-proxy-iptables-ipvs/)
## 关联笔记
- [[technical/xinfra-preview/k8s-rke2-fundamentals]] — K8s 基础概念,包含 Service 类型概览和 Ingress 示例
- [[technical/xinfra-preview]] — xinfra 预习总览,本笔记的父文档