【Kubernetes从入门到精通】第16篇:Service的负载均衡原理——kube-proxy到底干了什么

上一篇【第15篇】Service——K8s的服务发现和负载均衡
下一篇【第17篇】Ingress——HTTP流量的"总管家"


摘要

上一篇文章你学会了用Service——创建个ClusterIP,Pod之间就能用backend-svc:8080互相调用,美滋滋。但你有没有想过一个问题:流量打到Service的虚拟IP(10.96.100.50)之后,是怎么精准落到某个Pod(10.244.1.5:8080)的?这个IP转换是谁做的?负载均衡又是怎么实现的?

答案是kube-proxy——藏在每个K8s Node上的"流量调度员"。它不创造流量、不承载流量,只是在每个Node上配置转发规则。默认用iptables搞随机均衡,大集群换成IPVS(性能高出一大截)。本文就把kube-proxy的底裤扒开,给你看清iptables模式下那堆规则链到底是怎么生成的,IPVS为什么比iptables快那么多,以及怎么平滑切换。


一、kube-proxy是干什么的——“翻译官+调度员”

kube-proxy跑在K8s集群的每个Node上,以DaemonSet形式部署。它不承载数据流量,只负责在节点上"写规则"。

【kube-proxy 的角色——不是司机,是导航员】 ┌─────────────────────────────────────────────────────┐ │ kube-proxy │ │ │ │ "我只负责设置规则,不碰数据包" │ │ │ │ Service创建 kube-proxy监听到 写转发规则到 │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ├──────┐ ┌─────────┐ ┌───────────┐ │ │ │API │─────►│kube- │──────►│ iptables │ │ │ │Server│ │proxy │ │ 或 IPVS │ │ │ └──────┘ └─────────┘ └─────┬─────┘ │ │ │ │ │ ▼ │ │ 数据包来了就按规则转发 │ └─────────────────────────────────────────────────────┘ 数据流路径(kube-proxy不参与): 客户端Pod → Service ClusterIP → [iptables/IPVS规则] → 后端Pod ↑ kube-proxy写的规则 数据包自己走的,不经过kube-proxy进程

要点:kube-proxy本身不转发流量——它只负责往内核里写转发规则。数据包进入Node后,Linux内核根据kube-proxy写的iptables/IPVS规则自己做转发。这意味着kube-proxy挂了,已经写好的规则还在(但规则不会更新了)。


二、iptables模式——“随机均衡的规则链工厂”

iptables是Linux内核自带的防火墙工具,kube-proxy用它来实现Service的负载均衡。来,咱们一步步看它是怎么工作的。

2.1 原理:一条Service对应一堆iptables规则

每当创建一个Service(假设叫backend-svc,有3个后端Pod),kube-proxy就在Node的iptables里写入规则链:

【iptables 规则链示意图(一个Service,3个后端Pod)】 客户端发包:目标地址 10.96.100.50:8080 │ ▼ ┌──────────────────────────────────────────────┐ │ iptables PREROUTING 链 │ │ → 跳到 KUBE-SERVICES 链 │ └──────────────┬───────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ KUBE-SERVICES 链 │ │ "目的IP是 10.96.100.50,目的端口是 8080?" │ │ YES → 跳到 KUBE-SVC-XXXXX 链 │ └──────────────┬───────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ KUBE-SVC-XXXXX 链(负载均衡规则) │ │ │ │ 规则1: 33%概率 → 跳到 KUBE-SEP-POD1 │ │ 规则2: 50%概率 → 跳到 KUBE-SEP-POD2 │ │ 规则3: 否则 → 跳到 KUBE-SEP-POD3 │ │ │ │ 用的是统计模块(statistic)的random模式 │ └──────┬───────────────┬───────────────┬───────┘ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │KUBE-SEP-POD1 │ │KUBE-SEP-POD2 │ │KUBE-SEP-POD3 │ │DNAT到 │ │DNAT到 │ │DNAT到 │ │10.244.1.5:8080│ │10.244.2.3:8080│ │10.244.3.7:8080│ └──────────────┘ └──────────────┘ └──────────────┘
# 在Node上查看kube-proxy生成的iptables规则# 1. 看Service相关的规则链sudoiptables-tnat-LKUBE-SERVICES-n|grepbackend-svc# 2. 看负载均衡规则链sudoiptables-tnat-LKUBE-SVC-XXXXX-n# 3. 看具体到某个Pod的DNAT规则sudoiptables-tnat-LKUBE-SEP-POD1-n# 输出类似:# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.244.1.5:8080

