kube-proxy IPVS 模式深度解析(基于 Kubernetes v1.34.9 源码)

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 {
// IP 协议族(IPv4 / IPv6)
ipFamily v1.IPFamily

// 变更追踪器(来自 Informer 事件)
endpointsChanges *proxy.EndpointsChangeTracker
serviceChanges *proxy.ServiceChangeTracker

mu sync.Mutex
svcPortMap proxy.ServicePortMap // 当前 Service 快照
endpointsMap proxy.EndpointsMap // 当前 Endpoint 快照
topologyLabels map[string]string // 节点拓扑标签(用于 topology-aware routing)

// 初始化状态
initialSync bool
endpointSlicesSynced bool
servicesSynced bool
initialized int32

// 同步调度器(限速)
syncRunner *runner.BoundedFrequencyRunner
syncPeriod time.Duration // 最长同步间隔(默认 30s)
minSyncPeriod time.Duration // 最短同步间隔(默认 1s)

// 排除的 CIDR(这些 IP 的 IPVS 服务不会被 kube-proxy 管理)
excludeCIDRs []*net.IPNet

// 核心内核接口
iptables utiliptables.Interface // iptables 操作
ipvs utilipvs.Interface // IPVS netlink 接口
ipset utilipset.Interface // ipset 操作
conntrack conntrack.Interface // conntrack 清理

// SNAT 配置
masqueradeAll bool
masqueradeMark string // e.g. "0x4000"

// 节点信息
localDetector proxyutil.LocalTrafficDetector
nodeName string
nodeIP net.IP

// IPVS 负载均衡算法(默认 "rr")
ipvsScheduler string

// 内存缓冲(避免频繁内存分配)
iptablesData *bytes.Buffer
natChains proxyutil.LineBuffer
filterChains proxyutil.LineBuffer
natRules proxyutil.LineBuffer
filterRules proxyutil.LineBuffer

// netlink 句柄(用于操作 kube-ipvs0 接口)
netlinkHandle NetLinkHandle

// 17 个 ipset 的管理映射
ipsetList map[string]*IPSet

// NodePort 地址绑定策略
nodePortAddresses *proxyutil.NodePortAddresses

// 优雅终止管理器
gracefuldeleteManager *GracefulTerminationManager

// 无本地端点统计(用于 metrics)
serviceNoLocalEndpointsInternal sets.Set[string]
serviceNoLocalEndpointsExternal sets.Set[string]
}

BoundedFrequencyRunner — 限速同步调度

1
2
3
4
5
6
7
// syncRunner 控制 syncProxyRules 的触发频率:
// - 事件触发后至少等待 minSyncPeriod(1s)才执行
// - 最长等待 syncPeriod(30s)后强制执行
// 避免 API Server 事件风暴时频繁重算所有规则
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。但问题在于:

  1. ClusterIP 是虚拟 IP,没有任何物理接口绑定它
  2. Linux 内核收到目标为 ClusterIP 的包时,如果没有本地接口持有该 IP,会认为它是外部 IP 并直接丢弃(不触发 IPVS)
  3. 解决方案:创建 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
// 创建 kube-ipvs0 dummy 接口
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)
}

// 将 VIP 绑定到 kube-ipvs0
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)})
// 忽略 ENXIO(地址已不存在)
return nil
}

5.3 ARP 抑制(strictARP)

将大量 VIP 绑定到 dummy 接口后,需要防止这些 IP 响应 ARP 请求(否则会引起网络混乱):

1
2
3
# kube-proxy 在 IPVS 模式下设置(strictARP=true):
sysctl -w net.ipv4.conf.all.arp_ignore=1 # 只响应目标 IP 在入包接口上的 ARP
sysctl -w net.ipv4.conf.all.arp_announce=2 # ARP 回复使用最优源 IP(避免暴露 VIP)

六、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
}

// 从变更追踪器拉取增量更新,合并到 svcPortMap / endpointsMap
serviceUpdates := proxier.serviceChanges.PendingChanges()
endpointUpdates := proxier.endpointsChanges.PendingChanges()
proxier.svcPortMap.Update(serviceUpdates)
proxier.endpointsMap.Update(endpointUpdates)

// 确保 kube-ipvs0 接口存在
_, err := proxier.netlinkHandle.EnsureDummyDevice(defaultDummyDevice)

// 重置所有 ipset 的 activeEntries(每轮重新计算)
for _, set := range proxier.ipsetList {
set.resetEntries()
}

// 获取已绑定到 kube-ipvs0 的地址集合(用于增量 bind/unbind)
alreadyBoundAddrs, _ := proxier.netlinkHandle.GetLocalAddresses(defaultDummyDevice)

