深入 ‘Kube-proxy IPVS Mode’:解析 Go 是如何操作内核 IPVS 表来实现大规模服务负载均衡的?

各位技术同仁,大家好!

今天,我们将深入探讨 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 规则的数量会急剧增长,这带来了几个显著问题:

  1. 性能瓶颈: iptables 规则链的遍历是线性的。当规则数量达到数万甚至数十万条时,每次数据包经过 netfilter 时,都需要花费大量 CPU 时间进行规则匹配,导致转发性能下降。
  2. conntrack 表压力: iptables 依赖 netfilter 的连接跟踪(conntrack)机制来维护会话状态。大规模连接可能导致 conntrack 表溢出,引发网络问题。
  3. 规则同步延迟: kube-proxy 每次更新 ServiceEndpoint 状态时,都需要原子性地刷新所有 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 的核心概念

  1. 虚拟服务器 (Virtual Server / Service):

    • 由一个唯一的 IP 地址端口协议 组成。
    • 在 Kubernetes 中,这对应于 Service 的 ClusterIP、端口和协议。
    • 每个虚拟服务器都配置了一个 负载均衡调度算法,用于决定如何将请求分发给后端真实服务器。
    • ipvsadm 命令中通常表示为 -A (add service) 或 -E (edit service)。
  2. 真实服务器 (Real Server / Destination):

    • 是处理实际请求的后端服务器,由其 IP 地址端口 组成。
    • 在 Kubernetes 中,这对应于 Endpoint(即 Pod 的 IP 和端口)。
    • 每个真实服务器还可以配置一个 权重 (weight),用于加权调度算法。
    • 每个真实服务器还关联一个 转发方法 (forwarding method)
    • ipvsadm 命令中通常表示为 -a (add destination) 或 -e (edit destination)。
  3. 负载均衡调度算法 (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) 等。
  4. 转发方法 (Forwarding Methods):
    IPVS 支持三种主要的转发方法,决定了数据包如何从负载均衡器转发到真实服务器,以及真实服务器如何响应客户端:

    • NAT (Network Address Translation): 网络地址转换。这是 Kubernetes kube-proxy IPVS 模式默认使用的方法。负载均衡器接收到请求后,将数据包的目标 IP 地址和端口转换为真实服务器的 IP 地址和端口,然后发送给真实服务器。真实服务器的响应会通过负载均衡器返回给客户端,负载均衡器再次进行源地址转换。这种模式下,真实服务器不需要知道客户端的真实 IP。
    • TUN (IP Tunneling): IP 隧道。负载均衡器将请求封装在 IP 隧道中,发送给真实服务器。真实服务器解封装后直接将响应发送回客户端(不需要经过负载均衡器)。这要求真实服务器配置隧道接口。
    • DR (Direct Routing): 直接路由。负载均衡器只修改数据包的 MAC 地址,直接将请求转发给真实服务器。真实服务器直接将响应发送回客户端。这要求负载均衡器和真实服务器在同一物理网络中,且真实服务器需要将虚拟 IP 地址配置在非 ARP 响应的接口上(如 lo:0),以避免 IP 冲突。

在 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:644310.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 中的 ServiceEndpoint 对象的变化,并据此维护节点上的 IPVS 规则。

  1. 观察者模式: kube-proxy 启动后,会建立与 Kubernetes API Server 的连接,并注册为 ServiceEndpoint 对象的观察者。当这些对象发生增、删、改事件时,kube-proxy 会收到通知。
  2. 状态同步: kube-proxy 内部维护一个本地缓存,存储当前所有 ServiceEndpoint 的状态。当 API Server 发送事件时,kube-proxy 会更新其本地缓存。
  3. 规则计算: 基于最新的 ServiceEndpoint 状态,kube-proxy 会计算出当前节点上应该存在的 IPVS 虚拟服务和真实服务器的完整集合。
    • 每个 Service 的 ClusterIP、Port 和 Protocol 对应一个 IPVS 虚拟服务。
    • 每个 Service 关联的每个 Endpoint(即 Pod 的 IP 和 Port)对应一个 IPVS 真实服务器。
    • kube-proxy 还会根据 ServicesessionAffinity 属性选择合适的 IPVS 调度算法(例如,如果 sessionAffinityClientIP,则可能使用 sh)。
  4. IPVS 表更新: kube-proxy 将计算出的期望状态与当前内核中的 IPVS 实际状态进行比较。它会执行一系列操作来同步两者:
    • 添加 新增的虚拟服务和真实服务器。
    • 更新 现有虚拟服务(例如,更改调度算法)或真实服务器(例如,更改权重或转发方法)。
    • 删除 不再存在的虚拟服务和真实服务器。
    • 对于 Service 的 ClusterIP,kube-proxy 还会确保该 IP 地址被绑定到节点的一个网络接口上(通常是一个 dummy 接口,如 kube-ipvs0),以便内核能够正确地接收发往该 ClusterIP 的流量。
  5. 健康检查: 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:

  1. 用户空间进程创建一个 Netlink socket。
  2. 通过 sendmsg()recvmsg() 函数发送和接收 Netlink 消息。
  3. Netlink 消息包含一个头部(nlmsghdr)和实际的数据负载。数据负载通常包含一个或多个属性(nlattr),用于传递具体的配置信息。