2.2 iptables的问题是——规则多了性能爆炸

iptables的一个致命问题是:规则是按顺序逐条匹配的,时间复杂度O(n)

【iptables的"线性遍历"问题】 Service数量 规则总数 每次匹配耗时 ──────────────────────────────────────────────── 1个Service ≈ 5条规则 ≈ 微秒级 ✅ 没啥感觉 10个Service ≈ 50条规则 ≈ 几十微秒 ✅ 还行 100个Service ≈ 500条规则 ≈ 百微秒级 ⚠️ 开始慢了 1000个Service ≈ 5000条规则 ≈ 毫秒级 ❌ 明显卡了 5000个Service ≈ 25000条规则 ≈ 几十毫秒 💀 不行了 每个Service会产生: - 1条 KUBE-SVC 规则 - N条 KUBE-SEP 规则(N = 后端Pod数) - 外加匹配/跳转规则 2万+条规则线性遍历 → iptables-restore都要跑几秒

要点:我见过一个生产集群跑了2000多个Service,iptables规则超过一万条,每次新Pod上线kube-proxy做iptables-restore都要卡2-3秒——这2-3秒里新Pod根本没流量进来。这就是iptables模式的"天花板":规则越多,匹配越慢,更新越慢


三、IPVS模式——“Hash表秒匹配”

IPVS(IP Virtual Server)是Linux内核的一个传输层负载均衡模块,专门为大规模负载均衡场景设计。它用Hash表存储转发规则,查找时间复杂度O(1)。

【IPVS vs iptables——底层数据结构的差异】 iptables(规则链): IPVS(Hash表): ┌──────────────────┐ ┌────────────────┐ │ 规则1: 如果是svc-A│ │ Key: svc-A │ │ 规则2: 如果是svc-B│ │ ┌──────────┐ │ │ 规则3: 如果是svc-A│ │ │ Hash │ │ │ 规则4: 如果是svc-C│ │ │ 直接定位 │ │ │ 规则5: 如果是svc-B│ │ └──────────┘ │ │ ... │ │ │ │ 规则10000: ... │ │ Key: svc-B │ │ │ │ ┌──────────┐ │ │ 查找方式:逐条遍历 │ │ │ Hash │ │ │ 时间复杂度:O(n) │ │ │ 直接定位 │ │ │ │ │ └──────────┘ │ │ 规则越多越慢 │ │ │ └──────────────────┘ │ 查找方式:Hash │ │ 时间复杂度:O(1) │ │ │ │ 规则再多也几乎 │ │ 不影响查找速度 │ └────────────────┘

3.1 IPVS的调度算法——不止随机

IPVS支持多种调度算法,每种适用不同场景:

算法缩写含义适用场景
Round Robinrr轮询,一人一次通用,后端Pod性能差不多
Least Connectionlc最少连接,谁清闲找谁长连接场景(gRPC、WebSocket)
Destination Hashingdh按目标IP哈希需要会话亲和性
Source Hashingsh按源IP哈希同一客户端始终到同一Pod
Shortest Expected Delaysed最短预期延迟后端性能不均衡
Never Queuenq不排队对延迟极敏感的场景
Weighted RRwrr加权轮询Pod配置不同权重的场景
Weighted LCwlc加权最少连接带权重的最少连接
# 查看当前kube-proxy的配置模式kubectl get configmap kube-proxy-nkube-system-oyaml|grepmode# mode: "" 或 "iptables"(默认)# 查看IPVS的调度算法kubectl get configmap kube-proxy-nkube-system-oyaml|grepscheduler
【各种调度算法决策图】 请求到达 IPVS │ ┌────┴────────────────────────────┐ │ 需要同一客户端始终到一个Pod吗? │ └────┬───────────────┬────────────┘ │ 是 │ 否 ▼ ▼ 用 sh ❯ "后端Pod性能一样吗?" 源IP哈希 │ ┌──────┴──────┐ │ 一样 │ 不一样 ▼ ▼ "请求类型?" 用 wrr/wlc ❯ │ 加权算法 ┌──────┴──────┐ │ 短连接 │ 长连接(gRPC等) ▼ ▼ 用 rr ❯ 用 lc ❯ 轮询 最少连接

