从内核协议栈到生产级四层负载均衡架构的实战解析

一、IPVS架构概览

在现代数据中心架构中,四层负载均衡是基础设施的关键组件。许多人在提到负载均衡时首先想到Nginx、HAProxy这类七层方案,但在流量洪峰面前,内核级的四层负载均衡才是性能天花板最低、吞吐能力最强的选择。

IPVS(IP Virtual Server)是Linux内核Netfilter框架之上的四层负载均衡子系统,自Linux 2.4.x起就合入主线内核,至今仍是kube-proxy IPVS模式、Keepalived、LVS(Linux Virtual Server)等基础设施的核心引擎。

IPVS的核心架构可分为三层:

• Netfilter钩子层:在 NF_INET_LOCAL_IN 和 NF_INET_FORWARD 挂载钩子,截获发往Virtual Service的流量

• 调度器层:根据配置的调度算法从Real Server池中选择一个后端

• 转发层:通过NAT、DR(Direct Routing)、TUNNEL、FQDN四种模式将数据包转发出

这种架构使IPVS的转发完全不进入用户态协议栈,每个数据包仅经过一次Netfilter遍历即可完成调度与转发。

二、核心数据结构

2.1 虚拟服务与真实服务器

IPVS通过三个核心结构体描述其服务对象:

// include/net/ip_vs.h
struct ip_vs_service {
    struct list_head         s_list;        // 全局服务链表
    struct list_head         f_list;        // 防火墙标记链表
    struct list_head         d_list;        // 目标广告链表
    
    __u16                    af;            // 地址族 (AF_INET/AF_INET6)
    __u16                    protocol;      // IPPROTO_TCP/UDP
    __be32                   addr;          // 虚拟IP地址
    __be16                   port;          // 虚拟端口
    __u32                    fwmark;        // 防火墙标记
    
    struct list_head         destinations;  // 真实服务器链表
    struct ip_vs_scheduler  *scheduler;     // 调度算法
    
    atomic_t                 refcnt;        // 引用计数
    rwlock_t                 lock;          // 服务级读写锁
    struct rcu_head          rcu_head;      // RCU释放
};

struct ip_vs_dest {
    struct list_head         n_list;        // 服务目标链表节点
    struct list_head         d_list;        // 全局目标链表节点
    
    __u16                    af;
    __be32                   addr;
    __be16                   port;
    
    atomic_t                 weight;        // 调度权重
    atomic_t                 activeconns;   // 活跃连接数
    atomic_t                 inactconns;    // 非活跃连接数
    atomic_t                 persistweight; // 持久化权重
    
    struct ip_vs_stats       stats;         // 统计计数器
    
    struct rcu_head          rcu_head;
};

关键设计要点:

  • 读写锁(rwlock_t):读多写少的场景下,更新目标时允许并发遍历连接表
  • RCU释放:service和dest结构体通过RCU延迟释放,避免正在处理的数据包访问已释放内存
  • 原子计数器:activeconns/inactconns使用原子操作,调度器可在不加锁的情况下读取以做出调度决策

2.2 连接跟踪表

IPVS维护自己的连接哈希表(ip_vs_conn_tab_size),用于将同一连接的所有包转发到同一个Real Server:

struct ip_vs_conn {
    struct hlist_node        c_list;        // 哈希桶链表
    __u16                    af;
    __be32                   caddr;         // 客户端IP
    __be32                   vaddr;         // 虚拟IP
    __be32                   daddr;         // 真实服务器IP
    __be16                   cport;
    __be16                   vport;
    __be16                   dport;
    
    __u16                    protocol;
    atomic_t                 refcnt;
    struct timer_list        timer;         // 连接超时定时器
    spinlock_t               lock;
    
    struct ip_vs_dest       *dest;          // 绑定的真实服务器
    void                    *app_ctrl;      // 应用层控制数据
    
    struct rcu_head          rcu_head;
};

连接表的实现有几个显著的工程考量:

  • 哈希冲突处理:采用链式哈希(hlist),默认表大小可通过 conn_tab_size 调优
  • 连接 affinity:同一五元组的报文始终映射到同一后台,即使调度算法后续变化
  • 超时管理:TCP会话有ESTABLISHED/TIME_WAIT等不同超时,UDP设定固定流超时(默认300秒)
  • 定时器淘汰:低负载时定时器到期自动回收,高负载时通过lazy purge避免遍历整个表