// 获取节点所有真实网络接口地址(排除 kube-ipvs0)
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]()  // 本轮有效的 IPVS VS
activeBindAddrs := sets.New[string]() // 本轮需要绑定的地址

for svcPortName, svcInfo := range proxier.svcPortMap {
protocol := strings.ToLower(string(svcInfo.Protocol()))
svcPortNameString := svcPortName.String()

// ─── A. ClusterIP ────────────────────────────────────────────
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,
}
// Session Affinity(ClientIP sticky)
if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
serv.Flags |= utilipvs.FlagPersistent
serv.Timeout = uint32(svcInfo.StickyMaxAgeSeconds())
}
// source hash 调度器(mh)支持
if proxier.ipvsScheduler == "mh" {
serv.Flags |= utilipvs.FlagSourceHash
}

// 创建/更新 IPVS VS,并绑定 VIP 到 kube-ipvs0(bindAddr=true)
proxier.syncService(svcPortNameString, serv, true, alreadyBoundAddrs)
activeBindAddrs.Insert(serv.Address.String())

// 同步 Endpoints → IPVS Real Servers
// InternalPolicyLocal=true 时只用本节点 endpoint
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
    // ─── B. ExternalIP ───────────────────────────────────────────
for _, externalIP := range svcInfo.ExternalIPs() {
// 根据 externalTrafficPolicy 选择 ipset
if svcInfo.ExternalPolicyLocal() {
proxier.ipsetList[kubeExternalIPLocalSet].activeEntries.Insert(...)
} else {
proxier.ipsetList[kubeExternalIPSet].activeEntries.Insert(...)
}

serv := &utilipvs.VirtualServer{Address: externalIP, ...}
// 如果 externalIP 已在某个真实网卡上:不绑定到 dummy 接口
// 否则绑定,以便 IPVS 能够捕获该 IP 的流量
bindAddr := !nodeAddressSet.Has(externalIP.String())
proxier.syncService(svcPortNameString, serv, bindAddr, alreadyBoundAddrs)
proxier.syncEndpoint(svcPortName, svcInfo.ExternalPolicyLocal(), serv)
}

// ─── C. LoadBalancer ─────────────────────────────────────────
for _, ingress := range svcInfo.LoadBalancerVIPs() {
proxier.ipsetList[kubeLoadBalancerSet].activeEntries.Insert(...)
if svcInfo.ExternalPolicyLocal() {
proxier.ipsetList[kubeLoadBalancerLocalSet].activeEntries.Insert(...)
}
// 处理 LoadBalancerSourceRanges(防火墙白名单)
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)
}

// ─── D. NodePort ─────────────────────────────────────────────
if svcInfo.NodePort() != 0 {
// 协议决定 ipset 类型(SCTP 必须用 hash ip:port,不支持 bitmap)
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":
// SCTP 需要为每个节点 IP 分别插入 entry
for _, nodeIP := range nodeIPs {
proxier.ipsetList[kubeNodePortSetSCTP].activeEntries.Insert(
fmt.Sprintf("%s,%s:%d", nodeIP, protocol, svcInfo.NodePort()))
}
}

// 为每个节点 IP 创建 IPVS VS
for _, nodeIP := range nodeIPs {
serv := &utilipvs.VirtualServer{
Address: nodeIP,
Port: uint16(svcInfo.NodePort()),
Protocol: string(svcInfo.Protocol()),
Scheduler: proxier.ipvsScheduler,
}
// NodePort:不绑定到 kube-ipvs0(节点 IP 已在真实网卡上)
proxier.syncService(svcPortNameString, serv, false, nil)
proxier.syncEndpoint(svcPortName, svcInfo.ExternalPolicyLocal(), serv)
activeIPVSServices.Insert(serv.String())
}
}
} // end of service loop

Phase 4:IPSet 同步到内核

1
2
3
4
5
6
7
// 将所有绑定到 kube-ipvs0 的 VIP 写入 KUBE-IPVS-IPS(用于防止暴露 host port)
proxier.ipsetList[kubeIPVSIPsSet].activeEntries = activeBindAddrs

// 同步每个 ipset:比较 activeEntries 与内核现有 entries,增删差异
for _, set := range proxier.ipsetList {
set.syncIPSetEntries() // kernel: ipset add/del
}

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
# ──── NAT 表 ────────────────────────────────────────────────

# ClusterIP 流量:非本地来源打 SNAT 标记
-A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst \
-m addrtype ! --src-type LOCAL -j KUBE-MARK-MASQ