IPVS 模块通过 Netlink 的 NETLINK_GENERIC 家族(一个通用的 Netlink 家族,通过 protocolcommand 字段区分不同的子协议)进行通信。

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` 工具已安装在你的系统上。

代码解析:

  1. ipvs.Service 结构体:
    • NetID 字段封装了虚拟服务的网络标识(协议、IP 地址、端口)。
    • Scheduler 字段指定了负载均衡算法(如 "rr")。
    • Flags 字段可以用于设置 IPVS 服务的其他属性,如持久性。
  2. ipvs.Destination 结构体:
    • Service 字段指向它所属的 ipvs.Service 对象。
    • NetID 字段封装了真实服务器的网络标识(IP 地址、端口)。
    • Weight 字段用于加权调度算法。
    • FwdMethod 字段指定了转发方法,ipvs.FwdMethodMasq 对应 NAT 模式。
  3. 核心 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 库在底层做了什么?

  1. 它会构建一个 Netlink 消息,消息头包含 NETLINK_GENERIC 家族标识、RTM_NEWSERVICERTM_SETSERVICE 命令(对应添加或更新服务)。
  2. 消息的数据部分会被编码为一系列 nlattr(Netlink Attribute)结构体,这些属性包含了 ipvs.Service 结构体的各个字段(如 IP、端口、协议、调度器)。
  3. 这个 Netlink 消息通过 Netlink socket 发送到内核。
  4. 内核的 IPVS 模块接收并解析消息,然后相应地修改其内部的 IPVS 表。
  5. 操作成功或失败的响应也会通过 Netlink socket 返回给用户空间进程。

这个过程对于 netlink 库的用户来说是透明的,我们只需关注 Go 结构体和函数调用即可。

规模化与性能:IPVS 的优势

IPVS 模式的 kube-proxy 在大规模集群中展现出卓越的性能和扩展性:

  1. 哈希表查找性能: IPVS 在内核中维护了一个哈希表来存储虚拟服务。当数据包到达时,内核可以直接通过目标 IP、端口和协议的哈希值快速定位到对应的虚拟服务,然后根据调度算法选择后端真实服务器。这种 O(1) 或接近 O(1) 的查找速度,相比 iptables 的线性遍历(O(N),N 为规则数量)具有显著优势,尤其是在规则数量庞大时。
  2. 更低的 CPU 开销: 由于快速查找和直接在内核中处理负载均衡逻辑,IPVS 模式的 CPU 占用通常低于 iptables 模式。
  3. 高效的连接跟踪: IPVS 自身维护了连接状态,它可以在内核空间高效地进行连接跟踪,并将连接粘性(session affinity)直接应用于 IPVS 连接表。这减少了对 netfilter conntrack 表的压力,避免了 conntrack 溢出等问题,从而提高了整体网络稳定性。
  4. 增量更新: kube-proxy 在 IPVS 模式下可以执行增量更新。当 ServiceEndpoint 发生变化时,它只添加、更新或删除受影响的 IPVS 条目,而不是像 iptables 模式那样需要刷新整个规则集。这使得规则同步更快,对网络中断的影响更小。
  5. 多种调度算法: 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 的实践

  1. Session Affinity (会话保持):
    Kubernetes ServicesessionAffinity: 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,                 // 持久化超时时间(秒)
    }
  2. 健康检查与权重调整:
    kube-proxy 会持续观察 EndpointSliceEndpoint 对象中后端 Pod 的 Ready 状态。当一个 Pod 变为 NotReady 时,kube-proxy 会更新其对应的 IPVS 真实服务器:

    • 移除: 直接从 IPVS 表中删除该真实服务器。这是最常见的做法。
    • 权重设为 0: 将该真实服务器的 Weight 设为 0。这样,调度器就不会再向其发送新连接,但现有的连接会继续保持,直到超时。这在某些平滑下线场景中可能有用。
  3. IPVS Forwarding Methods 的选择:
    如前所述,IPVS 支持 NAT、TUN、DR。kube-proxy 默认且主要使用 NAT 模式(FwdMethodMasq)。这是因为它最简单,对后端 Pod 的网络配置没有特殊要求,并且能够很好地处理 Pod 跨节点甚至跨宿主机的场景。

kube-proxy 内部 IPVS 模式的实现细节

kube-proxy 的源代码中,IPVS 模式的实现位于 pkg/proxy/ipvs 目录下。

  1. Proxier 接口: kube-proxy 核心逻辑通过 Proxier 接口抽象了不同代理模式的实现。ipvsProxier 结构体实现了这个接口。
    // pkg/proxy/proxier.go
    type Proxier interface {
        // ...
        SyncLoop()
        Sync()
        // ...
    }
  2. syncLoop() 这是 kube-proxy 的主循环,它负责监听 Kubernetes API Server 的事件,并在检测到 ServiceEndpointSlice 变化时触发同步操作。
  3. syncProxyRules() 这是 ipvsProxier 的核心方法,它负责实际的 IPVS 规则计算和更新。
    • 它会从 kube-proxy 内部维护的 ServiceMapEndpointsMap 中获取最新的服务和后端信息。
    • 然后,它会遍历这些数据,构建出期望的 ipvs.Serviceipvs.Destination 列表。
    • 通过与当前内核中的 IPVS 状态(通过 ipvs.ServiceList()ipvs.DestinationList() 获取)进行比较,计算出需要添加、更新或删除的 IPVS 条目。
    • 最后,它会调用 netlink/ipvs 库提供的 ServiceAdd/Update/DelDestinationAdd/Update/Del 函数来更新内核 IPVS 表。
  4. ipvs.Manager ipvsProxier 内部通常会封装一个 ipvs.Manager 类似的组件,负责与 netlink/ipvs 库交互,处理 IPVS 规则的增删改查。这个 Manager 会处理 Netlink socket 的创建和关闭,以及错误处理等底层细节。
  5. 并发与锁: 由于 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 网络原理和排查相关问题至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注