三、调度算法深度分析

IPVS提供十余种内置调度算法,每种都针对不同场景优化。我们以三种最典型的算法剖析其设计哲学。

3.1 轮询调度 (Round Robin)

static struct ip_vs_dest *ip_vs_rr_schedule(struct ip_vs_service *svc,
                                              struct sk_buff *skb,
                                              const struct ip_vs_iphdr *iph)
{
    struct ip_vs_dest *dest;
    struct list_head *p = &svc->destinations;
    
    write_lock_bh(&svc->sched_lock);
    p = p->next;
    dest = list_entry(p, struct ip_vs_dest, n_list);
    
    /* Simple round-robin: 直接取下一个 */
    if (list_is_last(p, &svc->destinations))
        p = svc->destinations.next;
    
    write_unlock_bh(&svc->sched_lock);
    return dest;
}

轮询的核心逻辑极其简洁:维护一个链表游标,每次调用返回下一个目标。这种设计在权重相同时是最优选择——O(1)决策时间、零状态依赖。

但轮询的一个重要bug来源是:当目标池变化(增删节点)时,游标的语义变得不清晰。当前的实现方式是读写锁保护下的简单移动。

3.2 加权最小连接 (Weighted Least Connection)

这是生产环境中最常用的调度算法,其决策逻辑如下:

static struct ip_vs_dest *ip_vs_wlc_schedule(struct ip_vs_service *svc,
                                               struct sk_buff *skb,
                                               const ip_vs_iphdr *iph)
{
    struct ip_vs_dest *dest, *least;
    int loh, doh;
    
    /* 遍历所有dest负载最轻的 */
    list_for_each_entry_rcu(dest, &svc->destinations, n_list) {
        if (!(dest->flags & IP_VS_DEST_F_AVAILABLE))
            continue;
        if (atomic_read(&dest->weight) == 0)
            continue;
        
        least = dest;
        loh = atomic_read(&least->activeconns) * 50 / atomic_read(&least->weight);
        
        /* 检查是否过载 */
        list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) {
            if (!(dest->flags & IP_VS_DEST_F_AVAILABLE))
                continue;
            
            doh = atomic_read(&dest->activeconns) * 50 / atomic_read(&dest->weight);
            if (doh < loh) {
                least = dest;
                loh = doh;
            }
        }
        return least;
    }
    return NULL;
}

核心公式:overhead = activeconns * 50 / weight

这里乘50是为了避免整数除法精度损失(将分数放大后再比较)。选择开销最小的目标。

这一算法的关键观察:

  • 连接成本假设:假设每条活跃连接消耗相同的资源,实际中这可能不成立(如长短连接混合场景)
  • 权重归一化:通过除以weight实现正比于能力的调度,10台2核机器与5台4核机器效果等价
  • O(V)复杂度:每次调度需要遍历所有目标,目标数在百级以下时表现良好

3.3 源地址哈希 (Source Address Hash)

static inline unsigned int ip_vs_sh_hashkey(int af, const union nf_inet_addr *addr,
                                             __be16 port, unsigned int mask)
{
    __u32 addr_fold = addr->ip;
    
    if (af == AF_INET6)
        addr_fold = addr->ip6[0] ^ addr->ip6[1] ^ addr->ip6[2] ^ addr->ip6[3];
    
    return (ntohs(port) ^ ntohl(addr_fold)) & mask;
}

源地址哈希通过客户端IP+端口的目标地址计算哈希值,在固定的映射表中找到对应的桶位,从而得到目标服务器。

这个算法的工程价值在于:

  • 会话亲和性:同一客户端的连续请求总是路由到同一后端
  • 零状态:不需要维护per-client的状态,仅需要一张静态映射表
  • 与连接表配合:哈希仅决定首次选择的dest,后续由连接表固化

在实际部署中,当动态增减后端时,哈希映射会导致约1/N的连接重新路由。为了缓解这个问题,IPVS实现了Sticky Entry机制——变化的池中保留旧dest一段时间,让已建立的连接继续工作。

四、数据包处理流程

4.1 Netfilter钩子注册

IPVS在内核中注册了两个Netfilter钩子:

