vault backup: 2026-07-07 15:02:02

This commit is contained in:
2026-07-07 15:02:02 +08:00
parent b9572cee3e
commit 0027259d64
3 changed files with 244 additions and 1 deletions
+1
View File
@@ -1,3 +1,4 @@
.obsidian/
.claude/
.claudian/
.DS_Store
+1
View File
@@ -45,3 +45,4 @@ graph LR
| 3 | [[technical/xinfra-preview/cloud-dm-overview]] | 数据库 SQL 审核平台 |
| 4 | [[technical/xinfra-preview/cachecloud-overview]] | Redis 实例管理平台 |
| 5 | [[technical/xinfra-preview/ansible-playbook-basics]] | 服务自动化部署工具 |
| 6 | [[technical/xinfra-preview/multi-host-virtual-ports]] | Service 虚拟端口与 kube-proxy 转发机制 |
@@ -0,0 +1,241 @@
---
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 预习总览,本笔记的父文档