kube-proxy 是 Kubernetes 网络层的核心组件,负责将 Service 抽象转化为内核级别的流量转发规则。IPVS 模式相比传统 iptables 模式在大规模集群中具有显著的性能优势。本文基于 Kubernetes v1.34.9 源码,从 Proxier 结构、核心同步流程到 IPVS 虚拟服务器管理,对 IPVS 模式进行全面的代码级深度解析。
一、为什么选择 IPVS 模式 在分析代码之前,先回顾一下 kube-proxy 三种模式的核心区别:
特性
userspace
iptables
IPVS
流量路径
用户空间代理
内核 netfilter
内核 IPVS
规则复杂度
O(1)
O(n) 链式遍历
O(1) 哈希查找
连接跟踪
独立实现
依赖 conntrack
内核 IPVS 表
负载均衡算法
轮询
随机
rr/lc/wlc/sh/mh 等
Session Affinity
软件实现
recent 模块
内核 persistent 标志
大规模性能
差
随规则数线性劣化
稳定
IPVS(IP Virtual Server)是 Linux 内核 LVS 子系统的核心,基于哈希表实现 O(1) 的规则查找,当 Service 数量超过 1000 时性能优势极为显著。
二、整体架构 kube-proxy IPVS 模式的核心数据流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 API Server │ Service/EndpointSlice 变更事件 ▼ ServiceChangeTracker + EndpointsChangeTracker │ 批量缓冲变更(minSyncPeriod ~ syncPeriod 之间触发) ▼ syncProxyRules() ← 核心同步函数 ├─ 更新 svcPortMap / endpointsMap ├─ 确保 kube-ipvs0 dummy 接口存在 ├─ 遍历所有 Service: │ ├─ ClusterIP → IPVS VS + 绑定 kube-ipvs0 │ ├─ ExternalIP → IPVS VS + 有条件绑定 │ ├─ LoadBalancer → IPVS VS + 防火墙 ipset │ └─ NodePort → IPVS VS(不绑定 kube-ipvs0) ├─ 同步所有 ipset 到内核 ├─ 生成并应用 iptables 规则(伪装/转发/防火墙) ├─ 清理废弃的 dummy 地址和 IPVS 服务 └─ 更新健康检查 & 指标 GracefulTerminationManager(独立 goroutine) └─ 每分钟:检查连接数归零的 RS,完成最终删除
三、Proxier 结构体解析 文件: pkg/proxy/ipvs/proxier.go:163
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 type Proxier struct { ipFamily v1.IPFamily endpointsChanges *proxy.EndpointsChangeTracker serviceChanges *proxy.ServiceChangeTracker mu sync.Mutex svcPortMap proxy.ServicePortMap endpointsMap proxy.EndpointsMap topologyLabels map [string ]string initialSync bool endpointSlicesSynced bool servicesSynced bool initialized int32 syncRunner *runner.BoundedFrequencyRunner syncPeriod time.Duration minSyncPeriod time.Duration excludeCIDRs []*net.IPNet iptables utiliptables.Interface ipvs utilipvs.Interface ipset utilipset.Interface conntrack conntrack.Interface masqueradeAll bool masqueradeMark string localDetector proxyutil.LocalTrafficDetector nodeName string nodeIP net.IP ipvsScheduler string iptablesData *bytes.Buffer natChains proxyutil.LineBuffer filterChains proxyutil.LineBuffer natRules proxyutil.LineBuffer filterRules proxyutil.LineBuffer netlinkHandle NetLinkHandle ipsetList map [string ]*IPSet nodePortAddresses *proxyutil.NodePortAddresses gracefuldeleteManager *GracefulTerminationManager serviceNoLocalEndpointsInternal sets.Set[string ] serviceNoLocalEndpointsExternal sets.Set[string ] }
BoundedFrequencyRunner — 限速同步调度 1 2 3 4 5 6 7 proxier.syncRunner = runner.NewBoundedFrequencyRunner( "sync-runner" , proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
四、17 个 IPSet 详解 文件: pkg/proxy/ipvs/ipset.go
IPVS 模式用 ipset 代替 iptables 的逐条匹配,将规则复杂度从 O(n) 降到 O(1):
IPSet 名称
类型
用途
KUBE-LOOP-BACK
hash ip:port:ip
hairpin 场景:Pod 访问自身所在 Service 的 SNAT 豁免
KUBE-CLUSTER-IP
hash ip:port
所有 ClusterIP:Port 组合,用于 SNAT 标记
KUBE-EXTERNAL-IP
hash ip:port
ExternalIP(Cluster 策略),用于 SNAT 标记
KUBE-EXTERNAL-IP-LOCAL
hash ip:port
ExternalIP(Local 策略),保留源 IP
KUBE-LOAD-BALANCER
hash ip:port
LB 入口 IP:Port,全局 LB 流量
KUBE-LOAD-BALANCER-LOCAL
hash ip:port
LB 入口(Local 策略)
KUBE-LOAD-BALANCER-FW
hash ip:port
带 sourceRanges 限制的 LB,启用防火墙过滤
KUBE-LOAD-BALANCER-SOURCE-IP
hash ip:port:ip
LB IP:Port + 允许的源 IP(逐 IP 白名单)
KUBE-LOAD-BALANCER-SOURCE-CIDR
hash ip:port:net
LB IP:Port + 允许的源 CIDR(CIDR 白名单)
KUBE-NODE-PORT-TCP
bitmap port
NodePort TCP 端口号,用于 SNAT 标记
KUBE-NODE-PORT-LOCAL-TCP
bitmap port
NodePort TCP(Local 策略)
KUBE-NODE-PORT-UDP
bitmap port
NodePort UDP 端口号
KUBE-NODE-PORT-LOCAL-UDP
bitmap port
NodePort UDP(Local 策略)
KUBE-NODE-PORT-SCTP-HASH
hash ip:port
NodePort SCTP(需要 hash,不支持 bitmap)
KUBE-NODE-PORT-LOCAL-SCTP-HASH
hash ip:port
NodePort SCTP(Local 策略)
KUBE-HEALTH-CHECK-NODE-PORT
bitmap port
健康检查 NodePort
KUBE-IPVS-IPS
hash ip
所有绑定到 kube-ipvs0 的 VIP 集合
设计思想: ipset 将”集合查找”提取到内核 hash 表,iptables 规则只需 -m set --match-set <NAME> 一条,无论 Service 增加到多少,iptables 规则数量保持常数级。
五、kube-ipvs0 Dummy 接口与 IP 绑定机制 5.1 为什么需要 Dummy 接口 IPVS 是内核级别的 NAT,当客户端连接到 ClusterIP(VIP)时,IPVS 将数据包 DNAT 到后端 Pod IP。但问题在于:
ClusterIP 是虚拟 IP,没有任何物理接口绑定它
Linux 内核收到目标为 ClusterIP 的包时,如果没有本地接口持有该 IP,会认为它是外部 IP 并直接丢弃(不触发 IPVS)
解决方案:创建 kube-ipvs0 dummy 接口,并把所有 VIP 绑定到它
绑定后内核的 PREROUTING 链会识别该包为”本地流量”,进入 INPUT 链,IPVS 钩子在 LOCAL_IN 处捕获并做 DNAT。
5.2 Dummy 接口操作代码 文件: pkg/proxy/ipvs/netlink_linux.go
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 func (h *netlinkHandle) EnsureDummyDevice(devName string ) (bool , error ) { _, err := h.LinkByName(devName) if err == nil { return true , nil } dummy := &netlink.Dummy{ LinkAttrs: netlink.LinkAttrs{Name: devName}, } return false , h.LinkAdd(dummy) } func (h *netlinkHandle) EnsureAddressBind(address, devName string ) (exist bool , err error ) { dev, _ := h.LinkByName(devName) addr := ParseIPSloppy(address) err = h.AddrAdd(dev, &netlink.Addr{IPNet: NewIPNet(addr)}) if err == unix.EEXIST { return true , nil } return false , err } func (h *netlinkHandle) UnbindAddress(address, devName string ) error { dev, _ := h.LinkByName(devName) addr := ParseIPSloppy(address) h.AddrDel(dev, &netlink.Addr{IPNet: NewIPNet(addr)}) return nil }
5.3 ARP 抑制(strictARP) 将大量 VIP 绑定到 dummy 接口后,需要防止这些 IP 响应 ARP 请求(否则会引起网络混乱):
1 2 3 sysctl -w net.ipv4.conf.all.arp_ignore=1 sysctl -w net.ipv4.conf.all.arp_announce=2
六、syncProxyRules() 全流程解析 文件: pkg/proxy/ipvs/proxier.go:866
这是整个 IPVS 模式的核心函数,每次触发时重新计算并同步所有规则到内核。
Phase 1:初始化与准备 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 func (proxier *Proxier) syncProxyRules() { proxier.mu.Lock() defer proxier.mu.Unlock() if !proxier.isInitialized() { proxier.logger.V(2 ).Info("Not syncing, proxy not fully initialized" ) return } serviceUpdates := proxier.serviceChanges.PendingChanges() endpointUpdates := proxier.endpointsChanges.PendingChanges() proxier.svcPortMap.Update(serviceUpdates) proxier.endpointsMap.Update(endpointUpdates) _, err := proxier.netlinkHandle.EnsureDummyDevice(defaultDummyDevice) for _, set := range proxier.ipsetList { set.resetEntries() } alreadyBoundAddrs, _ := proxier.netlinkHandle.GetLocalAddresses(defaultDummyDevice) nodeAddressSet, _ := proxier.nodeAddresses()
Phase 2:Service 遍历 — 主循环 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 activeIPVSServices := sets.New[string ]() activeBindAddrs := sets.New[string ]() for svcPortName, svcInfo := range proxier.svcPortMap { protocol := strings.ToLower(string (svcInfo.Protocol())) svcPortNameString := svcPortName.String() if svcInfo.ClusterIP() != nil { entry := &utilipset.Entry{ IP: svcInfo.ClusterIP().String(), Port: svcInfo.Port(), Protocol: protocol, SetType: utilipset.HashIPPort, } proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String()) serv := &utilipvs.VirtualServer{ Address: svcInfo.ClusterIP(), Port: uint16 (svcInfo.Port()), Protocol: string (svcInfo.Protocol()), Scheduler: proxier.ipvsScheduler, } if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { serv.Flags |= utilipvs.FlagPersistent serv.Timeout = uint32 (svcInfo.StickyMaxAgeSeconds()) } if proxier.ipvsScheduler == "mh" { serv.Flags |= utilipvs.FlagSourceHash } proxier.syncService(svcPortNameString, serv, true , alreadyBoundAddrs) activeBindAddrs.Insert(serv.Address.String()) proxier.syncEndpoint(svcPortName, svcInfo.InternalPolicyLocal(), serv) activeIPVSServices.Insert(serv.String()) }
Phase 3:ExternalIP、LoadBalancer、NodePort 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 for _, externalIP := range svcInfo.ExternalIPs() { if svcInfo.ExternalPolicyLocal() { proxier.ipsetList[kubeExternalIPLocalSet].activeEntries.Insert(...) } else { proxier.ipsetList[kubeExternalIPSet].activeEntries.Insert(...) } serv := &utilipvs.VirtualServer{Address: externalIP, ...} bindAddr := !nodeAddressSet.Has(externalIP.String()) proxier.syncService(svcPortNameString, serv, bindAddr, alreadyBoundAddrs) proxier.syncEndpoint(svcPortName, svcInfo.ExternalPolicyLocal(), serv) } for _, ingress := range svcInfo.LoadBalancerVIPs() { proxier.ipsetList[kubeLoadBalancerSet].activeEntries.Insert(...) if svcInfo.ExternalPolicyLocal() { proxier.ipsetList[kubeLoadBalancerLocalSet].activeEntries.Insert(...) } if len (svcInfo.LoadBalancerSourceRanges()) > 0 { proxier.ipsetList[kubeLoadBalancerFWSet].activeEntries.Insert(...) for _, cidr := range svcInfo.LoadBalancerSourceRanges() { proxier.ipsetList[kubeLoadBalancerSourceCIDRSet].activeEntries.Insert(...) } } serv := &utilipvs.VirtualServer{Address: ingress, ...} bindAddr := !nodeAddressSet.Has(ingress.String()) proxier.syncService(svcPortNameString, serv, bindAddr, alreadyBoundAddrs) proxier.syncEndpoint(svcPortName, svcInfo.ExternalPolicyLocal(), serv) } if svcInfo.NodePort() != 0 { switch protocol { case "tcp" : proxier.ipsetList[kubeNodePortSetTCP].activeEntries.Insert(...) if svcInfo.ExternalPolicyLocal() { proxier.ipsetList[kubeNodePortLocalSetTCP].activeEntries.Insert(...) } case "udp" : proxier.ipsetList[kubeNodePortSetUDP].activeEntries.Insert(...) case "sctp" : for _, nodeIP := range nodeIPs { proxier.ipsetList[kubeNodePortSetSCTP].activeEntries.Insert( fmt.Sprintf("%s,%s:%d" , nodeIP, protocol, svcInfo.NodePort())) } } for _, nodeIP := range nodeIPs { serv := &utilipvs.VirtualServer{ Address: nodeIP, Port: uint16 (svcInfo.NodePort()), Protocol: string (svcInfo.Protocol()), Scheduler: proxier.ipvsScheduler, } proxier.syncService(svcPortNameString, serv, false , nil ) proxier.syncEndpoint(svcPortName, svcInfo.ExternalPolicyLocal(), serv) activeIPVSServices.Insert(serv.String()) } } }
Phase 4:IPSet 同步到内核 1 2 3 4 5 6 7 proxier.ipsetList[kubeIPVSIPsSet].activeEntries = activeBindAddrs for _, set := range proxier.ipsetList { set.syncIPSetEntries() }
Phase 5:iptables 规则生成 IPVS 本身只做 DNAT(目标地址转换),SNAT(源地址伪装)、防火墙过滤等仍需 iptables。
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 -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst \ -m addrtype ! --src-type LOCAL -j KUBE-MARK-MASQ -A KUBE-SERVICES -m set --match-set KUBE-LOAD-BALANCER dst,dst -j KUBE-LOAD-BALANCER -A KUBE-LOAD-BALANCER -j KUBE-MARK-MASQ -A KUBE-NODE-PORT -p tcp -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ -A KUBE-NODE-PORT -p tcp -m set --match-set KUBE-NODE-PORT-LOCAL-TCP dst -j RETURN -A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN -A KUBE-POSTROUTING -j MARK --xor-mark 0x4000 -A KUBE-POSTROUTING -j MASQUERADE --random-fully -A KUBE-PROXY-FIREWALL -m set --match-set KUBE-LOAD-BALANCER-FW dst,dst \ -j KUBE-SOURCE-RANGES-FIREWALL -A KUBE-SOURCE-RANGES-FIREWALL \ -m set --match-set KUBE-LOAD-BALANCER-SOURCE-CIDR dst,dst,src -j RETURN -A KUBE-SOURCE-RANGES-FIREWALL -j DROP -A KUBE-IPVS-FILTER -m set --match-set KUBE-LOAD-BALANCER dst,dst -j RETURN -A KUBE-IPVS-FILTER -m set --match-set KUBE-CLUSTER-IP dst,dst -j RETURN -A KUBE-IPVS-FILTER -m set --match-set KUBE-IPVS-IPS dst -j REJECT -A KUBE-FORWARD -m mark --mark 0x4000/0x4000 -j ACCEPT -A KUBE-FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Phase 6:清理废弃资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for addr := range alreadyBoundAddrs.Difference(activeBindAddrs) { proxier.netlinkHandle.UnbindAddress(addr, defaultDummyDevice) } currentIPVSServices, _ := proxier.ipvs.GetVirtualServers() for _, vs := range currentIPVSServices { if activeIPVSServices.Has(vs.String()) { continue } if proxier.excludeCIDRs.Has(vs.Address) { continue } proxier.ipvs.DeleteVirtualServer(vs) }
七、syncService():IPVS 虚拟服务器的创建与更新 文件: pkg/proxy/ipvs/proxier.go:1754
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 func (proxier *Proxier) syncService(svcName string , vs *utilipvs.VirtualServer, bindAddr bool , alreadyBoundAddrs sets.Set[string ]) error { appliedVS, _ := proxier.ipvs.GetVirtualServer(vs) if appliedVS == nil { proxier.ipvs.AddVirtualServer(vs) } else if !appliedVS.Equal(vs) { proxier.ipvs.UpdateVirtualServer(vs) } if bindAddr { if alreadyBoundAddrs != nil && alreadyBoundAddrs.Has(vs.Address.String()) { return nil } _, err := proxier.netlinkHandle.EnsureAddressBind( vs.Address.String(), defaultDummyDevice) return err } return nil }
不同 Service 类型的 bindAddr 决策:
Service 类型
bindAddr
原因
ClusterIP
true
VIP 无物理接口,必须绑定 dummy
ExternalIP
!nodeAddressSet.Has(ip)
如果 externalIP 已在真实网卡,不重复绑定
LoadBalancer
!nodeAddressSet.Has(ip)
同上
NodePort
false
使用节点真实 IP,已有物理接口
八、syncEndpoint():Endpoint 到 Real Server 的映射 文件: pkg/proxy/ipvs/proxier.go:1794
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func (proxier *Proxier) syncEndpoint(svcPortName proxy.ServicePortName, onlyNodeLocalEndpoints bool , vs *utilipvs.VirtualServer) error { curDests, _ := proxier.ipvs.GetRealServers(vs) curEndpoints := sets.New[string ]() for _, d := range curDests { curEndpoints.Insert(d.String()) } endpoints := proxier.endpointsMap[svcPortName] if onlyNodeLocalEndpoints { clusterEPs, localEPs, _, hasAny := proxy.CategorizeEndpoints( endpoints, svcInfo, proxier.nodeName, proxier.topologyLabels) if len (localEPs) > 0 { endpoints = localEPs } else { endpoints = clusterEPs if hasAny { proxier.serviceNoLocalEndpointsExternal.Insert(svcPortName.String()) } } } newEndpoints := sets.New[string ]() for _, ep := range endpoints { newEndpoints.Insert(ep.String()) } for _, ep := range newEndpoints.Difference(curEndpoints).UnsortedList() { ip, port, _ := net.SplitHostPort(ep) newDest := &utilipvs.RealServer{ Address: net.ParseIP(ip), Port: uint16 (portInt), Weight: 1 , } uniqueRS := GetUniqueRSName(vs, newDest) if proxier.gracefuldeleteManager.InTerminationList(uniqueRS) { proxier.gracefuldeleteManager.MoveRSOutofGracefulDeleteList(uniqueRS) } else { proxier.ipvs.AddRealServer(vs, newDest) } } for _, ep := range curEndpoints.Difference(newEndpoints).UnsortedList() { if proxier.gracefuldeleteManager.InTerminationList(vs.String() + "/" + ep) { continue } ip, port, _ := net.SplitHostPort(ep) delDest := &utilipvs.RealServer{ Address: net.ParseIP(ip), Port: uint16 (portInt), } proxier.gracefuldeleteManager.GracefulDeleteRS(vs, delDest) } return nil }
九、优雅终止机制(GracefulTerminationManager) 文件: pkg/proxy/ipvs/graceful_termination.go
当 Pod 被删除时,直接删除 IPVS Real Server 会导致已建立的 TCP 连接被 RST 终止。优雅终止机制通过 weight=0 + 等待连接耗尽来解决这个问题。
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 43 44 45 46 47 48 49 type GracefulTerminationManager struct { rsList graceTerminateRSList ipvs utilipvs.Interface } func (m *GracefulTerminationManager) GracefulDeleteRS( vs *utilipvs.VirtualServer, rs *utilipvs.RealServer) error { deleted, err := m.deleteRsFunc(&listItem{VirtualServer: vs, RealServer: rs}) if deleted { return nil } rs.Weight = 0 m.ipvs.UpdateRealServer(vs, rs) m.rsList.add(&listItem{VirtualServer: vs, RealServer: rs}) return err } func (m *GracefulTerminationManager) deleteRsFunc(rsToDelete *listItem) (bool , error ) { rss, _ := m.ipvs.GetRealServers(rsToDelete.VirtualServer) for _, rs := range rss { if !rsToDelete.RealServer.Equal(rs) { continue } if !IsRsGracefulTerminationNeeded(rsToDelete.VirtualServer.Protocol) { return m.ipvs.DeleteRealServer(rsToDelete.VirtualServer, rs) == nil , nil } if rs.ActiveConn+rs.InactiveConn == 0 { return m.ipvs.DeleteRealServer(rsToDelete.VirtualServer, rs) == nil , nil } return false , nil } return true , nil } func (m *GracefulTerminationManager) Run() { go wait.Until(m.tryDeleteRs, 1 *time.Minute, wait.NeverStop) }
优雅终止的完整流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Pod 被删除 ↓ EndpointSlice 更新(endpoint 从 ready 变为 terminating) ↓ syncEndpoint() 检测到 endpoint 不在期望列表 ↓ GracefulDeleteRS(vs, rs) ├─ UDP/SCTP: 立即删除 Real Server └─ TCP: 设置 weight=0,加入 rsList ↓ (已有连接继续使用此 RS 直到完成,新连接不会分发到此 RS) ↓ GracefulTerminationManager.tryDeleteRs() [每1分钟] ├─ ActiveConn + InactiveConn > 0: 继续等待 └─ ActiveConn + InactiveConn = 0: 删除 Real Server
十、Session Affinity(ClientIP 粘性会话) IPVS 内核模块原生支持 persistent 会话,无需 iptables 的 recent 模块辅助:
1 2 3 4 5 6 7 8 9 serv := &utilipvs.VirtualServer{ Address: clusterIP, Port: uint16 (port), Protocol: "TCP" , Scheduler: "rr" , Flags: utilipvs.FlagPersistent, Timeout: 10800 , }
工作原理:
IPVS 内核模块为每个(客户端 IP, VIP, 端口)三元组维护连接映射
同一客户端在 Timeout 时间内的所有连接始终路由到同一个 Real Server
每次新连接会刷新超时计时器
完全在内核空间实现,延迟极低
hairpin 问题处理:
当 Pod 通过 ClusterIP 访问自身所在 Service 时,IPVS DNAT 将目标 IP 改为 Pod IP,但源 IP 仍为 Pod IP,内核会认为这是本地环回流量,不走 SNAT。KUBE-LOOP-BACK ipset 专门处理这种场景,强制做 SNAT 使数据包能正确回环。
十一、初始化时的内核参数配置 文件: pkg/proxy/ipvs/proxier.go:283
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sysctls := map [string ]string { "net/ipv4/vs/conntrack" : "1" , "net/ipv4/vs/conn_reuse_mode" : "0" , "net/ipv4/vs/expire_nodest_conn" : "1" , "net/ipv4/vs/expire_quiescent_template" : "1" , "net/ipv4/ip_forward" : "1" , "net/ipv4/conf/all/arp_ignore" : "1" , "net/ipv4/conf/all/arp_announce" : "2" , }
十二、Service 类型与 IPVS 映射汇总
Service 类型
VIP 来源
绑定 kube-ipvs0
主要 IPSet
externalTrafficPolicy 影响
ClusterIP
spec.clusterIP
是
KUBE-CLUSTER-IP
internalTrafficPolicy 影响端点过滤
ExternalIP
spec.externalIPs[]
仅当 IP 不在真实网卡
KUBE-EXTERNAL-IP / LOCAL
Local → 保留源 IP,仅本节点端点
NodePort
节点所有 IP
否
KUBE-NODE-PORT-{TCP/UDP/SCTP}
Local → 保留源 IP,仅本节点端点
LoadBalancer
status.loadBalancer.ingress[]
仅当 IP 不在真实网卡
KUBE-LOAD-BALANCER + 防火墙 set
Local → 保留源 IP,仅本节点端点
十三、与 iptables 模式的关键差异总结 1 2 3 4 5 6 7 8 9 10 11 iptables 模式: 每个 Service endpoint 对应若干 iptables 规则 匹配方式:链式线性遍历,O(n) 复杂度 1000 个 Service → 数万条 iptables 规则 SNAT + DNAT 均由 iptables/netfilter 完成 IPVS 模式: 每个 Service → 1 个 IPVS Virtual Server(内核 hash 表) 每个 Endpoint → 1 个 IPVS Real Server(O(1) 查找) DNAT 由 IPVS 完成,SNAT/防火墙仍需少量固定 iptables 规则 + ipset 1000 个 Service → iptables 规则数量固定(约 20~30 条)+ 17 个 ipset
十四、调试命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ipvsadm -Ln ipvsadm -Ln --stats | grep -A5 "10.96.0.1:443" ip addr show kube-ipvs0 ipset list KUBE-CLUSTER-IP ipset list KUBE-NODE-PORT-TCP ipvsadm -Ln | grep "^ -> " | grep " 0 " iptables -t nat -L KUBE-POSTROUTING -v -n
十五、总结 kube-proxy IPVS 模式的设计精妙之处在于:
职责分离 :IPVS 负责高效 DNAT(内核 hash 表,O(1)),iptables 负责 SNAT 和防火墙(规则数量固定,与 Service 数量无关)
dummy 接口技巧 :通过 kube-ipvs0 让内核将所有 VIP 识别为本地地址,触发 IPVS 处理路径
ipset 批量匹配 :将 O(n) 的 iptables 规则匹配压缩为 O(1) 的 hash 集合查找
优雅终止 :weight=0 + 连接计数轮询,避免已有 TCP 连接被强制中断
增量同步 :BoundedFrequencyRunner 限速 + 变更追踪器 (ChangeTracker) 保证同步的正确性和效率
对于超过几百个 Service 的生产集群,IPVS 模式是 kube-proxy 的首选配置。