Kubernetes Service Proxy 与负载均衡:iptables 流量劫持全解析 核心矛盾:Pod IP 的不稳定 vs 访问的稳定需求 问题背景 在 Kubernetes 集群中,每个 Pod 都有独立的 IP 地址(如 10.244.0.3),但 Pod 的生命周期极其短暂:
1 2 3 4 5 6 7 8 9 10 11 12 13 kubectl get pods -l app=my-nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE nginx-1 1/1 Running 0 69s 10.244.0.3 minikube nginx-2 1/1 Running 0 64s 10.244.0.4 minikube kubectl delete pod nginx-1 kubectl run nginx-1 --image=nginx --labels="app=my-nginx" kubectl get pods -l app=my-nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE nginx-1 1/1 Running 0 10s 10.244.0.5 minikube nginx-2 1/1 Running 0 64s 10.244.0.4 minikube
痛点 :客户端不可能每次调用前都问一句”你现在 IP 是多少?”
Service 的解决方案 Kubernetes 引入了 ClusterIP (虚拟 IP,VIP)的概念:
1 2 3 4 5 6 7 kubectl create service clusterip my-service --tcp=80:80 kubectl set selector service my-service "app=my-nginx" kubectl get svc my-service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-service ClusterIP 10.101.164.222 <none> 80/TCP 98s
关键特性 :
✅ Client 只需记住这个固定的假 IP(10.101.164.222)
✅ 后端 Pod 随便换,VIP 永远不变
✅ 自动负载均衡到多个后端 Pod
颠覆认知:ClusterIP 根本不是真实存在的 IP 常见误区 很多人以为集群里有个路由器或网卡占用了 10.101.164.222 这个地址。
真相 :
❌ 没有任何网卡配置这个 IP
❌ Ping 这个 IP 大概率不通(没有物理实体回应 ICMP)
✅ 它只存在于 Linux 内核的 iptables/IPVS 规则表中
✅ 只有发起 TCP/UDP 连接时,内核才会根据规则捕获数据包
验证实验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 minikube ssh ip addr show | grep "inet " ping 10.101.164.222 -c 2 curl http://10.101.164.222 <!DOCTYPE html> <html> <head ><title>Welcome to nginx!</title></head> <body>...</body> </html>
流量劫持全过程:数据包的奇幻漂流记 四个核心步骤 Step 1: 拦截 (KUBE-SERVICES) 1 2 3 4 5 sudo iptables-save | grep "10.101.164.222" -A KUBE-SERVICES -d 10.101.164.222/32 -p tcp \ -m comment --comment "default/my-service:80-80 cluster IP" \ -j KUBE-SVC-JLFZCDARX7J7XLB7
动作 :内核监控所有流经的数据包逻辑 :”如果你的目标地址是 10.101.164.222,别走通用通道,来我这儿!”
Step 2: 分发 (KUBE-SVC-XXX) - 负载均衡 1 2 3 4 5 6 7 8 9 10 11 12 13 14 sudo iptables-save | grep "KUBE-SVC-JLFZCDARX7J7XLB7" :KUBE-SVC-JLFZCDARX7J7XLB7 - [0:0] -A KUBE-SVC-JLFZCDARX7J7XLB7 \ -m comment --comment "default/my-service:80-80 -> 10.244.0.3:80" \ -m statistic --mode random --probability 0.50000000000 \ -j KUBE-SEP-4BQUF3WY4YN4VT6H -A KUBE-SVC-JLFZCDARX7J7XLB7 \ -m comment --comment "default/my-service:80-80 -> 10.244.0.4:80" \ -j KUBE-SEP-GGSVNIZKCFPBLJPS
核心技术 :statistic mode random 随机概率掷骰子效果 :每个请求有 50% 概率去 Pod A,50% 概率去 Pod B
Step 3: 篡改 (KUBE-SEP-XXX → DNAT) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 sudo iptables-save | grep "KUBE-SEP-4BQUF3WY4YN4VT6H" :KUBE-SEP-4BQUF3WY4YN4VT6H - [0:0] -A KUBE-SEP-4BQUF3WY4YN4VT6H \ -s 10.244.0.3/32 \ -m comment --comment "default/my-service:80-80" \ -j KUBE-MARK-MASQ -A KUBE-SEP-4BQUF3WY4YN4VT6H \ -p tcp \ -m comment --comment "default/my-service:80-80" \ -m tcp \ -j DNAT --to-destination 10.244.0.3:80
关键动作 :DNAT(Destination Network Address Translation)发生位置 :Linux 内核 Netfilter 模块改写过程 :
1 2 3 原始数据包:Dst IP = 10.101.164.222 (Service VIP) ↓ DNAT 改写 新数据包: Dst IP = 10.244.0.3 (Pod Real IP)
结果 :数据包变成发往普通 Pod 的普通包,Pod 根本不知道最初访问的是 Service IP
Step 4: 转发 1 2 3 4 5 route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 mini-cni0
流程 :
内核拿着被修改的目标 IP(10.244.0.3)查路由表
路由表指示:”去 10.244.0.0/24 网段,走 mini-cni0 网桥接口”
数据包被推到网桥,通过 ARP 找到对应 MAC 地址
封装成以太网帧,穿过 Veth Pair 虚拟网线
抵达 Pod 内部的 eth0 接口
完整数据流时序图 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 ┌─────────────┐ │ Client │ Step 1: 发出请求 │ (宿主机) │ ┌──────────────────────────┐ └──────┬──────┘ │ Dst IP: 10.101.164.222 │ │ │ (Service ClusterIP) │ ▼ └──────────────────────────┘ ┌─────────────────────────────────────────────┐ │ Linux Kernel Netfilter │ 👈 DNAT 在此发生 │ ┌───────────────────────────────────────┐ │ │ │ KUBE-SERVICES (拦截) │ │ │ │ "目标是 VIP?到我这里来!" │ │ │ └───────────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────┐ │ │ │ KUBE-SVC-XXX (负载均衡) │ │ │ │ 掷骰子:random probability 0.5 │ │ │ └───────────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────┐ │ │ │ KUBE-SEP-XXX (DNAT 篡改) │ │ │ │ Dst IP: 10.101.164.222 → 10.244.0.3 │ │ ⭐ 关键改写 │ └───────────────────────────────────────┘ │ └──────────────┬──────────────────────────────┘ │ 现在数据包目标已是 Pod IP ▼ ┌──────────────┐ │ mini-cni0 │ Step 2: 查路由表 │ (网桥) │ "去 10.244.0.x 走我这里" └──────┬───────┘ │ ▼ ┌──────────────┐ │ Veth Pair │ Step 3: 穿过虚拟网线 │ (虚拟管道) │ 像坐滑梯一样瞬间到达 └──────┬───────┘ │ ▼ ┌──────────────┐ │ Pod NS │ Step 4: Pod 接收 │ 10.244.0.3 │ "这就是发给我的!" │ Nginx │ 处理 HTTP 请求 └──────────────┘
Kube-Proxy:规则的幕后操盘手 Kube-Proxy 的工作循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (kp *KubeProxy) syncLoop() { for { event := <-kp.eventChannel switch event.Type { case ServiceAdded, ServiceUpdated: kp.onServiceChange(event.Service) case EndpointsAdded, EndpointsUpdated: kp.onEndpointsChange(event.Endpoints) } rules := kp.computeAllRules() kp.syncRulesToKernel(rules) } }
规则计算的三个阶段 Phase 1: 生成拦截规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: v1 kind: Service metadata: name: my-service spec: clusterIP: 10.101.164.222 ports: - port: 80 targetPort: 80 -A KUBE-SERVICES \ -d 10.101.164.222/32 \ -p tcp \ -m comment --comment "default/my-service:80-80 cluster IP" \ -j KUBE-SVC-JLFZCDARX7J7XLB7
Phase 2: 生成负载均衡规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 endpoints: - 10.244.0.3:80 - 10.244.0.4:80 -A KUBE-SVC-JLFZCDARX7J7XLB7 \ -m statistic --mode random --probability 0.5 \ -j KUBE-SEP-4BQUF3WY4YN4VT6H -A KUBE-SVC-JLFZCDARX7J7XLB7 \ -j KUBE-SEP-GGSVNIZKCFPBLJPS
Phase 3: 生成 DNAT 规则 1 2 3 4 5 6 7 endpoint: 10.244.0.3:80 -A KUBE-SEP-4BQUF3WY4YN4VT6H \ -p tcp \ -j DNAT --to-destination 10.244.0.3:80
批量同步到内核 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 rules := ` *KUBE-SERVICES -N KUBE-SERVICES -A KUBE-SERVICES -d 10.101.164.222/32 -p tcp -j KUBE-SVC-JLFZCDARX7J7XLB7 COMMIT *NAT -N KUBE-SVC-JLFZCDARX7J7XLB7 -A KUBE-SVC-JLFZCDARX7J7XLB7 -m statistic --mode random --probability 0.5 -j KUBE-SEP-4BQUF3WY4YN4VT6H -A KUBE-SVC-JLFZCDARX7J7XLB7 -j KUBE-SEP-GGSVNIZKCFPBLJPS -N KUBE-SEP-4BQUF3WY4YN4VT6H -A KUBE-SEP-4BQUF3WY4YN4VT6H -p tcp -j DNAT --to-destination 10.244.0.3:80 -N KUBE-SEP-GGSVNIZKCFPBLJPS -A KUBE-SEP-GGSVNIZKCFPBLJPS -p tcp -j DNAT --to-destination 10.244.0.4:80 COMMIT ` echo "$rules " | iptables-restore
进阶话题:ExternalTrafficPolicy=Local 的源地址保留 默认行为的问题 1 2 3 4 client_ip=$(curl -s http://my-service/api/client-ip) echo $client_ip
Local 模式的解决方案 1 2 3 4 5 6 kubectl patch service my-service -p '{"spec":{"externalTrafficPolicy":"Local"}}' sudo iptables-save | grep "EXTERNAL"
原理 :
只将流量转发到本机 Node 上的 Pod
避免跨节点的 SNAT 操作
保留原始 Client IP 供后端应用读取
代价 :
可能失去部分负载均衡能力(如果某些 Node 上没有 Pod)
性能对比:iptables vs IPVS Iptables 模式的瓶颈 1 2 3 4 5 6 7 8 9 kubectl scale deployment my-app --replicas=1000 sudo iptables-save | wc -l
问题 :
规则数量随 Pod 数线性增长
每次匹配都要从头遍历链表
大规模场景下延迟显著上升
IPVS 模式的优势 1 2 3 4 5 6 7 8 9 10 11 kube-proxy --proxy-mode=ipvs sudo ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.101.164.222:80 rr -> 10.244.0.3:80 Masq 1 0 0 -> 10.244.0.4:80 Masq 1 0 0
优势 :
基于哈希表查找,时间复杂度 O(1)
支持多种调度算法(rr/wlc/lc 等)
万级后端 Pod 仍能保持毫秒级响应
调度算法对比
算法
全称
适用场景
rr
Round Robin
均匀轮询,后端性能相近
wrr
Weighted RR
后端服务器权重不同
lc
Least Connection
优先选择连接数最少的
wlc
Weighted LC
带权重的最少连接
dh
Destination Hashing
相同 Client IP 固定到同一后端
总结 通过追踪 Service Proxy 的全链路,我们揭示了:
ClusterIP 的本质 - 不是真实 IP,而是内核中的一套拦截规则
四层抽象模型 - Service → Endpoints → iptables/IPVS → Pod
DNAT 魔法 - 在内核态悄无声息地改写数据包目标地址
负载均衡实现 - 利用 statistic mode random 实现概率分流
Kube-Proxy 角色 - 监听变更→计算规则→批量刷新的闭环控制器
这套机制的精妙之处在于:对上层透明,对下层解耦 。Client 无需感知后端的存在形式,Pod 也无需知道自己是通过什么方式被访问到的。一切都在 Linux 内核的 Netfilter 框架中静默完成。