# LoadBalancer 流量打标记
-A KUBE-SERVICES -m set --match-set KUBE-LOAD-BALANCER dst,dst -j KUBE-LOAD-BALANCER
-A KUBE-LOAD-BALANCER -j KUBE-MARK-MASQ

# NodePort TCP 打标记
-A KUBE-NODE-PORT -p tcp -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ
# Local 策略直接 RETURN(保留源 IP,不 SNAT)
-A KUBE-NODE-PORT -p tcp -m set --match-set KUBE-NODE-PORT-LOCAL-TCP dst -j RETURN

# POSTROUTING SNAT:有标记的包做 MASQUERADE
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000 # 清除标记(避免二次 SNAT)
-A KUBE-POSTROUTING -j MASQUERADE --random-fully # 真正的 SNAT

# ──── Filter 表 ──────────────────────────────────────────────

# LoadBalancer sourceRanges 防火墙
-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 # 不在白名单内:DROP

# IPVS IP 保护(防止 kube-ipvs0 上的 VIP 被直接访问为 host 服务)
-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

# 允许已标记流量通过 FORWARD
-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
// 1. 解绑不再需要的 VIP(已删除的 Service)
for addr := range alreadyBoundAddrs.Difference(activeBindAddrs) {
proxier.netlinkHandle.UnbindAddress(addr, defaultDummyDevice)
}

// 2. 删除废弃的 IPVS 虚拟服务
currentIPVSServices, _ := proxier.ipvs.GetVirtualServers()
for _, vs := range currentIPVSServices {
if activeIPVSServices.Has(vs.String()) {
continue // 仍然需要,跳过
}
if proxier.excludeCIDRs.Has(vs.Address) {
continue // 用户排除的 CIDR,不管理
}
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 {

// 查询内核当前的 VS 状态
appliedVS, _ := proxier.ipvs.GetVirtualServer(vs)

if appliedVS == nil {
// 全新 Service:直接创建
proxier.ipvs.AddVirtualServer(vs)
} else if !appliedVS.Equal(vs) {
// 已存在但配置变更(如调度算法、Session Affinity):原地更新
// IPVS 支持热更新,不会中断已有连接
proxier.ipvs.UpdateVirtualServer(vs)
}
// 配置未变化:跳过(避免无谓的 syscall)

// 地址绑定逻辑
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 {

// 获取内核当前的 Real Servers
curDests, _ := proxier.ipvs.GetRealServers(vs)
curEndpoints := sets.New[string]()
for _, d := range curDests {
curEndpoints.Insert(d.String())
}

// 根据流量策略过滤 Endpoint
endpoints := proxier.endpointsMap[svcPortName]
if onlyNodeLocalEndpoints {
// externalTrafficPolicy: Local 或 internalTrafficPolicy: Local
clusterEPs, localEPs, _, hasAny := proxy.CategorizeEndpoints(
endpoints, svcInfo, proxier.nodeName, proxier.topologyLabels)

if len(localEPs) > 0 {
endpoints = localEPs // 优先使用本节点 endpoint
} else {
endpoints = clusterEPs // 本节点无 endpoint,回退到 cluster-wide
// 记录 metrics:此 Service 在本节点无本地 endpoint
if hasAny {
proxier.serviceNoLocalEndpointsExternal.Insert(svcPortName.String())
}
}
}

// 构建期望状态
newEndpoints := sets.New[string]()
for _, ep := range endpoints {
newEndpoints.Insert(ep.String())
}

// 新增 Real Servers
for _, ep := range newEndpoints.Difference(curEndpoints).UnsortedList() {
ip, port, _ := net.SplitHostPort(ep)
newDest := &utilipvs.RealServer{
Address: net.ParseIP(ip),
Port: uint16(portInt),
Weight: 1,
}
// 检查是否正在优雅终止中(endpoint 先被删后又重新出现)
uniqueRS := GetUniqueRSName(vs, newDest)
if proxier.gracefuldeleteManager.InTerminationList(uniqueRS) {
// 从终止列表移除,恢复 weight=1
proxier.gracefuldeleteManager.MoveRSOutofGracefulDeleteList(uniqueRS)
} else {
proxier.ipvs.AddRealServer(vs, newDest)
}
}

// 删除废弃 Real Servers(优雅终止)
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),
}
// 交给 GracefulTerminationManager 处理(TCP 等待连接归零)
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 // 等待删除的 RS 列表
ipvs utilipvs.Interface
}