3.2 IPVS为什么比iptables好——全方位对比

维度iptablesIPVS
实现方式逐条规则链Hash表
查找复杂度O(n),n=规则数O(1),常数时间
规模上限~1000个Service开始变慢数万个Service无压力
调度算法随机(statistic random)rr/lc/dh/sh/sed/nq/wrr/wlc
后端健康检查❌ 无✅ 主动TCP健康检查
连接追踪依赖conntrack表自己维护连接表
更新效率全量iptables-restore增量更新,更快
内核模块内置需加载ip_vs内核模块
生产成熟度久经考验成熟(K8s 1.11 GA)

要点:如果你的集群Service不多(<100个),iptables够用,不用折腾。但如果你在跑微服务架构,几百上千个Service是家常便饭,切换IPVS是性价比最高的优化——一键切换,性能飙升,不用改任何应用代码


四、iPvs vs iptables——深入底层数据流

来对比一下两者在转发数据包时的完整路径:

【iptables 数据流路径】 Client Pod (10.244.1.10) │ │ 发请求到 10.96.100.50:8080 (Service ClusterIP) ▼ ┌─────────────────────────────────────────┐ │ Node iptables (nat表) │ │ │ │ PREROUTING → KUBE-SERVICES │ │ → KUBE-SVC-XXXXX (逐个匹配规则) │ │ → KUBE-SEP-POD2 (匹配到!) │ │ → DNAT 到 10.244.2.3:8080 │ │ │ │ 全程在规则链中线性搜索 │ └─────────────────────────────────────────┘ │ ▼ 转发到 Backend Pod (10.244.2.3:8080) 【IPVS 数据流路径】 Client Pod (10.244.1.10) │ │ 发请求到 10.96.100.50:8080 (Service ClusterIP) ▼ ┌─────────────────────────────────────────┐ │ Node iptables (nat表) —— 只剩一条规则 │ │ │ │ PREROUTING → KUBE-SERVICES │ │ → "去IPVS查吧!" (跳转一次) │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ IPVS Hash表 │ │ │ │ hash(10.96.100.50:8080) → 后端服务器列表 │ │ 调度算法选一台 (如 rr 轮询) │ │ → 转发到 10.244.2.3:8080 │ │ │ │ Hash查找,O(1),跟Service数量无关 │ └─────────────────────────────────────────┘ │ ▼ 转发到 Backend Pod (10.244.2.3:8080)

五、如何切换到IPVS——三步搞定

# Step 1: 确认Node已加载IPVS内核模块lsmod|grepip_vs# ip_vs_rr# ip_vs# nf_conntrack# 如果没加载,手动加载sudomodprobe ip_vssudomodprobe ip_vs_rrsudomodprobe ip_vs_wrrsudomodprobe ip_vs_sh# Step 2: 编辑kube-proxy ConfigMap,切换到IPVS模式kubectl edit configmap kube-proxy-nkube-system# 找到 mode 字段,改为 ipvs:# mode: "ipvs"# 同时指定调度算法(可选,默认rr)# scheduler: "lc" # 用最少连接算法# Step 3: 重启所有kube-proxy Podkubectl delete pod-nkube-system-lk8s-app=kube-proxy# kube-proxy DS会自动重建Pod,新Pod用IPVS模式# 验证切换成功kubectl logs-nkube-system-lk8s-app=kube-proxy|grep-i"ipvs"# 或者看某个Node上的IPVS规则sudoipvsadm-Ln# IP Virtual Server version 1.2.1# Prot LocalAddress:Port Scheduler Flags# -> RemoteAddress:Port Forward Weight ActiveConn InActConn# TCP 10.96.100.50:8080 rr# -> 10.244.1.5:8080 Masq 1 0 0# -> 10.244.2.3:8080 Masq 1 0 0# -> 10.244.3.7:8080 Masq 1 0 0

要点:切换IPVS需要Node上有ip_vs内核模块。新版本Linux内核默认自带了,但如果你用的是定制过的精简内核(比如某些云厂商的镜像),可能要额外安装ipvsadm包。切换过程kube-proxy重建期间,已建立的连接不受影响(因为旧iptables规则还在),新连接会在1-2秒内开始走IPVS。

优化建议:配合调整参数

