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

📅 2026/8/5 13:11:30
【Kubernetes从入门到精通】第16篇:Service的负载均衡原理——kube-proxy到底干了什么
上一篇【第15篇】Service——K8s的服务发现和负载均衡下一篇【第17篇】Ingress——HTTP流量的总管家摘要上一篇文章你学会了用Service——创建个ClusterIPPod之间就能用backend-svc:8080互相调用美滋滋。但你有没有想过一个问题流量打到Service的虚拟IP10.96.100.50之后是怎么精准落到某个Pod10.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个后端Podkube-proxy就在Node的iptables里写入规则链【iptables 规则链示意图一个Service3个后端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:80802.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多个Serviceiptables规则超过一万条每次新Pod上线kube-proxy做iptables-restore都要卡2-3秒——这2-3秒里新Pod根本没流量进来。这就是iptables模式的天花板规则越多匹配越慢更新越慢。三、IPVS模式——“Hash表秒匹配”IPVSIP Virtual Server是Linux内核的一个传输层负载均衡模块专门为大规模负载均衡场景设计。它用Hash表存储转发规则查找时间复杂度O(1)。【IPVS vs iptables——底层数据结构的差异】 iptables规则链: IPVSHash表: ┌──────────────────┐ ┌────────────────┐ │ 规则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、WebSocketDestination Hashingdh按目标IP哈希需要会话亲和性Source Hashingsh按源IP哈希同一客户端始终到同一PodShortest 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 randomrr/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-appkube-proxy# kube-proxy DS会自动重建Pod新Pod用IPVS模式# 验证切换成功kubectl logs-nkube-system-lk8s-appkube-proxy|grep-iipvs# 或者看某个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的shSource 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上布下了精密的流量转发网络iptables模式默认模式通过规则链实现随机均衡。适合中小集群1000个Service规则越多性能越差IPVS模式用Hash表专用调度算法时间复杂度O(1)支持rr/lc/sh/dh等多种算法大规模集群必备切换方法改ConfigMap 重启kube-proxy一次操作永久受益会话亲和sessionAffinity和流量策略externalTrafficPolicy是控制流量行为的精细开关下一篇咱们聊Ingress——如果Service是内部电话本那Ingress就是前台接待处专门处理HTTP流量的域名路由和TLS终结。上一篇【第15篇】Service——K8s的服务发现和负载均衡下一篇【第17篇】Ingress——HTTP流量的总管家