static struct nf_hook_ops ip_ops[] = {
    {
        .hook     = ip_vs_xmit,
        .pf       = PF_INET,
        .hooknum  = NF_INET_LOCAL_IN,    // 进入本机目的地址为VIP的包
        .priority = NF_IP_PRI_NAT_SRC - 2,
    },
    {
        .hook     = ip_vs_in_icmp,
        .pf       = PF_INET,
        .hooknum  = NF_INET_LOCAL_IN,
        .priority = NF_IP_PRI_NAT_SRC - 1,
    },
};

4.2 入站处理流程

当一个发往VIP的数据包进入内核时,触发路径如下:

Packet arrives at VIP
       |
       v
NF_INET_LOCAL_IN hook (ip_vs_local_in_handler)
       |
       v
ip_vs_in()
  |- ip_vs_schedule()
  |     |- 先查连接池,命中则直接复用
  |     |- 未命中则调用scheduler->schedule()
  |     |- 创建新连接记录
  |     |- 设置dest引用
  |
  |- 根据packet_type选择转发方式
  |     |- IP_VS_CONN_SNAT  : SNAT转发(修改源地址->NAT)
  |     |- IP_VS_CONN_DNAT  : DNAT转发(修改目的地址)
  |     |- IP_VS_CONN_MASQ  : MASQUERADE (旧NAT)
  |     |- IP_VS_CONN_LOCAL : 本机返回
  |
  |- 调用dest->packet_xmit()转发包
       |
       v
dev_queue_xmit() -> 网卡发送

关键路径上的几个重要检查:

  • 连接亲和性检查:查找现有连接记录,命中则跳过调度器直接复用目标
  • 调度器选择:未命中连接时调用注册的调度算法选择目标
  • 反向NAT构造:将从Real Server返回的报文,源地址还原为VIP

4.3 转发模式详解

IPVS支持三种主要转发模式,各有不同的工程取舍:

模式 网络要求 性能 RS修改 适用场景
NAT 跨网段 中 无 小规模、复杂拓扑
DR 同网段 高 ARP抑制 高性能要求
TUNNEL 三层可达 中 IPIP模块 异地容灾、跨AZ

实施指导:

  • NAT模式最经典,出向报文源地址改为Director IP(SNAT),返回报文回流Director后再修改。额外的往返增加了Director的带宽压力
  • DR模式(Direct Routing)在性能场景中最常见:Director仅修改目的MAC即转发,RS直接以VIP为源回复客户端,绕开了Director的回程流量瓶颈
  • TUNNEL模式将报文封装在IPIP隧道中,使RS与Director可以跨三层网络部署

五、生产环境部署

5.1 传统LVS+Keepalived架构

经典的LVS-DR部署需要如下网络配置:

# 在Director上配置VIP(浮动IP)
ip addr add 192.168.1.100/32 dev lo

# 启用IP转发
sysctl -w net.ipv4.ip_forward=1

# 防止RS回复ARP查询(关键!在RS上配置)
echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce

# 将VIP绑定到lo接口(RS不响应VIP的ARP,但接受VIP作为目的地址)
ip addr add 192.168.1.100/32 dev lo

RS上的核心参数语义:

  • arp_ignore=1:只响应目的地址为本接口IP的ARP请求
  • arp_announce=2:发送ARP时强制使用本接口的IP作为源地址

5.2 kube-proxy IPVS模式

在Kubernetes生态中,kube-proxy的IPVS模式在生产环境得到广泛应用。以下是一个典型的IPVS规则配置:

# 添加虚拟服务(VIP:WPORT)
ipvsadm -A -t 10.96.0.10:443 -s wlc

# 添加真实服务器池
ipvsadm -a -t 10.96.0.10:443 -r 10.244.1.2:6443 -m
ipvsadm -a -t 10.96.0.10:443 -r 10.244.2.3:6443 -m
ipvsadm -a -t 10.96.0.10:443 -r 10.244.4.5:6443 -m

# 查看当前规则
ipvsadm -L -n --stats

kube-proxy IPVS模式相比iptables模式的核心优势:

维度 iptables模式 IPVS模式
规则数量 O(N*M) 线性增长 O(N+M) 分离存储
增量更新 全量重写规则链 增量增删dest
调度能力 随机等概率 rr/wrr/lc/wlc/sh/dh等
规模上限 ~5000服务 ~100000+服务

在万级Service数量下,iptables模式因规则表膨胀导致延迟不可接受,此时IPVS成为必选项。