// GracefulDeleteRS:将 RS 权重设为 0,加入终止队列
func (m *GracefulTerminationManager) GracefulDeleteRS(
vs *utilipvs.VirtualServer, rs *utilipvs.RealServer) error {

// 先尝试立即删除(若无活跃连接)
deleted, err := m.deleteRsFunc(&listItem{VirtualServer: vs, RealServer: rs})
if deleted {
return nil
}

// 有活跃连接:设置 weight=0(IPVS 不再将新连接分发到此 RS)
rs.Weight = 0
m.ipvs.UpdateRealServer(vs, rs)

// 加入终止队列,等待后台 goroutine 完成清理
m.rsList.add(&listItem{VirtualServer: vs, RealServer: rs})
return err
}

// deleteRsFunc:判断是否可以立即删除
func (m *GracefulTerminationManager) deleteRsFunc(rsToDelete *listItem) (bool, error) {
rss, _ := m.ipvs.GetRealServers(rsToDelete.VirtualServer)
for _, rs := range rss {
if !rsToDelete.RealServer.Equal(rs) {
continue
}
// UDP/SCTP:无连接状态,立即删除
if !IsRsGracefulTerminationNeeded(rsToDelete.VirtualServer.Protocol) {
return m.ipvs.DeleteRealServer(rsToDelete.VirtualServer, rs) == nil, nil
}
// TCP:检查连接计数
if rs.ActiveConn+rs.InactiveConn == 0 {
// 连接已全部关闭,可以删除
return m.ipvs.DeleteRealServer(rsToDelete.VirtualServer, rs) == nil, nil
}
return false, nil // 还有连接,等待
}
return true, nil // RS 已不在内核中(可能被其他原因删除)
}

// Run:启动后台 goroutine,每分钟检查一次
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
// 创建带 Session Affinity 的 IPVS VS
serv := &utilipvs.VirtualServer{
Address: clusterIP,
Port: uint16(port),
Protocol: "TCP",
Scheduler: "rr",
Flags: utilipvs.FlagPersistent, // 启用 persistent 模式
Timeout: 10800, // 默认 3 小时(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
// NewProxier 初始化时设置内核参数
sysctls := map[string]string{
// IPVS 相关
"net/ipv4/vs/conntrack": "1", // 启用 IPVS conntrack(NAT 穿透必需)
"net/ipv4/vs/conn_reuse_mode": "0", // 禁止连接重用(避免部分内核版本的端口复用问题)
"net/ipv4/vs/expire_nodest_conn": "1", // RS 被删除后立即清理相关连接
"net/ipv4/vs/expire_quiescent_template": "1", // 清理静默的持久化连接模板

// 基础网络
"net/ipv4/ip_forward": "1", // 开启 IP 转发(Pod 间通信必需)

// ARP 抑制(strictARP=true 时)
"net/ipv4/conf/all/arp_ignore": "1", // 只回应目标 IP 在入包接口上的 ARP
"net/ipv4/conf/all/arp_announce": "2", // ARP 回复使用最优源 IP
}

十二、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
# 查看所有 IPVS 虚拟服务
ipvsadm -Ln

# 查看特定 ClusterIP 的 Real Servers 及连接统计
ipvsadm -Ln --stats | grep -A5 "10.96.0.1:443"

# 查看 kube-ipvs0 绑定的所有 VIP
ip addr show kube-ipvs0

# 查看所有 ipset 内容
ipset list KUBE-CLUSTER-IP
ipset list KUBE-NODE-PORT-TCP

# 查看优雅终止中的 RS(weight=0)
ipvsadm -Ln | grep "^ -> " | grep " 0 "

# 查看 iptables KUBE-POSTROUTING(SNAT 规则)
iptables -t nat -L KUBE-POSTROUTING -v -n

十五、总结

kube-proxy IPVS 模式的设计精妙之处在于:

  1. 职责分离:IPVS 负责高效 DNAT(内核 hash 表,O(1)),iptables 负责 SNAT 和防火墙(规则数量固定,与 Service 数量无关)
  2. dummy 接口技巧:通过 kube-ipvs0 让内核将所有 VIP 识别为本地地址,触发 IPVS 处理路径
  3. ipset 批量匹配:将 O(n) 的 iptables 规则匹配压缩为 O(1) 的 hash 集合查找
  4. 优雅终止:weight=0 + 连接计数轮询,避免已有 TCP 连接被强制中断
  5. 增量同步:BoundedFrequencyRunner 限速 + 变更追踪器 (ChangeTracker) 保证同步的正确性和效率

对于超过几百个 Service 的生产集群,IPVS 模式是 kube-proxy 的首选配置。