做网络和运维的朋友应该都跟 LVS 打过交道。这套基于 Linux 内核的负载均衡方案,网上讨论最多的是 NAT、DR、TUN 三种模式怎么选、转发性能能跑到多少,但我自己这些年折腾下来,真正让线上集群翻车的往往不是转发速度,而是调度算法。有几次大半夜爬起来看 ipvsadm 输出,都是因为后端服务器负载明显不均,最后定位来定位去,问题都落在调度算法选型和对连接统计的理解上。
这篇文章打算把 LVS 调度算法这块掰开揉碎,不讲安装教程,重点讲清楚几件事:每类算法到底靠什么做决策、适用于什么业务、有哪些反直觉的坑,以及线上出现调度失衡之后怎么一步步定位和处理。适合正在用 LVS 做负载均衡、或者准备从其他四层代理工具迁移到 LVS 的读者。看完之后,你至少能根据业务特征直接列出候选算法,并且知道用什么命令观察调度效果。
1. 从一次失衡排查谈起:调度器在连接建立那一刻做了什么
1.1 我遇到的那次“流量均匀却负载不均”
先说一个印象非常深的案例。某次压测,后端两台机器配置完全相同,ipvsadm -L -n看连接数基本五五开,轮询算法也没有配置错误。但一台机器 CPU 已经跑到 90%,另一台只有 10%。当时第一反应是 LVS 出了问题,后来在客户端抓包才发现,压测工具维护着连接池,大量请求复用了少数几条长连接,而这几天连接恰好都被调度到了一台 RS 上。
也就是说,LVS 层面的“调度”是公平的,但业务层面的请求分布完全由连接建立模型决定。这个案例给我的印象特别深:如果对 LVS 的调度粒度没有清晰认知,很多失衡问题都会排查错方向。你可能会说这种事太巧了,但生产环境里客户端连接池、服务间 RPC 长连接、数据库中间件复用连接,都是同样的原理,一点也不罕见。
1.2 LVS 调度到底发生在哪个环节
LVS 的转发路径有好几种,但不管 NAT、DR 还是 TUN,调度这件事都发生在同一个位置:当一个新的 TCP 连接(对 TCP 而言就是第一个 SYN 包)到达 Director 时,Director 会调用注册在内核里的调度器,从当前存活的 RealServer 列表里选一台,然后把这条连接的转发关系写进连接跟踪表。之后这条连接的所有报文,都是直接查表转发,不再经过任何调度算法的计算。
换句话说,调度算法只决定“新连接去哪儿”,不参与“已建立连接怎么转发”。这里有一个非常关键的推论:调度算法的输入是“连接建立事件”,而不是“请求报文”。所以判断一个算法适不适合你的业务,核心要看业务的连接特征——连接是长是短、请求是否复用连接、连接数能不能代表后端压力,而不是看单连接的吞吐量或报文大小。很多人在社区里问“为什么 LVS 不按 CPU 负载调度”,根源就是没理解这个环节。LVS 的内核调度器根本不看后端负载,它只能看自己维护的连接表里这个有限的信息维度。
NAT、DR、TUN 三种模式的区别在于数据报文的转发路径和封装方式:NAT 模式下请求和响应都经过 Director,适合中小规模集群;DR 模式通过改写 MAC 让响应直接回客户端,性能上限最高,但要求后端和 Director 在同一个二层网络;TUN 模式用 IP 封装隧道解决跨网段问题,但会带来额外封装开销。有一点需要特别注意:不管用哪种模式,调度算法的选择范围都是一样的。工作模式解决的是“怎么把包送过去”的问题,调度算法解决的是“把新连接送给谁”的问题,排查时如果把这两件事混在一起,很容易绕进死胡同。
2. 静态调度算法:轮询、加权轮询与哈希的适用边界
2.1 rr 和 wrr 的“平均”到底指什么
rr 是所有算法里最好理解的:新连接挨个轮流分配给各台 RS,第 1 个去 RS1,第 2 个去 RS2,循环往复。它不读任何状态,内核开销最小。但“轮询”保证的是连接数量的平均分布,不是流量的平均分布。在短连接场景下,连接数量、请求数量、流量三者强相关,rr 表现非常好;一旦业务出现大量长连接,问题就来了——先建立的连接会长时间占用某几台 RS,而新连接依然被均匀分给所有 RS,于是那些被长连接“占座”的服务器,实际可处理的并发请求反而更少。压测工具连接池引起的分布不均,就是这个原理。
wrr 在 rr 的基础上引入了权重。假设 RS1 的权重是 4,RS2 是 1,那么调度器每轮会尽量按 4:1 的比例分配连接。权重怎么设,我的经验是从硬件能力出发:按 CPU 核数、内存容量、网卡吞吐综合评估。比如一台 8 核的机器对一台 2 核的机器,起步权重可以设为 4:1,然后再根据线上观测微调。这里特别想提醒一点:很多人在负载不均时第一反应是“换算法”,但在后端能力有明显差异的集群里,先把 wrr 或 wlc 的权重调准,往往比换算法更有效。权重是调度器唯一能感知“后端能力差异”的入口,不会用权重就谈不上精确调度。
2.2 dh 和 sh:哈希调度换来的“亲和”与埋下的“热点”
dh 是根据目标 IP 做哈希,同一目标 IP 的新连接始终落在同一台 RS 上。这个特性的典型应用场景是后端缓存集群:如果不同业务请求的目标 IP 被映射到不同缓存节点,同一个目标的访问会稳定落在同一台缓存实例上,缓存命中率会明显提升。但 dh 有一个大坑——它不是一致性哈希。RS 列表只要发生变化(扩容、缩容、宕机),哈希取模结果的重新分布会让大量映射关系跟着变,原本固定在一台 RS 上的连接会大面积迁移。对缓存类业务来说,这可能瞬间击穿缓存。所以使用 dh 的集群,扩缩容动作必须非常谨慎,最好在低峰期操作。
sh 是根据源 IP 做哈希,作用是让同一个客户端总是调度到同一台 RS,常被用来做会话保持。但 sh 能不能用,取决于源 IP 的分布是否足够分散。我见过一个案例:某个办公网出口做了 NAT,几千个用户对外只显示同一个源 IP,sh 算法下这些用户的所有连接全被分到了一台 RS 上,其他 RS 闲得没事干。这个场景下应该改用其他会话保持方式,或者让出口 NAT 改成多 IP。另一方面,sh 也不关心后端负载,如果某个源 IP 是超大规模流量入口,比如被高频调用的开放接口,哈希会将这个热点完整地送给对应那台 RS,形成单点压力。别指望哈希算法做负载均衡,它的核心价值是“亲和”。
3. 动态调度算法:连接数统计公式里的门道
3.1 lc 与 wlc:默认算法的短板
动态调度算法的共同特点是:每次调度时都去看看当前各台 RS 的连接状态,再从中挑一台。lc 的逻辑最简单,谁当前连接数最少,新连接就给谁。wlc 则是 lc 的加权版,它选的是“连接数与权重比值最小”的那台。LVS 内核里 wlc 的计算公式是(active*256 + inactive) / weight,各台 RS 取最小值。公式里 active 是活跃连接数,inactive 是不活跃连接数,把它们都算进来,是为了避免分配完连接就立刻被释放导致判断波动,256 这个系数则让活跃连接在决策里比重更大。正因为这个公式既能覆盖大多数业务又计算简单,ipvsadm在创建虚拟服务时不指定调度算法,默认就是 wlc。
但 wlc 有一个非常反直觉的点:它假设“连接数越少,后端越空闲”,这个假设在长连接和请求复用场景下经常失效。举一个实际见过的例子:一组 API 服务,后端机器处理能力完全一致,用 wlc 跑高并发短请求。很快发现其中一台机器 CPU 被打满,其他机器还有余量。原因是那台机器处理请求特别快,每次它的活跃连接数都最先降到低位,调度器于是不停地给它派新连接。它越处理越快,被派得就越多,最终把自己压垮了。连接数不是负载,这个教训我是用一次线上问题换来的。在后端处理速度差异大的短连接场景里,wlc 反而可能放大失衡。
3.2 sed 和 nq:为“不要排队”设计的改进
sed 全称是 Shortest Expected Delay,它把目标改成“期望延迟最小”。公式是(active + 1) * 256 / weight,取最小值的 RS。相比 wlc,它多算了“新连接即将带来的一次处理任务”,可以理解成“当前这台 RS 的期望响应时间”。如果某台 RS 的活跃连接数已经很高,即使它的权重也很大,sed 也不会轻易把新连接塞给它,因为再塞过去,用户体验已经劣化了。所以 sed 更适合那些连接处理时间相对固定、连接数能比较好反映后端压力的业务。
nq 是在 sed 基础上加了一条优先规则:如果存在完全空闲的 RS,active 和 inactive 都为 0,那就直接发给它,不用比较公式;只有所有 RS 都有连接时,才退回到 sed 逻辑。这个“先把空闲机器塞满再排队”的思路,在突发流量到来时特别有用。比如业务高峰刚起步,后端几十台机器大部分是空的,nq 会迅速把请求摊到所有空机器上,避免流量先堆在前几台被轮询到的机器。但要注意,nq 只关心“有没有空闲 RS”,如果业务全是长连接,RS 几乎不会进入完全空闲状态,nq 就基本等价于 sed。
3.3 lblc 和 lblcr:为目标地址服务的动态亲和
lblc(Locality-Based Least-Connection)稍微特殊一点,它不是为源 IP 亲和,而是为目标 IP 亲和。算法会为每个目标 IP 记录最近一次选中的 RS,后续该目标 IP 的新连接优先仍然去这台 RS,只有在这台 RS 的连接数明显高于整体水平时,才改用 wlc 逻辑另选一台。你可以把它理解成“带逃逸机制的 dh”。它的价值在于:既保留同一目标尽量落在同一台 RS 的亲和性,又避免某个热点目标把一台 RS 彻底打挂。所以缓存类业务如果觉得 dh 太死板,lblc 会平滑很多。
lblcr(Locality-Based Least-Connection with Replication)是 lblc 的进阶版,允许同一个目标 IP 对应多个候选 RS,类似“一台缓存不够就把它复制到备选列表”。新连接来了,优先使用最近使用的 RS,如果它过载就尝试备选列表里负载最小的一台,实在不行才走全局 wlc。代价是调度逻辑更复杂,每台 RS 的复制状态需要额外维护;后端扩缩容时,复制列表的更新节奏如果跟不上,会出现短暂的调度偏差。我的建议是:如果后端缓存规模不大,先用 lblc 就够了,lblcr 留给那些真需要多级缓存副本的集群,不要为了功能全选更复杂的算法。
4. 生产环境选型:按业务特征反向匹配调度算法
4.1 选型前先回答三个问题
很多读者上来就问“LVS 用哪个调度算法最好”,这个问题本身就问错了。调度算法没有银弹,只有匹配不匹配。我一般建议团队先回答三个问题。
第一,业务连接是短还是长?一个 HTTP 请求几十毫秒就结束,这算短连接;WebSocket、推送通道、数据库连接池一次挂几个小时甚至几天,就是长连接。第二,后端各台 RS 的能力是否一致?如果用了两代硬件混部,权重设置就直接决定了最终的调度上限。第三,业务是否有会话保持要求?也就是同一个客户端是否能接受被调度到不同 RS。这三个问题问完,候选算法基本就收敛了。
4.2 一张选型表与实测体会
根据这些年维护过的集群,我整理了一张选型表供参考。注意这里写的是适用场景,不是铁律,真正上线前还是要压测验证。
| 业务特征 | 推荐算法 | 关键点 |
|---|---|---|
| 短连接、无状态、RS 规格一致 | rr / wrr | wrr 权重设为 1 时等价于 rr,保留后续调权能力 |
| 短连接、RS 规格差异大 | wrr | 权重依据 CPU 核数、内存先定基线,再按实测微调 |
| 长连接、连接池复用 | nq / wrr | nq 优先填空闲机器,wrr 按预设比例控制 |
| 处理速度差异大的短请求高并发 | wrr / sed | 避免 wlc 的快机器被连续派单打爆 |
| 缓存集群,要求目标 IP 亲和 | dh / lblc | dh 简单但扩缩容代价大,lblc 有逃逸机制 |
| 多级缓存,允许副本复制 | lblcr | 适合后端数量较多、热点分散的场景 |
| 必须会话保持 | sh / persistent | sh 无超时,persistent 有保持窗口 |
| 拿不准、先用默认 | wlc | 常规业务能扛住,但要注意长连接场景 |
我自己实际用下来,最常翻车的是两列:长连接场景用了 wlc,以及会话保持用了 sh 却没注意源 IP 数量。前者会让连接数统计失真,后者会产生单点热点。如果业务特征刚好落在表格里这两类的交界处,宁可多花半天压测,也不要拍脑袋直接上线。
举个例子,之前做一个带实时推送功能的社区系统,后端有大量长连接,一开始用的 wlc,结果其中一台机器活跃连接数总是偏低,新连接不断涌过来,CPU 飙得很高。后来改成 wrr,按后端规格设成 1:1 权重,每台机器的连接分布马上稳定了。事后复盘,长连接模式下连接数本来就反映不了瞬时压力,再让调度器按连接数决策,天然就是错配。
4.3 会话保持:sh 和 persistent 别混为一谈
sh 算法的亲和是纯哈希的,只要后端 RS 列表不变化,它就永远不变化,没有超时概念。persistent 则是 LVS 提供的一个独立机制,创建虚拟服务时用-p参数指定保持时间,比如-p 3600表示在 3600 秒内,来自同一个源 IP 的新连接都会进入同一个持久性模板,强制发给第一次命中的那台 RS。
两者的主要区别有三个:sh 靠源 IP 哈希,persistent 靠模板记录;sh 没有过期时间,persistent 有;sh 在 RS 宕机时通过重新哈希来分配新连接,persistent 在保持窗口内会继续把新连接尽量发往原 RS,直到窗口过期。实际业务里,如果应用层已经用 Cookie、Token 或者共享 Session 存储解决了会话保持,LVS 这层就别再加 sh 或 persistent,加了只会让负载分布变差。我见过有人为了让推送系统不掉线,同时开了 sh 和 persistent,结果某个源 IP 段因为 NAT 出口集中,导致一台 RS 压力巨大。后来去掉 persistent 只保留 sh,并且让后端把会话信息放到共享存储,问题才缓解。会话保持的选择,本质是“你愿意为亲和付出多少均衡代价”的取舍。
5. 调度失衡的排查思路与调优实操
5.1 从 ipvsadm 统计看连接分布
线上怀疑调度有问题,第一件事是看 Director 上的 ipvsadm 统计。常用三种输出方式:
ipvsadm -L -n:查看每台 RS 的 ActiveConn 和 InActConnipvsadm -L -n --stats:查看累计连接数、进出字节数ipvsadm -L -n --rate:查看每秒连接数、每秒流量
怎么看呢?先看各台 RS 的 ActiveConn 分布。如果明显偏斜,直接怀疑权重配置或连接模型;如果 ActiveConn 分布正常但后端负载差异大,就要去 RS 上确认 CPU、内存、磁盘 IO,问题往往不在调度而在单机资源争抢。这里有一个容易误判的点:某台 RS 的 InActConn 很高时,不代表它空闲,可能是 TCP 连接处于 ESTABLISHED 状态但长时间没有活跃,比如客户端连接池挂着一堆长连接。这种情况,连接数统计是虚的,直接按连接数判断负载会踩坑。
5.2 常见“调度不均”的根因与处理链路
根据经验,调度不均基本逃不出下面几类原因,对号入座反而最快。
- 权重与机器能力不匹配。RS 硬件是两代产品混部,权重还都是 1。处理办法是先把权重改成按核数、内存比例设置,wrr 和 wlc 都支持。
- 长连接占座。rr 看似平均,但先建立的长连接长期占用某些 RS,新连接不断被轮询到其他机器。处理办法是改用 wrr 并配合连接数上限,必要时在后端加连接数告警。
- 连接池复用。客户端根本不新建连接,LVS 调度的粒度是连接不是请求。这种情况只能从客户端或业务层入手,LVS 层无法感知单条连接内的请求频率。
- 健康检查失效。RS 已经宕机但权重没有被置 0,新连接继续被调度过去。处理办法是检查健康检查组件的探测间隔和判定逻辑,故障 RS 要尽快从 ipvs 规则里摘掉。
- InActConn 堆积。TCP 超时参数设置不合理,连接表里积攒了大量已结束的连接。处理办法是按业务调整
ipvsadm --set的超时时间。
任何一次失衡排查,都建议按这个顺序走:先看 Director 连接统计,再看后端 RS 负载;先确认 RS 存活和权重,再讨论算法选择。不要一上来就动算法,先把变量控制住。
5.3 权重、超时参数和压测验证的经验
权重调整有一个容易忽略的细节:LVS 的权重只影响新连接分配,已经建立的老连接不会因为权重调低就被断开或迁移。所以想通过降低某台 RS 权重来给它“减负”,只能等现有连接自然结束,效果有明显的滞后性。如果某台 RS 已经明显过载,更快的办法是直接把它的权重设成 0,让它停止接收新连接,等存量连接消化完再恢复。
超时参数用ipvsadm --set tcp tcpfin udp调整。默认值我记得 TCP 是 900 秒,TCP-FIN 是 120 秒,UDP 是 300 秒。如果业务全是短连接,TCP 900 秒意味着很多早已结束的连接还会在 LVS 连接表里保留 15 分钟,这些条目占用内存不说,还会让 InActConn 虚高,干扰你对连接分布的判断。短连接场景建议把 tcp 和 tcpfin 调小,长连接场景反而要把 tcp 调大,避免正常长连接被提前回收。
最后说一个我自己的习惯:每次调整算法或权重之后,不要只看一两分钟就说结论。连接建立模型受业务流量周期影响很大,白天和夜里的分布可能完全不同,至少跑一个完整的压测周期,我通常压 30 分钟以上,再对比 ipvsadm 统计和 RS 监控图。另外我会把 Director 的 ipvsadm 输出和后端每台 RS 的连接数监控放在同一个看板里,一旦出现失衡立刻能对上号。做运维久了你会发现,调度算法本身并不难,难的是把“连接分配”和“真实负载”这两套数据在同一个时间轴上对照着看。