# kube-proxy ConfigMap 中的优化参数apiVersion:v1kind:ConfigMapmetadata:name:kube-proxynamespace:kube-systemdata:config.conf:|apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: ipvs # IPVS模式 ipvs: scheduler: lc # 最少连接算法(长连接场景最优) strictARP: false tcpTimeout: 0 tcpFinTimeout: 0 udpTimeout: 0 iptables: masqueradeAll: false # 在大集群中减少API请求频率 clientConnection: qps: 100 burst: 200

六、会话亲和性——“同一个用户始终到同一个Pod”

有些场景你希望同一客户端的请求始终打到同一个Pod——比如WebSocket连接、有状态应用。kube-proxy通过sessionAffinity实现:

apiVersion:v1kind:Servicemetadata:name:sticky-svcspec:selector:app:myappsessionAffinity:ClientIP# 基于客户端IP的会话亲和sessionAffinityConfig:clientIP:timeoutSeconds:10800# 3小时后超时ports:-port:8080
【会话亲和性——ClientIP模式】 客户端 A (IP: 1.2.3.4) 客户端 B (IP: 5.6.7.8) │ │ │ 第一次请求 │ 第一次请求 ▼ ▼ ┌─────────┐ ┌─────────┐ │ Pod-1 │ ← IPVS/iptables记住 │ Pod-2 │ ← 另一条规则 └─────────┘ "1.2.3.4 → Pod-1" └─────────┘ │ 后续请求 │ 后续请求 ▼ ▼ ┌─────────┐ ┌─────────┐ │ Pod-1 │ ← 还是Pod-1! │ Pod-2 │ ← 还是Pod-2! └─────────┘ └─────────┘

要点sessionAffinity: ClientIP在iptables和IPVS模式下都支持。但它有个局限——只认客户端IP,如果客户端在NAT后面(比如公司出口IP对所有员工一样),就起不到"每用户独立"的效果。IPVS的sh(Source Hashing)算法本质上也是做这个,但粒度更灵活。


七、Service的externalTrafficPolicy——避免"二次跳转"

当Service类型是NodePort或LoadBalancer时,有一个让人头疼的问题:流量打到Node-3的NodePort上,但Pod在Node-1上,流量要跨Node转发一次——这叫"额外一跳"。

【externalTrafficPolicy: Cluster(默认)——有额外跳转】 外部请求 → Node-3:30080 │ │ Pod不在Node-3上... │ iptables规则:随机挑一个Pod! ▼ 可能选了 Node-1 上的 Pod │ │ 跨Node转发(额外一跳!) ▼ Node-1: Pod ← 源IP被SNAT成Node-3的IP(丢失了真实客户端IP) 【externalTrafficPolicy: Local——无跳转,保留源IP】 外部请求 → Node-3:30080 │ │ Pod不在本Node?直接丢弃,不跨Node转发 ├── Node-3没有Pod → 连接失败 外部请求 → Node-1:30080 │ │ Pod在本Node! ▼ Node-1: Pod ← 源IP是真实客户端IP!✅
apiVersion:v1kind:Servicemetadata:name:backend-nodeportspec:type:NodePortexternalTrafficPolicy:Local# 只用本Node的Pod,保留源IPselector:app:backendports:-port:8080nodePort:30080
策略跨Node转发源IP保留负载均衡适用场景
Cluster(默认)✅ 允许❌ 丢失(SNAT)均匀不关心源IP的通用服务
Local❌ 不允许✅ 保留可能不均衡需要源IP做访问控制/日志

本篇小结

kube-proxy是K8s Service机制的"幕后英雄"——它不转发数据,但在每个Node上布下了精密的流量转发网络:

  1. iptables模式:默认模式,通过规则链实现随机均衡。适合中小集群(<1000个Service),规则越多性能越差
  2. IPVS模式:用Hash表+专用调度算法,时间复杂度O(1),支持rr/lc/sh/dh等多种算法,大规模集群必备
  3. 切换方法:改ConfigMap + 重启kube-proxy,一次操作,永久受益
  4. 会话亲和(sessionAffinity)和流量策略(externalTrafficPolicy)是控制流量行为的精细开关

下一篇咱们聊Ingress——如果Service是"内部电话本",那Ingress就是"前台接待处",专门处理HTTP流量的域名路由和TLS终结。


上一篇【第15篇】Service——K8s的服务发现和负载均衡
下一篇【第17篇】Ingress——HTTP流量的"总管家"