5.3 连接表性能调优

IPVS的连接跟踪表大小直接决定最大并发连接数:

# 查看当前连接表大小
cat /proc/sys/net/ip_vs_conn_tab_size  # 默认 12 (4096桶)

# 调整到21 (2百万桶)
echo 21 > /proc/sys/net/ip_vs_conn_tab_size

另一个关键调优参数是连接超时时间:

# TCP会话超时(默认15分钟),可适当调短以快速回收连接
sysctl -w net.ipvs.tcp_timeout=900

# TCP FIN超时(默认1分钟)
sysctl -w net.ipvs.tcp_fin_timeout=30

# UDP流超时(默认5分钟),UDP无连接需要更长超时
sysctl -w net.ipvs.udp_timeout=300

六、内核实现中的核心优化技巧

6.1 零拷贝转发

IPVS的转发路径尽量避免了数据包复制。在NAT模式下,虽然需要修改IP头中的地址和checksum,但这在sk_buff只读修改即可完成,不会触发kmem_cache_alloc。

6.2 RCU读多写少优化

目标列表(destinations)的遍历使用RCU(list_for_each_entry_rcu),使得调度器选择目标时无需获得读锁。只有在增删目标或服务参数修改时才会获取写锁,这对连接调度这种每包必过的热路径极为关键。

6.3 每CPU统计

IPVS的统计计数器使用每CPU变量:

struct ip_vs_stats {
    __u32                   conns;        // 连接建立数
    __u32                   inpkts;       // 入包数
    __u64                   inbytes;      // 入字节
    __u32                   outpkts;
    __u64                   outbytes;
    
    __u32                   cps;          // 当前CPS
    __u32                   inpps;
    __u32                   outpps;
    __u64                   inbps;
    __u64                   outbps;
};

每个CPU核心维护独立的stats结构,读取时通过smp_rmb配合cur针刺(seqcount)获取快照,避免了多核争用。

6.4 无锁连接查找

连接表的查找路径经过精心设计:

• 计算哈希索引(基于五元组)

• 读取对应哈希桶的头指针(RCU读保护下)

• 遍历链表匹配五元组

整个过程不需要获取任何锁,仅依赖RCU保证哈希桶结构不被释放。写入时才需要桶级自旋锁。

七、常见问题与排障

7.1 连接被重置或丢弃

# 检查VS和RS状态
ipvsadm -L -n

# 检查连接表是否已满
cat /proc/net/ip_vs_conn | wc -l
cat /proc/sys/net/ip_vs_conn_tab_size

# 查看drop计数
watch -n1 'cat /proc/net/ip_vs_stats'

7.2 Director成为瓶颈

当观察到以下现象时,应考虑切换到DR/TUNNEL模式:

  • CPU使用率集中在软中断(si)
  • rx_missed_errors 网卡计数器快速增长
  • 回程流量与去程流量接近2:1(NAT模式特征)

7.3 ARP震荡

在DR模式下,错误的ARP配置会导致严重的Network flapping:

# 检查ARP状态
ipvsadm -L --daemon  # 显示是否开启了sync daemon

# 确保RS上的arp_ignore/arp_announce配置正确
cat /proc/sys/net/ipv4/conf/all/arp_ignore
cat /proc/sys/net/ipv4/conf/all/arp_announce

7.4 监控指标采集

通过读取 /proc/net/ip_vs 和 /proc/net/ip_vs_stats 可获取完整的运行时指标。在eBPF场景下,还可以通过kprobe挂载到 ip_vs_schedule() 来追踪调度延迟。

八、总结

IPVS作为Linux内核网络栈中最成熟的负载均衡子系统,历经二十余年生产验证。其工程实现中的几个设计理念值得所有系统软件开发者学习:

• 零状态热路径:连接查找无锁化、调度决策最小化

• RCU优先:读者路径完全RCU化,写者仅在冷路径加锁

• 每CPU隔离:统计计数器避免跨核争用

• 模块化设计:调度算法可插拔,转发模式可组合

在云原生时代,随着Kubernetes的广泛采用,IPVS作为kube-proxy的底层引擎正在被更多工程师所使用。理解IPVS的内部机制,对于排查复杂网络问题、优化容器网络性能,都有着不可替代的价值。


相关技术:Netfilter、LVS、Keepalived、kube-proxy IPVS mode、eBPF网络观测

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部