从内核协议栈到生产级四层负载均衡架构的实战解析
一、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网络观测

发表评论 取消回复