做负载均衡最怕的不是机器不够,而是“均衡”本身出了问题。
之前我在处理一个电商线上集群时,三台应用服务器配置一模一样,Haproxy用的还是默认的轮询,可监控面板上中间那台CPU已经冲到80%,另外两台只有30%。我一开始怀疑是Haproxy坏了,后来手动抓包才发现:轮询虽然把请求平均发给了三台机器,但有一个响应特别慢的第三方接口调用,总是在某一台机器上结束长连接,导致这台机器的连接被长时间占用。换用leastconn并适当调整超时参数后,集群负载肉眼可见地平了。
也是从那次开始,我把Haproxy的负载均衡算法当成一门正经功课去研究。Haproxy之所以在负载均衡领域坐稳“瑞士军刀”的位置,除了性能强、配置灵活,更关键的是它提供的算法覆盖了从四层到七层绝大多数的业务形态:轮询、最少连接、源地址哈希、URI哈希、随机、一致性哈希……每个算法都是一套调度哲学,用对了事半功倍,用错了容易把故障无限放大。
这篇就按我实际调研和线上验证过的顺序,把Haproxy负载均衡算法的原理、适用场景、配置姿势和踩坑记录完整整理一遍。
1. 算法在Haproxy中的定位:配置位置与调度边界
1.1 一条balance指令能出现在哪里
Haproxy的负载均衡算法不是写在代码里的固定逻辑,而是通过配置指令 balance 来声明。这条指令有三个可写的位置:defaults、listen、backend。它们的生效范围是逐层覆盖的:defaults 里写的算法是全局默认值,listen 或者 backend 里写了就覆盖默认值。
我见过不少新手把算法写进 frontend,然后用 default_backend 转发,发现怎么都不生效,其实就是没搞清楚配置层级。下面是一段常见写法:
haproxy复制defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
balance roundrobin
backend web_servers
balance leastconn
server web1 192.168.1.10:8080 check
server web2 192.168.1.11:8080 check
上面的配置里,defaults 中的 roundrobin 是默认算法,但 backend web_servers 里显式声明了 leastconn,最终这组后端实际使用的是 leastconn。
另一个容易忽略的点:backend 里可以针对不同服务器组使用不同算法,这一点在把多个前端入口复用同一个后端集群时特别有用。比如管理后台入口需要会话保持,可以用 source;用户前台入口没有会话要求,可以用 roundrobin。两个 frontend 各自指向同一个 backend 时,算法是在 backend 上全局生效的,所以如果入口不同、算法要求也不同,最好拆成两个 backend。
1.2 四层与七层的算法分界线
Haproxy 有两种运行模式:mode tcp 和 mode http。四层模式下,Haproxy 只解析到 TCP 层,能看到的信息是源IP、目的IP、端口;七层模式下,Haproxy 可以解析 HTTP 协议,能看到 URI、Header、Cookie、请求参数等。这个差异直接决定了哪些算法可用。
我整理了一张常用算法的层级可用性表:
| 算法 | 四层(mode tcp) | 七层(mode http) | 核心调度依据 |
|---|---|---|---|
| roundrobin | 支持 | 支持 | 加权轮询 |
| static-rr | 支持 | 支持 | 静态加权轮询 |
| leastconn | 支持 | 支持 | 当前连接数/权重 |
| first | 支持 | 支持 | 按序号优先填满 |
| source | 支持 | 支持 | 源IP哈希 |
| uri | 不支持 | 支持 | 请求URI哈希 |
| url_param | 不支持 | 支持 | 请求参数哈希 |
| hdr | 不支持 | 支持 | 指定请求头哈希 |
| rdp-cookie | 支持 | 不支持 | RDP Cookie哈希 |
| random | 支持 | 支持 | 加权随机 |
如果你在 mode tcp 下配置了 balance uri,Haproxy 启动时会直接报错,告诉你这个算法不能在 TCP 模式下使用。反过来,rdp-cookie 是为远程桌面协议场景设计的,通常在四层模式配合 mode tcp 使用。理解这条分界线,能帮你少走很多弯路。
1.3 权重和maxconn是算法的“调校参数”
算法本身只是调度策略,真正让算法贴合业务的是 weight 和 maxconn 这两个参数。weight 表示服务器权重,默认都是1,权重越大,算法会越倾向于把流量分给这台机器。maxconn 表示单台服务器的最大连接数,是硬性保护阈值。
在 roundrobin 里,权重决定分配比例;在 leastconn 里,权重是连接数的分母,也就是说连接数除以权重最小的机器会优先收到新请求;在 source、uri 这类哈希算法里,权重影响哈希映射的目标选择概率。maxconn 则像是一个安全阀,不管算法怎么选,单机连接数不能超过这个值,一旦达到上限,Haproxy 会把这台机器视为“满”,新请求会尽量分配给其他机器。
我见过很多生产事故,本质上是 weight 和 maxconn 的配置比例不协调。比如给一台 8 核机器配了 weight 8,给另一台 4 核机器配了 weight 4,看起来很合理,但 maxconn 都设成 1000,结果高权重机器连接堆满,低权重机器还闲着。后面我会专门讲这个坑,这里先记住:权重的比例要服务能力的比例,而 maxconn 也要跟随权重比例去设置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 静态算法拆解:roundrobin、static-rr与first
2.1 roundrobin:最常用的加权轮询
roundrobin 是 Haproxy 的默认算法,也是绝大多数人接触 Haproxy 时用的第一个算法。它的核心逻辑很简单:维护一个服务器队列,按照权重比例,把请求轮流发给每台后端服务器。比如两台服务器权重分别是 2 和 1,那么每 3 个请求里,前两台机器收到 2 个,后一台收到 1 个,形成一个调度周期。
Haproxy 的 roundrobin 实现里有个容易被忽视的细节:它不是“先让权重高的机器连续收 2 个,再让权重低的收 1 个”,而是尽可能平滑地穿插。这意味着权重高的机器不会被瞬间打满,权重低的机器也不会长时间空闲。这个平滑特性在请求到达率很稳定时尤其明显。
我看过不少介绍文章,写 roundrobin 的例子都是“服务器A权重2,服务器B权重1,那么两个请求去A,一个请求去B”,这个说法简化了很多。真实调度中,Haproxy 会在每个周期里动态调整分配顺序,尽量让短时间窗口内的流量也接近权重比例,这样急性子客户端的体验会好很多。
haproxy复制backend web_servers
balance roundrobin
server web1 10.0.0.1:8080 weight 4 maxconn 1000
server web2 10.0.0.2:8080 weight 2 maxconn 500
server web3 10.0.0.3:8080 weight 2 maxconn 500
上面配置里,web1 的权重是其他两台的两倍,所以按比例它会承担一半流量,另外两台各承担四分之一。maxconn 的数值我也特意保持了 4:2:2 的比例,这样权重高的机器被允许建立更多连接,不会出现“权重高但连接上限低”的尴尬局面。
2.2 static-rr:不做运行时调整的静态轮询
static-rr 和 roundrobin 的表面行为很像,都是加权轮询,但有个关键区别:static-rr 不支持运行时修改权重。也就是说,如果你想通过 Haproxy 的 stats socket 动态调整某台机器的权重,roundrobin 可以即改即生效,static-rr 做不到,它只在配置文件加载时计算一次调度顺序。
这个差异在什么场景下重要?很多运维脚本会通过 socat 命令在重保期间临时修改权重,把某台机器调低权重,让它慢慢排空连接。如果你用的是 static-rr,这个操作会失败或者不生效,导致脚本失效,这时候排查起来非常让人困惑。
static-rr 适合后端拓扑非常固定、完全不需要动态调整权重的场景。它省去了运行时的动态计算开销,但说实话,在现代服务器上,这个性能差异微乎其微。我自己的用法是:如果后端节点短期内不会变化,就用 static-rr 省一点维护心智;如果要做灰度发布或者需要频繁动态调权,就用 roundrobin。
2.3 first:先把前面的节点填满
first 算法比较反直觉,它不是为了“均衡”而设计的,而是为了让流量优先进入序号最小的后端服务器,当这台服务器的连接数达到 maxconn 后,流量才开始进入下一台。
举个例子:
haproxy复制backend overflow_servers
balance first
server primary 10.0.0.1:8080 maxconn 200
server backup 10.0.0.2:8080 maxconn 200
只要 primary 的连接数没到 200,所有新请求都会给 primary;只有当 primary 满了,请求才开始进入 backup。这个行为和“主备”模式很像,但不完全是主备,因为 backup 机器在 primary 满负荷时也会承担流量。
first 适合什么场景?我见过两种用法:一种是主备架构下希望优先使用主节点,主节点扛不住时自动溢出到备份节点;另一种是突发流量场景,希望新扩容的节点先不接入流量,等原节点压力上来后再逐步启用。它和“轮询”的思路完全不同,用的时候要想清楚业务是否真的需要这种“填满才能轮到下一个”的调度策略。
3. 动态调度算法:leastconn与random的适用边界
3.1 leastconn:按当前连接数做等开销分配
leastconn 的核心逻辑是:每次选择“当前活动连接数 / 权重”最小的服务器,把新请求交给它。这样做的目的是让每台后端服务器当前承担的开销尽可能接近,而不是机械地按请求个数轮流分。
为什么说“连接数”可以代表开销?因为在长连接场景下,连接数基本可以反映服务器正在处理的业务量。比如数据库连接池,应用服务器和数据库之间维持着一批长连接,连接数越多,数据库需要消耗的内存、CPU、文件句柄就越多。这种情况下,按连接数来分配新连接,比按请求轮询更贴近真实负载。
haproxy复制backend db_servers
mode tcp
balance leastconn
option tcp-check
server db1 10.0.0.1:3306 weight 4 maxconn 400
server db2 10.0.0.2:3306 weight 2 maxconn 200
server db3 10.0.0.3:3306 weight 2 maxconn 200
这里我同样把 maxconn 设置成和 weight 成比例。db1 权重是 4,maxconn 是 400;其余两台权重是 2,maxconn 是 200。这样 db1 在连接数达到 200 时,它的 conn/weight 是 50;而 db2 在连接数达到 100 时,conn/weight 也是 50。也就是说,连接数按比例增长时,所有机器的“负载指标”是同步的,调度器不会出现误判。
leastconn 最适合的场景是:WebSocket、IM 推送、数据库中间件、游戏长连接这类连接生命周期长的服务。对于普通的 HTTP API,请求往往是毫秒级完成,连接数会剧烈波动,leastconn 的感知反而滞后,这时候它不一定比 roundrobin 更准,还可能因为维护连接计数带来额外开销。
3.2 random:大规模节点下的加权随机
random 算法是 Haproxy 1.6 版本引入的,它的核心思路是:按照服务器权重进行随机采样,权重越高的机器被选中的概率越大。在实现层面,Haproxy 的 random 并不是简单的 rand() % n,它内部采用了一个基于哈希的伪随机函数,通过这个哈希值在服务器权重分布上做一次“加权选择”。
这个算法最大的优势是:不需要维护轮询状态,也没有轮询顺序带来的周期性。当后端服务器数量很大时,比如 20 台、30 台,roundrobin 的轮询周期会很长,某个时段可能集中在某几台机器上,而 random 天然避免了这种“周期性热点”。
haproxy复制backend api_servers
balance random
server api1 10.0.0.1:8080 weight 5
server api2 10.0.0.2:8080 weight 3
server api3 10.0.0.3:8080 weight 2
不过 random 也有它的脾气:在小节点场景下,随机性会导致短时间窗口内比例不均。比如只有 2 台机器,权重相同,理论上应该五五开,但随机事件里连续 3 个请求都落在同一台机器上的概率并不低。所以节点数量少于 5 台时,我一般还是用 roundrobin 更直观、更稳定。
3.3 判断场景:短连接还是长连接
很多朋友问我,leastconn 和 random 到底怎么选?我的判断逻辑很简单:先问自己,后端服务处理一个请求平均需要多久?
如果请求在几十毫秒到几百毫秒内完成,连接数波动很快,roundrobin 或 random 就足够;如果连接会保持几秒、几分钟甚至更久,比如 WebSocket 推送、数据库连接、FTP 传输,那就需要用 leastconn 来做“等开销”分配。
需要补充的是,即使选了 leastconn,也强烈建议给每台服务器设置 maxconn 作为保护阈值。没有 maxconn 时,调度器只看到连接数少就继续往这台机器扔请求,但每台机器都有处理上限,一旦超过这个上限,连接会在内核队列里排队,响应变慢,反而拖垮整条链路。maxconn 的作用不是限制流量,而是保护后端,给调度器一个明确的“满了”信号。
4. 哈希型算法:source、uri、url_param、hdr怎么选
4.1 source:源IP哈希与会话保持的经典手段
source 算法的原理很简单:取客户端源IP地址做哈希运算,把哈希结果映射到某个后端服务器上。同一个源IP的请求,只要后端节点列表不变,就会一直打到同一台机器。这天然实现了“同源IP会话保持”。
这个特性在什么场景下有价值?比如某些老业务系统,登录状态存在本机 Session 里,没有做 Session 共享,用户第一次请求落在 A 机器,登录后第二次请求也必须落在 A 机器,否则 Session 就丢了。这种业务无法快速改造,用 source 算法是最快的过渡方案。
haproxy复制backend legacy_web
balance source
hash-type consistent
server web1 10.0.0.1:8080
server web2 10.0.0.2:8080
但 source 算法有个著名的坑:NAT 出口环境。如果大量客户端都从同一个公网出口IP访问,比如公司办公网、校园网、数据中心出口,source 会把所有这些用户都哈希到同一台后端服务器,导致这台机器流量暴涨,其他机器闲着。这个问题不是调参能解决的,因为它本质上是因为哈希键(源IP)的数量太少,分布无法均匀。
遇到这种场景,我的建议是:如果只有少数几个出口IP,千万不要用 source;如果出口IP数量足够多且分布均匀,source 是个轻量有效的会话保持方案。同时配合 hash-type consistent,让后端节点上下线时尽量少影响已有映射关系。
4.2 一致性哈希:哈希算法的“稳定器”
提到哈希型算法,必然绕不开 hash-type 选项。Haproxy 支持两种模式:map-based 和 consistent。
map-based 是传统做法:把哈希值映射到一个固定大小的数组里,每个槽位对应一台服务器。它的优点是计算快、内存占用小,但缺点是后端服务器列表发生变化时,几乎所有的映射关系都会重新洗牌。比如你原来有 10 台机器,某一台宕机后,其他 9 台机器的哈希映射可能全部变化,这会导致大量会话跳变。
consistent 是一致性哈希:把服务器放到一个哈希环上,每个请求的哈希值在环上顺时针找最近的服务器。当一台服务器下线时,只有它负责的那一段哈希区间会转移到下一台机器,其他机器的映射关系保持不变。用个生活类比:环形停车场上每个入口固定对应一个停车位,某个停车位被围起来后,只有原来停这个位置的车辆会被引导到旁边车位,其他车位不受影响。
haproxy复制backend cache_servers
balance uri
hash-type consistent
server cache1 10.0.0.11:8080
server cache2 10.0.0.12:8080
在生产环境里,如果后端节点会频繁扩缩容,强烈建议使用 consistent。它能让缓存类业务的命中率在节点变化后不至于崩盘,也能让长连接会话尽量保持。代价是每次哈希查找比 map-based 稍慢一点,但对现代 CPU 来说,这个开销可以忽略。
4.3 uri与url_param:按内容把流量钉在指定节点
uri 算法是针对 HTTP 请求的 URI 做哈希,相同 URI 的请求会被分发到同一台后端服务器。它最适合的领域是缓存类业务,比如图片、静态文件、API 响应缓存。如果所有相同资源的请求都固定到同一台缓存节点,缓存命中率会大幅提高,后端源站的请求压力也会明显下降。
haproxy复制backend image_cache
balance uri
hash-type consistent
server img1 10.0.0.21:8080
server img2 10.0.0.22:8080
server img3 10.0.0.23:8080
默认情况下,uri 哈希只取请求路径部分,不包含查询参数。如果你想连查询参数一起纳入哈希,可以配置为 balance uri whole。还有个 len 参数,比如 balance uri len 5 表示只取 URI 前 5 个字符做哈希,适合路径前缀结构明显的业务。
url_param 算法更精细一点,它指定某个查询参数作为哈希键。比如 balance url_param user_id,那么所有携带 user_id=10086 的请求都会落到同一台后端机器。这在老系统无 Cookie、依赖请求参数做用户标识的场景下很实用,但要注意:如果请求里没有这个参数,Haproxy 会退回到轮询算法,这可能导致调度行为不符合预期,最好在测试环境确认一遍。
4.4 hdr和cookie:识别真实用户身份
hdr 算法允许你指定某个 HTTP 请求头作为哈希键。比如 balance hdr(X-User-ID),如果上层网关已经注入了 X-User-ID 这个请求头,Haproxy 就可以按真实用户ID做会话保持。这比 source 按IP更精确,因为同一个出口IP下可能有很多不同用户,但按用户ID哈希就能把它们分散到不同后端。
haproxy复制backend user_api
mode http
balance hdr(X-User-ID)
hash-type consistent
server api1 10.0.0.31:8080
server api2 10.0.0.32:8080
但 hdr 有个安全前提:这个请求头必须是可信的。如果客户端可以自行伪造 X-User-ID,哈希就会失真,甚至被利用来做流量攻击。所以生产环境中,要么在边缘网关把原始请求头覆盖掉,注入可信的用户标识,要么只在内部网络链路里使用。
cookie 是一种更专业的会话保持方案。它不是独立的 balance 算法,而是与 roundrobin、leastconn 等算法配合使用。Haproxy 在第一次响应时写入一个带后端标识的 Cookie,后续请求携带这个 Cookie,Haproxy 就会直接把请求转发到对应的后端。
haproxy复制backend web_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server web1 10.0.0.1:8080 cookie web1
server web2 10.0.0.2:8080 cookie web2
这种做法的好处是,它绕开了哈希算法带来的热点问题,每个用户能被精确地固定在同一个后端,同时算法本身还是轮询,新用户的流量仍然会被均匀分发。代价是依赖客户端的 Cookie 支持。如果业务客户端会禁用 Cookie,这个方案就失效了。
到这里,我把 Haproxy 常见的算法族都拆了一遍。下面用一张表总结哈希型算法的键来源和场景:
| 算法 | 哈希键 | 适用层级 | 典型业务 |
|---|---|---|---|
| source | 源IP | 四层/七层 | 老系统会话保持 |
| uri | 请求URI | 七层 | 缓存、静态资源 |
| url_param | 指定查询参数 | 七层 | 按业务参数分片 |
| hdr | 指定请求头 | 七层 | 内网可信用户标识 |
| cookie | Set-Cookie标识 | 七层 | 用户登录态保持 |
5. 真实业务中的算法选型对照与案例复盘
5.1 算法全景对照表
很多架构师在选型时,习惯先看性能压测数据,再看功能是否匹配。我自己的经验是:先明确后端会话是否必须保持,再考虑请求处理时延和连接生命周期。下面这张对照表是我在内部技术分享时会直接投出来的图,方便初选。
| 算法 | 调度策略 | 支持动态调权重 | 会话保持 | 推荐场景 |
|---|---|---|---|---|
| roundrobin | 加权轮询 | 支持 | 弱(需要额外Cookie) | 无状态API、短请求 |
| static-rr | 静态加权轮询 | 不支持 | 弱 | 后端拓扑固定的场景 |
| leastconn | 最少连接数 | 支持 | 强(长连接天然保持) | 数据库、WebSocket、IM |
| first | 优先填满 | 不支持 | 无 | 主备、突发流量溢出 |
| source | 源IP哈希 | 支持 | 强 | 源IP固定的会话保持 |
| uri | URI哈希 | 支持 | 强(同URI) | 缓存、静态资源 |
| url_param | 参数哈希 | 支持 | 强(同参数值) | 无Cookie老业务 |
| hdr | 请求头哈希 | 支持 | 强(同Header值) | 内网可信链路 |
| random | 加权随机 | 支持 | 弱 | 大规模无状态集群 |
5.2 四个真实场景的选型复盘
场景一:无状态 API 集群,约 30 台机器
这类集群的特点是请求处理很快,没有本地 Session 依赖,所有状态都在 Redis 或数据库里。最初我用的 roundrobin,压测结果还算平稳,但有一次流量波峰时发现部分节点 CPU 高、部分低,原因是某些请求耗时差异大,导致长尾请求在部分节点堆积。后来换成 random,配合权重与机器规格比例,整体 CPU 分布明显均匀了。原因在于 random 没有轮询周期,不会出现请求按固定顺序围着一圈打的情况。
场景二:WebSocket 推送集群
WebSocket 长连接一旦建立,会保持很久,连接数直接代表在线用户规模。这里我选 leastconn,并且把每个后端的 maxconn 设为在线容量的 80%。调整后,即使某台机器上新用户连接增长较快,Haproxy 也能因为“连接数少”的判断,自动把新用户引导到负载更低的节点。这个场景下,roundrobin 的表现就差很多,因为新用户如果按轮询分到连接已满的节点,会被迫排队或者握手失败。
场景三:图片缓存节点集群
图片访问是典型的缓存友好型流量。最初用 roundrobin,同一张图片在不同时间可能被转发到不同节点,缓存命中率只有 50% 多。改为 balance uri + hash-type consistent 后,同一图片路径固定落到同一缓存节点,缓存命中率提升到 90% 以上,源站压力降了一个量级。这是哈希算法在缓存场景中价值的直观体现。
场景四:无法改造 Session 共享的老业务
这套老系统登录状态写在本机内存,没法快速改造。一开始用 source 算法,结果办公网所有用户都从同一个出口IP进来,全被怼到一台机器上,现场很惨烈。后来改成 roundrobin + cookie 粘性方案,新用户按轮询均匀分发,但首个响应里种下 Cookie,后续请求按 Cookie 固定到同一后端,问题解决,也没有出现热点。
这四个案例说明一件事:算法没有绝对的好坏,只看和后端业务模型是否匹配。选型时先回答三个问题——你的连接是短还是长?业务能不能接受会话跳变?后端节点会不会频繁扩缩容?答案会直接把算法缩小到两三个候选。
6. Haproxy算法调优实战:权重、热更、观测
6.1 maxconn和权重比例不匹配,流量照样倾斜
这是我在生产环境里踩过最深的一个坑。某次给一个后端组升级配置,三台机器权重比是 2:1:1,我顺手把 maxconn 全设成了 1000。结果跑了一阵子,第一台机器连接数长期在 1000 附近徘徊,第二、第三台只有三四百。监控面板上看着像第一台被打爆了,但实际原因是第一台权重高、连接数上涨快,很快触顶 maxconn,而第二、第三台权重低、分发得少,根本达不到 maxconn。
问题出在 maxconn 和 weight 的不匹配。如果权重是 2:1:1,maxconn 也应该是 2:1:1,这样各台机器按权重被占满的时间点大致相同,才不至于一台先到顶、其他还闲着。调完之后,三台机器的连接数占比就稳定在 2:1:1 附近了。
6.2 运行时调整权重的正确方式
线上服务最怕的是改配置要重启。Haproxy 提供了 stats socket,可以让我们在不重启进程的情况下调整服务器权重、上下线节点。前提是在全局配置里打开 socket:
haproxy复制global
stats socket /var/run/haproxy.sock mode 600 level admin
然后用 socat 和 socket 通信:
bash复制# 查看当前权重
echo "get weight web_servers/web1" | socat stdio /var/run/haproxy.sock
# 修改权重
echo "set weight web_servers/web1 12" | socat stdio /var/run/haproxy.sock
# 将某台后端置为维护模式,摘除流量
echo "set server web_servers/web1 state maint" | socat stdio /var/run/haproxy.sock
# 重新启用
echo "set server web_servers/web1 state ready" | socat stdio /var/run/haproxy.sock
这里要注意:如果你的算法是 static-rr,set weight 会报错或无效,因为静态轮询在设计上就不支持运行时调权重。另外,对哈希类算法调整权重时,如果 hash-type 是 map-based,调权可能引起大量哈希映射变化;如果用了 consistent,影响面会小很多
