各位技术同仁,大家好!
今天,我们将深入探讨 Kubernetes 网络层一个至关重要的组件——kube-proxy,特别是其在 IPVS 模式下的工作原理。我们将聚焦于 Go 语言是如何作为桥梁,巧妙地操作 Linux 内核的 IPVS 表,从而实现大规模服务的高效负载均衡的。这不仅仅是一个理论讲解,更是一次对底层机制和 Go 语言实践的深度剖析。
Kubernetes 服务负载均衡的挑战与演进
在 Kubernetes 生态中,Service 对象是实现内部服务发现和负载均衡的核心抽象。它为一组 Pod 提供了一个稳定的网络入口,并将客户端请求分发到后端健康的 Pod 上。kube-proxy 作为每个节点上的网络代理,正是这一机制的实际执行者。
kube-proxy 经历了从用户空间代理(userspace 模式,已废弃)到 iptables 模式,再到我们今天重点讨论的 IPVS 模式的演进。
iptables 模式是 kube-proxy 长期以来的默认模式。它通过在 Linux 内核的 netfilter 框架中插入大量的 iptables 规则来实现负载均衡。当集群规模较小、服务和 Pod 数量不多时,iptables 模式表现良好。然而,随着集群规模的扩大,iptables 规则的数量会急剧增长,这带来了几个显著问题:
- 性能瓶颈:
iptables规则链的遍历是线性的。当规则数量达到数万甚至数十万条时,每次数据包经过netfilter时,都需要花费大量 CPU 时间进行规则匹配,导致转发性能下降。 conntrack表压力:iptables依赖netfilter的连接跟踪(conntrack)机制来维护会话状态。大规模连接可能导致conntrack表溢出,引发网络问题。- 规则同步延迟:
kube-proxy每次更新Service或Endpoint状态时,都需要原子性地刷新所有iptables规则,这在规则量大时会产生明显的延迟,甚至导致短暂的网络中断。
为了解决这些挑战,Kubernetes 社区引入了 IPVS(IP Virtual Server)模式。IPVS 是 Linux 内核中一个高性能的 Layer 4 负载均衡器,它基于哈希表实现,能够更高效地处理大量的服务和连接。
| 特性 | iptables 模式 |
IPVS 模式 |
|---|---|---|
| 实现机制 | netfilter 框架,基于规则链的线性匹配 |
IPVS 内核模块,基于哈希表的快速查找 |
| 性能 | 规则多时性能下降明显 | 规则多时性能依然出色,吞吐量高 |
| CPU 消耗 | 规则多时 CPU 占用高 | CPU 占用相对较低,尤其是在高并发场景下 |
| 连接跟踪 | 依赖 netfilter conntrack,压力大时易溢出 |
IPVS 自身维护连接状态,对 conntrack 压力小 |
| 规则更新 | 全量刷新,耗时较长,可能导致瞬时中断 | 增量更新,高效快速 |
| 负载均衡算法 | 随机(--random-fully) |
支持多种算法(RR, WRR, LC, WLC, SH, DH 等) |
| 内核要求 | Linux 2.4+ | Linux 3.10+ (通常是 4.x+) |
IPVS 模式的引入,极大地提升了 kube-proxy 在大规模 Kubernetes 集群中的性能和稳定性。现在,让我们深入了解 IPVS 本身。
IPVS 核心原理:内核级的 Layer 4 负载均衡器
IPVS 是 Linux 内 Virtual Server 项目的一部分,它在操作系统内核中实现了一个高性能的 Layer 4(传输层)负载均衡器。它能够将到达特定 IP 地址和端口(即虚拟服务)的 TCP/UDP/SCTP 请求,透明地分发到一组真实的服务器(即后端 Pod)上。
IPVS 的核心概念
-
虚拟服务器 (Virtual Server / Service):
- 由一个唯一的 IP 地址、端口和 协议 组成。
- 在 Kubernetes 中,这对应于
Service的 ClusterIP、端口和协议。 - 每个虚拟服务器都配置了一个 负载均衡调度算法,用于决定如何将请求分发给后端真实服务器。
ipvsadm命令中通常表示为-A(add service) 或-E(edit service)。
-
真实服务器 (Real Server / Destination):
- 是处理实际请求的后端服务器,由其 IP 地址 和 端口 组成。
- 在 Kubernetes 中,这对应于
Endpoint(即 Pod 的 IP 和端口)。 - 每个真实服务器还可以配置一个 权重 (weight),用于加权调度算法。
- 每个真实服务器还关联一个 转发方法 (forwarding method)。
ipvsadm命令中通常表示为-a(add destination) 或-e(edit destination)。
-
负载均衡调度算法 (Scheduling Algorithms):
IPVS 提供了多种调度算法,以适应不同的应用场景:- Round-Robin (RR): 轮询调度,将请求依次分发到每个真实服务器,不考虑服务器负载。
- Weighted Round-Robin (WRR): 加权轮询调度,根据服务器的权重比例进行分发。权重越高的服务器获得越多的请求。
- Least-Connection (LC): 最少连接调度,将请求发送到当前活动连接数最少的服务器。适用于连接时长不均的场景。
- Weighted Least-Connection (WLC): 加权最少连接调度,在 LC 的基础上考虑服务器权重。
- Source Hashing (SH): 源地址哈希调度,根据客户端源 IP 地址的哈希值来选择服务器。可以实现会话保持。
- Destination Hashing (DH): 目标地址哈希调度,根据目标地址的哈希值来选择服务器。
- 还有其他如 Shortest Expected Delay (SED), Never Queue (NQ) 等。
-
转发方法 (Forwarding Methods):
IPVS 支持三种主要的转发方法,决定了数据包如何从负载均衡器转发到真实服务器,以及真实服务器如何响应客户端:- NAT (Network Address Translation): 网络地址转换。这是 Kubernetes
kube-proxyIPVS 模式默认使用的方法。负载均衡器接收到请求后,将数据包的目标 IP 地址和端口转换为真实服务器的 IP 地址和端口,然后发送给真实服务器。真实服务器的响应会通过负载均衡器返回给客户端,负载均衡器再次进行源地址转换。这种模式下,真实服务器不需要知道客户端的真实 IP。 - TUN (IP Tunneling): IP 隧道。负载均衡器将请求封装在 IP 隧道中,发送给真实服务器。真实服务器解封装后直接将响应发送回客户端(不需要经过负载均衡器)。这要求真实服务器配置隧道接口。
- DR (Direct Routing): 直接路由。负载均衡器只修改数据包的 MAC 地址,直接将请求转发给真实服务器。真实服务器直接将响应发送回客户端。这要求负载均衡器和真实服务器在同一物理网络中,且真实服务器需要将虚拟 IP 地址配置在非 ARP 响应的接口上(如
lo:0),以避免 IP 冲突。
- NAT (Network Address Translation): 网络地址转换。这是 Kubernetes
在 Kubernetes 中,kube-proxy 主要使用 NAT 转发模式,因为它对后端 Pod 的网络配置侵入性最小,且能很好地与 Docker/Containerd 的网络模型兼容。
ipvsadm 工具:查看和管理 IPVS 表
ipvsadm 是用户空间与内核 IPVS 模块交互的命令行工具,它可以用来添加、修改、删除虚拟服务器和真实服务器,并查看当前的 IPVS 配置。
示例:查看当前的 IPVS 规则
# 查看所有 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.96.0.1:443 rr
# -> 10.244.0.2:6443 Masq 1 0 0
# -> 10.244.1.3:6443 Masq 1 0 0
# TCP 10.96.0.10:80 rr
# -> 10.244.2.4:8080 Masq 1 0 0
# UDP 10.96.0.53:53 rr
# -> 10.244.3.5:53 Masq 1 0 0
上述输出展示了三个虚拟服务:
- 一个 TCP 服务
10.96.0.1:443(Kubernetes API Server),使用rr调度,后端有10.244.0.2:6443和10.244.1.3:6443两个真实服务器。 - 一个 TCP 服务
10.96.0.10:80,后端是10.244.2.4:8080。 - 一个 UDP 服务
10.96.0.53:53(CoreDNS),后端是10.244.3.5:53。
Masq 表示使用了 NAT 转发模式。
示例:添加一个虚拟服务和真实服务器
# 添加一个 TCP 虚拟服务 192.168.1.100:80,使用轮询调度
sudo ipvsadm -A -t 192.168.1.100:80 -s rr
# 为该虚拟服务添加两个真实服务器
sudo ipvsadm -a -t 192.168.1.100:80 -r 10.0.0.1:8080 -m -w 1
sudo ipvsadm -a -t 192.168.1.100:80 -r 10.0.0.2:8080 -m -w 1
# -A: 添加虚拟服务
# -t: TCP 服务 (也可以是 -u for UDP, -f for fwmark)
# -s: 调度算法 (scheduler)
# -a: 添加真实服务器
# -r: 真实服务器的 IP:Port
# -m: 使用 Masquerading (NAT) 转发方法
# -w: 权重
ipvsadm 工具的存在,证明了内核 IPVS 模块提供了标准的用户空间接口来管理其表项。Go 语言操作 IPVS,正是通过这些接口(更准确地说,是底层的 Netlink 协议)来实现的。
kube-proxy 在 IPVS 模式下的工作流
kube-proxy 在 IPVS 模式下的核心职责是监控 Kubernetes API Server 中的 Service 和 Endpoint 对象的变化,并据此维护节点上的 IPVS 规则。
- 观察者模式:
kube-proxy启动后,会建立与 Kubernetes API Server 的连接,并注册为Service和Endpoint对象的观察者。当这些对象发生增、删、改事件时,kube-proxy会收到通知。 - 状态同步:
kube-proxy内部维护一个本地缓存,存储当前所有Service和Endpoint的状态。当 API Server 发送事件时,kube-proxy会更新其本地缓存。 - 规则计算: 基于最新的
Service和Endpoint状态,kube-proxy会计算出当前节点上应该存在的 IPVS 虚拟服务和真实服务器的完整集合。- 每个
Service的 ClusterIP、Port 和 Protocol 对应一个 IPVS 虚拟服务。 - 每个
Service关联的每个Endpoint(即 Pod 的 IP 和 Port)对应一个 IPVS 真实服务器。 kube-proxy还会根据Service的sessionAffinity属性选择合适的 IPVS 调度算法(例如,如果sessionAffinity为ClientIP,则可能使用sh)。
- 每个
- IPVS 表更新:
kube-proxy将计算出的期望状态与当前内核中的 IPVS 实际状态进行比较。它会执行一系列操作来同步两者:- 添加 新增的虚拟服务和真实服务器。
- 更新 现有虚拟服务(例如,更改调度算法)或真实服务器(例如,更改权重或转发方法)。
- 删除 不再存在的虚拟服务和真实服务器。
- 对于
Service的 ClusterIP,kube-proxy还会确保该 IP 地址被绑定到节点的一个网络接口上(通常是一个dummy接口,如kube-ipvs0),以便内核能够正确地接收发往该 ClusterIP 的流量。
- 健康检查:
kube-proxy持续监控后端 Pod 的健康状况(通过Endpoint对象的状态)。如果一个 Pod 不健康,kube-proxy会将其对应的真实服务器从 IPVS 规则中移除,或者将其权重设置为 0,从而停止向其发送流量。
这个循环被称为 syncLoop,它确保了 kube-proxy 的 IPVS 规则始终与 Kubernetes API Server 的期望状态保持一致。
Go 语言与内核 IPVS 的深度交互
Go 语言在 kube-proxy 中扮演着核心角色,它通过 Netlink 协议与 Linux 内核进行通信,从而实现对 IPVS 表的精确控制。
Netlink 协议:用户空间与内核的桥梁
Netlink 是一种基于 socket 的进程间通信 (IPC) 机制,专门用于用户空间进程和内核模块之间的通信。它比传统的 ioctl 机制更灵活,支持异步通信和多播。Linux 内核中几乎所有的网络配置和状态报告都通过 Netlink 实现,包括路由、IP 地址、ARP 表、防火墙规则以及我们今天的主角 IPVS。
Netlink 协议的工作方式类似于标准的 BSD Socket:
- 用户空间进程创建一个 Netlink socket。
- 通过
sendmsg()或recvmsg()函数发送和接收 Netlink 消息。 - Netlink 消息包含一个头部(
nlmsghdr)和实际的数据负载。数据负载通常包含一个或多个属性(nlattr),用于传递具体的配置信息。
IPVS 模块通过 Netlink 的 NETLINK_GENERIC 家族(一个通用的 Netlink 家族,通过 protocol 和 command 字段区分不同的子协议)进行通信。
github.com/vishvananda/netlink 库:Go 语言的 Netlink 封装
直接使用 Go 的 syscall 包来构建和解析原始的 Netlink 消息是非常繁琐且容易出错的。幸运的是,Go 社区提供了一个功能强大且广泛使用的库:github.com/vishvananda/netlink。
这个库提供了对 Linux Netlink 协议的高级封装,使得 Go 程序能够以更简洁、更安全的方式进行网络配置,包括:
- 管理网络接口(添加/删除接口、设置 IP 地址、UP/DOWN 状态)
- 管理路由表
- 管理 ARP 表
- 管理 IPVS 规则(通过其
ipvs子包)
netlink 库内部通过 Go 的 syscall 包与 Linux 内核交互,处理 Netlink 消息的序列化和反序列化。
Go 操作 IPVS 的核心流程与代码示例
在 kube-proxy 中,操作 IPVS 的逻辑主要封装在 pkg/proxy/ipvs 包中。它会使用 netlink 库中的 ipvs 子包提供的函数。
让我们通过一个简化的 Go 代码示例来演示如何使用 netlink 库来添加一个 IPVS 虚拟服务和它的真实服务器。
首先,确保你的 Go 环境中安装了 netlink 库:
go get github.com/vishvananda/netlink
package main
import (
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/ipvs" // 专门用于 IPVS 操作的子包
)
// IPVSConfig 定义了 IPVS 虚拟服务和真实服务器的配置
type IPVSConfig struct {
ServiceIP string
ServicePort int
Protocol string
Scheduler string
Destinations []DestinationConfig
}
// DestinationConfig 定义了 IPVS 真实服务器的配置
type DestinationConfig struct {
IP string
Port int
Weight int
}
func main() {
// 确保程序以 root 权限运行,因为 IPVS 操作需要 CAP_NET_ADMIN 权限
if os.Getuid() != 0 {
fmt.Println("This program must be run as root.")
os.Exit(1)
}
// 1. 定义 IPVS 配置
config := IPVSConfig{
ServiceIP: "192.168.100.100", // 虚拟服务 IP (ClusterIP)
ServicePort: 80,
Protocol: "tcp",
Scheduler: "rr", // Round-Robin 调度
Destinations: []DestinationConfig{
{IP: "10.0.1.1", Port: 8080, Weight: 100},
{IP: "10.0.1.2", Port: 8080, Weight: 100},
{IP: "10.0.1.3", Port: 8080, Weight: 50}, // 权重较低的后端
},
}
fmt.Printf("Configuring IPVS service %s:%d/%s with scheduler %s...n",
config.ServiceIP, config.ServicePort, config.Protocol, config.Scheduler)
// 2. 将字符串协议转换为 ipvs.Protocol
proto, err := parseProtocol(config.Protocol)
if err != nil {
fmt.Printf("Error parsing protocol: %vn", err)
return
}
// 3. 创建 ipvs.Service 对象
svc := &ipvs.Service{
NetID: ipvs.NetID{
Protocol: proto,
Addr: net.ParseIP(config.ServiceIP),
Port: uint16(config.ServicePort),
},
Scheduler: config.Scheduler,
Flags: 0, // 可以在这里设置 IPVS 标志,如持久性等
}
// 4. 尝试添加或更新 IPVS 虚拟服务
fmt.Printf("Attempting to add or update IPVS service: %vn", svc.NetID)
err = ipvs.ServiceAdd(svc)
if err != nil {
if os.IsExist(err) {
fmt.Printf("Service %v already exists, attempting to update...n", svc.NetID)
err = ipvs.ServiceUpdate(svc)
if err != nil {
fmt.Printf("Error updating IPVS service: %vn", err)
return
}
fmt.Printf("IPVS service %v updated successfully.n", svc.NetID)
} else {
fmt.Printf("Error adding IPVS service: %vn", err)
return
}
} else {
fmt.Printf("IPVS service %v added successfully.n", svc.NetID)
}
// 5. 添加或更新 IPVS 真实服务器 (Destination)
for _, destConfig := range config.Destinations {
fmt.Printf("Attempting to add or update destination %s:%d for service %v...n",
destConfig.IP, destConfig.Port, svc.NetID)
dest := &ipvs.Destination{
Service: svc, // 关联到之前创建的 Service
NetID: ipvs.NetID{
Addr: net.ParseIP(destConfig.IP),
Port: uint16(destConfig.Port),
},
Weight: destConfig.Weight,
FwdMethod: ipvs.FwdMethodMasq, // 使用 NAT (Masquerading) 转发
ActiveConn: 0,
InActConn: 0,
}
err = ipvs.DestinationAdd(dest)
if err != nil {
if os.IsExist(err) {
fmt.Printf("Destination %v already exists for service %v, attempting to update...n", dest.NetID, svc.NetID)
err = ipvs.DestinationUpdate(dest)
if err != nil {
fmt.Printf("Error updating IPVS destination %v: %vn", dest.NetID, err)
return
}
fmt.Printf("IPVS destination %v updated successfully.n", dest.NetID)
} else {
fmt.Printf("Error adding IPVS destination %v: %vn", dest.NetID, err)
return
}
} else {
fmt.Printf("IPVS destination %v added successfully.n", dest.NetID)
}
}
// 6. 验证 IPVS 配置
fmt.Println("nVerifying IPVS configuration with 'sudo ipvsadm -Ln':")
cmd := "ipvsadm -Ln"
out, err := runCommand("sudo", "ipvsadm", "-Ln")
if err != nil {
fmt.Printf("Error running ipvsadm: %vn", err)
} else {
fmt.Println(out)
}
// 7. (可选)清理 IPVS 配置
fmt.Println("nWaiting 10 seconds before cleaning up...")
time.Sleep(10 * time.Second)
fmt.Printf("Cleaning up IPVS service %v...n", svc.NetID)
err = ipvs.ServiceDel(svc)
if err != nil {
fmt.Printf("Error deleting IPVS service: %vn", err)
} else {
fmt.Printf("IPVS service %v deleted successfully.n", svc.NetID)
}
fmt.Println("nVerifying IPVS configuration after cleanup:")
out, err = runCommand("sudo", "ipvsadm", "-Ln")
if err != nil {
fmt.Printf("Error running ipvsadm: %vn", err)
} else {
fmt.Println(out)
}
}
// parseProtocol 将字符串协议转换为 ipvs.Protocol 类型
func parseProtocol(p string) (ipvs.Protocol, error) {
switch p {
case "tcp":
return ipvs.ProtocolTCP, nil
case "udp":
return ipvs.ProtocolUDP, nil
case "sctp":
return ipvs.ProtocolSCTP, nil
default:
return 0, fmt.Errorf("unsupported protocol: %s", p)
}
}
// runCommand 辅助函数,用于执行 shell 命令并返回输出
func runCommand(name string, arg ...string) (string, error) {
cmd := os.Args[0] // 获取当前可执行文件的路径
if len(os.Args) > 0 {
cmd = os.Args[0]
}
// 在实际的生产环境中,kube-proxy 不会直接调用 shell 命令
// 而是直接通过 netlink 库与内核交互。
// 这里使用 os/exec 只是为了方便演示和验证。
c := exec.Command(name, arg...)
output, err := c.CombinedOutput()
if err != nil {
return "", fmt.Errorf("command '%s %s' failed with error: %v, output: %s", name, strings.Join(arg, " "), err, string(output))
}
return string(output), nil
}
// 注意:为了运行上述代码,你可能需要 import "os/exec" 和 "strings"
// 并且在 Go Modules 中添加这些依赖。
// 实际测试时,请确保 `ipvsadm` 工具已安装在你的系统上。
代码解析:
ipvs.Service结构体:NetID字段封装了虚拟服务的网络标识(协议、IP 地址、端口)。Scheduler字段指定了负载均衡算法(如 "rr")。Flags字段可以用于设置 IPVS 服务的其他属性,如持久性。
ipvs.Destination结构体:Service字段指向它所属的ipvs.Service对象。NetID字段封装了真实服务器的网络标识(IP 地址、端口)。Weight字段用于加权调度算法。FwdMethod字段指定了转发方法,ipvs.FwdMethodMasq对应 NAT 模式。
- 核心 API 调用:
ipvs.ServiceAdd(svc):向内核添加一个 IPVS 虚拟服务。ipvs.ServiceUpdate(svc):更新一个已存在的 IPVS 虚拟服务。ipvs.ServiceDel(svc):从内核删除一个 IPVS 虚拟服务。ipvs.DestinationAdd(dest):为一个虚拟服务添加一个真实服务器。ipvs.DestinationUpdate(dest):更新一个真实服务器的属性。ipvs.DestinationDel(dest):删除一个真实服务器。os.IsExist(err):这是netlink库的一个有用特性,当尝试添加一个已存在的对象时,它会返回一个错误,但可以通过os.IsExist判断是否是“已存在”错误,然后尝试更新。这正是kube-proxy实现幂等性操作的关键。
这个示例清晰地展示了 Go 语言如何通过 netlink/ipvs 库,以结构化的方式直接与内核 IPVS 模块通信,执行添加、更新和删除操作。kube-proxy 的 IPVS 模式正是基于这样的机制,在集群规模变化时,高效、准确地调整内核的负载均衡规则。
netlink 库内部的 Netlink 消息处理
当我们调用 ipvs.ServiceAdd(svc) 时,netlink 库在底层做了什么?
- 它会构建一个 Netlink 消息,消息头包含
NETLINK_GENERIC家族标识、RTM_NEWSERVICE或RTM_SETSERVICE命令(对应添加或更新服务)。 - 消息的数据部分会被编码为一系列
nlattr(Netlink Attribute)结构体,这些属性包含了ipvs.Service结构体的各个字段(如 IP、端口、协议、调度器)。 - 这个 Netlink 消息通过 Netlink socket 发送到内核。
- 内核的 IPVS 模块接收并解析消息,然后相应地修改其内部的 IPVS 表。
- 操作成功或失败的响应也会通过 Netlink socket 返回给用户空间进程。
这个过程对于 netlink 库的用户来说是透明的,我们只需关注 Go 结构体和函数调用即可。
规模化与性能:IPVS 的优势
IPVS 模式的 kube-proxy 在大规模集群中展现出卓越的性能和扩展性:
- 哈希表查找性能: IPVS 在内核中维护了一个哈希表来存储虚拟服务。当数据包到达时,内核可以直接通过目标 IP、端口和协议的哈希值快速定位到对应的虚拟服务,然后根据调度算法选择后端真实服务器。这种 O(1) 或接近 O(1) 的查找速度,相比
iptables的线性遍历(O(N),N 为规则数量)具有显著优势,尤其是在规则数量庞大时。 - 更低的 CPU 开销: 由于快速查找和直接在内核中处理负载均衡逻辑,IPVS 模式的 CPU 占用通常低于
iptables模式。 - 高效的连接跟踪: IPVS 自身维护了连接状态,它可以在内核空间高效地进行连接跟踪,并将连接粘性(session affinity)直接应用于 IPVS 连接表。这减少了对
netfilterconntrack表的压力,避免了conntrack溢出等问题,从而提高了整体网络稳定性。 - 增量更新:
kube-proxy在 IPVS 模式下可以执行增量更新。当Service或Endpoint发生变化时,它只添加、更新或删除受影响的 IPVS 条目,而不是像iptables模式那样需要刷新整个规则集。这使得规则同步更快,对网络中断的影响更小。 - 多种调度算法: IPVS 提供了丰富的负载均衡调度算法,允许集群管理员根据应用特性选择最合适的算法,实现更精细的流量控制和资源优化。
IPVS 如何处理 Service IP?
为了让节点能够接收发往 Service ClusterIP 的流量,kube-proxy 会将每个 Service 的 ClusterIP 绑定到节点上的一个网络接口。通常,它会创建一个虚拟的 dummy 接口(例如 kube-ipvs0),并将所有 Service 的 ClusterIP 作为辅助 IP 地址添加到这个接口上。这样,当客户端(无论是 Pod 还是节点上的进程)尝试连接 Service ClusterIP 时,内核知道这个 IP 是本机的,并将流量路由到 IPVS 模块进行处理。
# 示例:查看 kube-proxy 创建的 dummy 接口和其上的 IP 地址
ip a show kube-ipvs0
# 示例输出:
# 10: kube-ipvs0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether 0a:5c:29:85:2b:f0 brd ff:ff:ff:ff:ff:ff
# inet 10.96.0.1/32 scope host kube-ipvs0 # Kubernetes API Server ClusterIP
# inet 10.96.0.10/32 scope host kube-ipvs0 # Other Service ClusterIP
# inet 10.96.0.53/32 scope host kube-ipvs0 # DNS Service ClusterIP
# ...
高级 IPVS 特性与 kube-proxy 的实践
-
Session Affinity (会话保持):
KubernetesService的sessionAffinity: ClientIP允许将来自同一客户端 IP 的请求始终路由到同一个后端 Pod。在 IPVS 模式下,kube-proxy通过将 IPVS 虚拟服务的调度算法设置为sh(Source Hashing) 并配置一个persistence_timeout来实现这一功能。IPVS 内核模块会根据客户端源 IP 的哈希值,将请求转发到特定的真实服务器,并在指定时间内保持这种关联。// 示例:在 Go 中设置持久性标志和超时 svc := &ipvs.Service{ NetID: ipvs.NetID{ Protocol: ipvs.ProtocolTCP, Addr: net.ParseIP("192.168.100.100"), Port: uint16(80), }, Scheduler: "sh", // Source Hashing Flags: ipvs.FlagPersistent, // 启用持久性 Timeout: 300, // 持久化超时时间(秒) } -
健康检查与权重调整:
kube-proxy会持续观察EndpointSlice或Endpoint对象中后端 Pod 的Ready状态。当一个 Pod 变为 NotReady 时,kube-proxy会更新其对应的 IPVS 真实服务器:- 移除: 直接从 IPVS 表中删除该真实服务器。这是最常见的做法。
- 权重设为 0: 将该真实服务器的
Weight设为 0。这样,调度器就不会再向其发送新连接,但现有的连接会继续保持,直到超时。这在某些平滑下线场景中可能有用。
-
IPVS Forwarding Methods 的选择:
如前所述,IPVS 支持 NAT、TUN、DR。kube-proxy默认且主要使用 NAT 模式(FwdMethodMasq)。这是因为它最简单,对后端 Pod 的网络配置没有特殊要求,并且能够很好地处理 Pod 跨节点甚至跨宿主机的场景。
kube-proxy 内部 IPVS 模式的实现细节
在 kube-proxy 的源代码中,IPVS 模式的实现位于 pkg/proxy/ipvs 目录下。
Proxier接口:kube-proxy核心逻辑通过Proxier接口抽象了不同代理模式的实现。ipvsProxier结构体实现了这个接口。// pkg/proxy/proxier.go type Proxier interface { // ... SyncLoop() Sync() // ... }syncLoop(): 这是kube-proxy的主循环,它负责监听 Kubernetes API Server 的事件,并在检测到Service或EndpointSlice变化时触发同步操作。syncProxyRules(): 这是ipvsProxier的核心方法,它负责实际的 IPVS 规则计算和更新。- 它会从
kube-proxy内部维护的ServiceMap和EndpointsMap中获取最新的服务和后端信息。 - 然后,它会遍历这些数据,构建出期望的
ipvs.Service和ipvs.Destination列表。 - 通过与当前内核中的 IPVS 状态(通过
ipvs.ServiceList()和ipvs.DestinationList()获取)进行比较,计算出需要添加、更新或删除的 IPVS 条目。 - 最后,它会调用
netlink/ipvs库提供的ServiceAdd/Update/Del和DestinationAdd/Update/Del函数来更新内核 IPVS 表。
- 它会从
ipvs.Manager:ipvsProxier内部通常会封装一个ipvs.Manager类似的组件,负责与netlink/ipvs库交互,处理 IPVS 规则的增删改查。这个 Manager 会处理 Netlink socket 的创建和关闭,以及错误处理等底层细节。- 并发与锁: 由于
kube-proxy是一个并发程序,它需要处理来自 API Server 的异步事件。在更新 IPVS 规则时,为了保证数据一致性,ipvsProxier会使用适当的锁机制(如sync.Mutex)来保护其内部状态和对 IPVS 内核表的操作。
总结:IPVS 模式的价值与 Go 语言的力量
kube-proxy 的 IPVS 模式是 Kubernetes 在大规模集群中实现高性能服务负载均衡的关键。它通过利用 Linux 内核原生的 IPVS 模块,解决了 iptables 模式在扩展性上的瓶颈,提供了更低的延迟、更高的吞吐量和更低的 CPU 消耗。Go 语言凭借其强大的网络编程能力和对底层系统调用的良好封装,通过 github.com/vishvananda/netlink 这样的库,使得 kube-proxy 能够以高效、可靠的方式与内核 IPVS 表进行深度交互,从而将 Kubernetes 的服务抽象无缝地映射到内核级的负载均衡机制上。理解这一机制,对于深入掌握 Kubernetes 网络原理和排查相关问题至关重要。