逛社区的时候经常看到有人问:HAProxy 的算法里哪些算动态算法?这个问题乍一看简单,但真要回答准,得先绕开一个坑——官网文档里其实没有“动态算法”这个分类,这是社区里大家为了便于讨论,把“决策依据包含服务器实时状态的那类调度器”统一叫成了动态算法。我自己刚接触 HAProxy 时也被这个概念绕晕过,所以这篇就按我实际使用和源码阅读的经验,把动态算法的边界、原理和带业务时的选用套路一次讲清楚。
凡是决定下一跳时参考的是当前连接数、当前存活状态、实时权重变化这类运行时数据,而不是只查一张固定映射表的,都属于动态算法。适合用它的场景也很明确:在线业务流量有波动,后端能力不均,或者你希望发布过程中不需要重启进程就能把流量搬来搬去。如果你正在做网关选型、七层负载均衡调优,或者只是想把 HAProxy 的调度行为彻底弄明白,这篇的内容可以直接拿来当参考。
1. 什么才算真正意义上的“动态算法”
1.1 先排除一个常见误解:能和 Runtime API 配合不等于动态算法
很多人看到 HAProxy 的 Runtime API 能执行set weight、disable server、shutdown sessions,就以为这说明算法是动态的。这个理解至少错了一半。
Runtime API 是 HAProxy 的“动态配置能力”,它解决的是运维侧能不能在不重启进程的情况下调整参数。比如我可以在凌晨流量低谷时,把某台服务器的权重从 100 调到 20,一秒后下一条新连接就会按新权重去分配,这确实是“动态调整”。但算法本身是静态还是动态,取决于它调度时使用什么输入数据。哪怕是balance source这种按客户端地址哈希的静态算法,我也照样可以在运行时改权重、摘除某台服务器,只是哈希环会重排而已。
所以判断一个算法是不是动态算法,要看它的调度决策是否依赖某一时刻的服务器实时状态。比如当前活动连接数、当前是否处于 drain 状态、当前权重在 slowstart 期间的渐进值。依赖,就算动态;不依赖,只是配置可热变更,那它依然属于静态算法。
1.2 一张表快速认清 HAProxy 算法全家族
为了不绕圈子,我把 HAProxy 常用算法的归属直接列出来,理解完这张表,后面拆解细节会轻松很多。
| 算法 | balance 参数 | 动态/静态 | 调度依据 |
|---|---|---|---|
| 加权轮询 | roundrobin | 动态 | 实时权重与调度轮次 |
| 最少连接 | leastconn | 动态 | 当前活动连接数与排队数 |
| 首个可用 | first | 动态 | 活动连接数是否低于 maxconn |
| 随机 | random | 动态 | 加权随机,运行时可调整概率 |
| 源地址哈希 | source | 静态 | 客户端 IP 的哈希映射 |
| URI 哈希 | uri | 静态 | URI 的哈希映射 |
| 请求参数哈希 | url_param | 静态 | 指定参数的哈希映射 |
| 报文头哈希 | hdr | 静态 | 指定请求头的哈希映射 |
| 通用哈希 | hash | 静态 | 自定义键值的哈希映射 |
注意,uri、url_param、hdr这些哈希算法后面还能挂hash-type consistent,一致性哈希会让节点增删时只影响少量 key,但这也只是改变了哈希映射的构建方式,不改变“我不看连接数、不看实时状态”这个本质。所以社区里讨论动态算法时,默认聚焦的是表格前三类加 random。
1.3 动态算法解决了哪些静态算法解决不了的问题
静态哈希类算法最大的痛点,是“感知不到后端状态”。一台服务器已经打满 CPU,负载均衡器还是会把大把请求哈希过去,因为哈希结果没有感知能力。动态算法恰好在这方面补位。
动态算法能给业务带来几个直接价值:一是让流量分配跟随后端处理能力自我修正,能力弱的节点连接数涨得快,调度器自然会少发新连接过去;二是支持精细的滚动发布,把某台节点权重降为 0 再观察,或者直接 drain,不会像静态哈希那样因为拓扑变化导致键值大范围迁移;三是配合 slowstart,让刚恢复的后端以一个渐进过程接受流量,而不是瞬间被冲垮。这些能力都是建立在调度器“实时读取节点状态”这个基本动作上的。
2. 三个主力动态算法逐个拆解
2.1 roundrobin:并不是无脑循环那么简单
roundrobin 是 HAProxy 的默认算法,也是很多人的入门算法。但它内部实现比你想象的讲究,不是简单维护一个下标,每来一个连接就加一。真实的实现里,HAProxy 为每个 backend 维护了一个调度轮次的状态机,服务器按权重被放进当前轮次的调度序列,每次分配时把权重差折算成“这一轮里还能分到多少次连接”。这么做的好处是分布非常均匀,长时间看,连接数比例会非常贴合权重比例,几乎能做到像素级的精确。
很多人踩过的坑集中在两点:第一,roundrobin 的调度单位是“新建连接”而不是“请求”。如果业务方用的是 HTTP 长连接和 keep-alive,连接建好后会持续处理大量请求,那么轮询只负责把连接分散开,不负责把请求分散开。极端情况下,某一条连接被客户端复用了上千次请求,而其他连接早就空闲了,这时候 roundrobin 的均衡效果会大打折扣。第二,roundrobin 的权重调整不是立竿见影的。运行时会话里执行set weight后,已建立的连接不受影响,新连接也会在下一轮调度中逐步体现新权重,但需要一个完整的轮次周期才能达到稳定比例。如果只是想临时把某台机器的流量亮出来,用set server xxx state drain或把权重置 0 会更快。
从场景上说,roundrobin 最适合短连接、高吞吐、请求处理速度快的场景。比如内网微服务之间的普通 HTTP/1.1 调用,连接生命周期非常短,新建连接的频率高,roundrobin 的均匀性优势能得到充分发挥。
2.2 leastconn:不是统计最少就直接发过去
leastconn 是我在生产环境里用得最多的动态算法。它的规则很简单:选择“活动连接数与等待连接数的加权和”最小的那台服务器。这里有个特别容易误解的细节,HAProxy 不会只盯着当前活动连接数,如果一个连接已经建立但因为后端处理慢,代理侧会把它放入等待队列,这个排队数量同样参与计算。
实际计算时,HAProxy 大致会算(当前会话数 + 等待队列长度) / 权重,选值最小的服务器。所以权重大的一方,即使连接数略高,计算后依然可能被选中,这有点类似“高配置服务器可以承受更多并发”的语义。如果你后端三台机器配置完全不同,用 leastconn 加不同权重,比单纯调 roundrobin 的权重更符合现实,因为 leastconn 的权重值会和实时连接数一起动态影响最终选路结果。
leastconn 有个先天缺陷:如果业务请求处理极快,比如都是 1 毫秒内的纯内存响应,那么任意时刻各台服务器的活动连接数基本都接近 0,leastconn 退化成一种几乎等价的顺序分配,体现不出对节点负载的感知能力。反过来,如果业务请求处理时间相对长,比如接口要写数据库、要调第三方服务,或者干脆是 WebSocket、消息推送这类长生命周期连接,leastconn 就非常合适。它能让每一台后端节点上的并发连接数维持在一个相对接近的水平,避免某一台因为拥塞效应持续恶化。
2.3 random 和 first:一个管高吞吐,一个管强约束
random 算法在 HAProxy 1.6 引入,最开始是纯随机,长尾分布比较明显。2.4 版本之后引入了真正的加权随机选择,官方文档里叫 random draw:调度器会先随机选中一台服务器,然后根据这台服务器的权重做一个概率接受判断,如果失败会再随机尝试。它的好处是调度过程几乎不进共享状态,多线程下吞吐表现非常稳,大流量场景下 CPU 开销比 roundrobin 更有优势,因为不需要维护全局轮次状态。
random 也有明显短板。小流量下随机波动会非常难看,可能连续好几个请求都打在同一个节点上。所以如果业务的 QPS 只有几十,不要用 random,容易把后端连接数打出不公平的尖刺。把 random 用在日活高、瞬时流量起伏大的网关层更合适,它处理突发流量时不会像轮询那样出现明显的排队偏移。值得一提的还有它天然支持运行时权重动态调整,这点在介入 K8s 弹性节点时很受用。
first 算法就很“一根筋”:严格按照配置顺序,找到第一台当前活动连接数低于 maxconn 的服务器,然后发过去;全都满了就等待队列排队。它最适用的场景是数据库中间层、AI 推理服务这类需要严格限制并发、不让后端被瞬间打满的系统。用 first 时,maxconn 参数的准确性是生命线,设太小浪费资源,设太大等于没限制。我一般不把它当通用调度算法,但在有并发硬约束的场景里它是不可替代的。
3. 影响动态算法实际效果的关键参数
3.1 权重在动态算法里如何真正落地
权重这个东西,在不同算法里的“解释方式”完全不同。roundrobin 把它解释为“一个调度轮次里可以被选中的次数”;leastconn 把它解释为“连接数计算时的除法分母”;random 把它解释为“概率接受时的判定阈值”。这就是为什么同样把某台服务器权重从 100 改成 20,在不同算法下看到的变化曲线完全不同。
举一个实际例子。假设后端三台机器,权重分别配 40、30、30,用 leastconn 调度。某时刻 web1 有 80 个活动连接,web2 有 50 个,web3 有 40 个。计算出的比较值大致是:
- web1:80 / 40 = 2
- web2:50 / 30 ≈ 1.67
- web3:40 / 30 ≈ 1.33
新连接会发给 web3,尽管它绝对连接数并不少,但除以权重后最低。这里权重起到的作用,是把“服务器硬件能力和配置差异”转化为调度容忍度。调权重时不要只凭感觉,最好用这个方式反推你的业务意图,这样发布时把某台权重临时降下来,你的预期才会和新连接走势一致。
3.2 slowstart:动态算法中保护后端的隐形气囊
刚接触 HAProxy 的人,很容易忽略 slowstart,直到一次线上事故教会做人。场景非常经典:某台后端因故障被健康检查标记为 down,恢复后直接进入 traffic 状态,如果没有 slowstart,下一瞬间大量新连接全部打到它身上,因为它此时的连接数最少。后端刚启动时依赖冷缓存,处理能力还没完全起来,直接被打挂,然后又触发健康检查失败,循环往复。
slowstart 的出现就是解决这个问题的。它让服务器从 down 恢复后,权重不是一步到位到配置值,而是在设定时间内线性递增。比如权重 100、slowstart 5 分钟,那么大约第 1 分钟权重是 20,第 3 分钟是 60,直到 5 分钟后才到 100。HAProxy 在实际计算时用的是“有效权重”这个概念,leastconn 做除法时用的也是有效权重,而不是配置文件里的最终权重。这个机制对 leastconn、random 这类和权重挂钩的算法特别有用,对 roundrobin 的作用相对有限,因为 roundrobin 本身是均匀分配,瞬时恢复也只会按比例分到一小份流量。
给新恢复节点配置 slowstart 时,建议把数值设置在后端进程真正完成预热所需时间的 1.5 倍以上。如果只是 JVM 起来,大概 2 到 3 分钟够用;如果需要预热本地缓存、限流器、连接池,那 5 到 10 分钟也不夸张。
3.3 maxconn 和 backlog:和 leastconn 配合时的排队普通话
leastconn 还有一个容易被忽略的组合项:服务器的 maxconn 与代理层的 backlog 队列。当所有节点的活动连接数都达到 maxconn 时, HAProxy 会把这个请求放到 backend 的队列里等待。队列长度默认等于maxconn值乘以一个倍数,也可以手动调queue-depth。
很多人在压测 leastconn 场景时,发现后端负载明明不高,但请求出现大量延迟,最后定位到问题出在排队设置。原因往往是 maxconn 配小了,实际吞吐需求超过总量,新请求只能排队等前面的长连接释放。反过来,maxconn 配得过大,leastconn 的负载感知就会变得迟钝,因为它要等到节点连接数很高才开始倾斜流量,在此之前所有节点看起来都“还行”。我的经验是,结合后端实际能承受的并发量来设定 maxconn,然后观察 leastconn 的分布曲线,如果某一台总是不被选中,基本可以断定它的 maxconn 或权重设置不合理。
4. 真实场景里的算法选型与配置模板
4.1 我平时怎么选算法:一张决策表
不同业务模型对调度算法的偏好差异很大,我整理了一个自己在项目里反复用的决策表。它不是教科书标准答案,但经过了多个线上环境的验证。
| 业务场景 | 推荐算法 | 关键参数 | 理由 |
|---|---|---|---|
| 内部微服务短连接 API | roundrobin | weight 按机器规格配 | 新建连接量大,均匀性最好 |
| 公网网关 HTTP 长连接 | leastconn | slowstart、maxconn | 长连接生命周期长,必须观连接数 |
| WebSocket / 消息推送 | leastconn | maxconn 精确设 | 并发数是核心资源 |
| 高并发瞬时突发流量 | random | weight 按能力配 | 抗突发、无轮次状态锁、开发抖动小 |
| 数据库中间层/推理服务 | first | maxconn 必须小 | 严格限制并发,防止打垮后端 |
| 需要 session 保持 | source / hdr | hash-type consistent | 静态哈希类,保证同一来源/会话稳定 |
特别提醒:如果业务同时要求“同一用户的请求尽量打到同一台后端”和“后端故障时快速切换”,静态哈希加 consistent 更适合;如果只是要求“同一用户的请求保证到同一节点且后端能承载”,那可以结合 sticky 表动态实现,但复杂度和一致性哈希不是一个量级。动态算法默认不保证任何亲和性,这是它和哈希类算法的最本质差异。
4.2 一套可直接抄的完整配置
下面这套配置,我线上验证过,覆盖了上面提到的动态算法主要参数。你可以直接复制后替换 IP 和端口,再根据实际机器配置调整权重。
global maxconn 50000 stats socket /var/run/haproxy.sock mode 600 level admin stats timeout 30s defaults mode http timeout connect 3s timeout client 30s timeout server 30s timeout queue 10s frontend web bind *:80 default_backend dynamic_demo backend dynamic_demo # 动态算法选型示例:最少连接 balance leastconn # 健康检查 option httpchk GET /health http-check expect status 200 server web1 192.168.1.11:8080 weight 40 maxconn 200 slowstart 5m check inter 5s fall 3 rise 2 server web2 192.168.1.12:8080 weight 30 maxconn 200 slowstart 5m check inter 5s fall 3 rise 2 server web3 192.168.1.13:8080 weight 30 maxconn 200 slowstart 5m check inter 5s fall 3 rise 2其中fall 3 rise 2表示健康检查连续失败 3 次后标记 down,连续成功 2 次后标记 up。rise相关的配置直接影响慢启动的触发时机:服务器从 down 恢复并变为 up,或者从 drain 重新进入可用状态,slowstart 才会生效。日常发布时,不要依靠 kill 进程来触发这个流程,正确做法是先在 Runtime API 里disable server,发布完再enable server,这样既能精确控制流量,也能让 slowstart 从头开始慢慢放量。
4.3 在 HAProxy Ingress 场景下的动态算法注意点
如果你用的是 Kubernetes 生态里的 HAProxy Ingress Controller,会发现它在不同版本里默认推荐的调度算法也会变,但核心逻辑和原生 HAProxy 一致。在 Ingress 场景下有个额外的变量:后端往往是一组 Pod,Pod 数量随 HPA 弹性伸缩。此时使用 roundrobin 会出现一个很微妙的副作用——Pod 扩容或缩容后,调度器需要重新平衡新连接,但旧连接还留在老 Pod 上,短时间内容易出现新 Pod 连接数偏低、老 Pod 连接数偏高的错位现象。
在 Ingress 中我更倾向于使用 leastconn,原因在于 Pod 之间的处理能力差异往往比物理机之间更大,特别是混部环境中,一台节点上的多个 Pod 共享 CPU,连接数才是更准确的负载指标。如果你开启了 keep-alive,还需要同时关注 Ingress Controller 的timeout http-keep-alive和timeout tunnel设置,否则连接长期不释放,动态算法算出来的连接数会失真。
5. 动态算法落地时最常踩的坑
5.1 配置了 leastconn,流量还是明显不均
这是我被问得最多的问题。出现这个现象,先别急着怀疑算法,先拿连接数说话。登录 HAProxy 的统计页,或者用 Runtime API 看每台服务器的cur和qcur两个指标。如果每台服务器的连接数确实接近,但后端负载不均,那问题大概率在 HTTP 长连接:一条连接承载了大量请求,连接数平衡不代表请求量平衡。此时你应该调整的是业务侧对 keep-alive 的复用策略,或者在 HAProxy 层做option http-server-close,把连接生命周期缩短。
如果连接数本身就差很多,再检查权重和 maxconn。权重配得悬殊,leastconn 计算时虽然会做除法修正,但极端权重差会让低权重节点几乎接收不到新连接,这是符合预期的,不是 bug。还有一种隐蔽情况:后端返回 keep-alive 响应,但前台客户端是 HTTP/1.0 或直接发 Connection: close,导致连接频繁新建,leastconn 的调度节奏被打乱。遇到这种要抓包才能定位。
5.2 权重调整后,新连接没有立刻按新比例走
很多人第一次用 Runtime API 调整动态算法权重时,会有一种错觉:改了马上生效,下一条连接就该立刻变。实际行为取决于算法。roundrobin 下,新权重在一个调度轮次内逐步生效,如果当前轮次还没结束,服务器依旧按旧状态参与调度。leastconn 下,下一条新连接就会用新权重做除法,响应最快。random 下,概率接受机制会立刻按新权重调整,但小样本内看不出明显差异。
如果你想在发布时快速掐断某台机器的流量,不要只调整权重,最直接的方法是:
echo "set server dynamic_demo/web1 state drain" | socat stdio /var/run/haproxy.sockdrain 状态下,已建立的连接继续维持,但不再接收任何新连接。这比把权重降为 0 更干净,因为权重 0 在 leastconn 里会变成无穷大,反而可能让调度器完全不碰它,但已有连接还是会正常服务。发布完成后执行set server dynamic_demo/web1 state ready恢复,并观察 slowstart 是否按预期生效。
5.3 slowstart 配了但没生效,服务器一恢复就被打挂
如果确认配置了 slowstart,但服务器 up 的瞬间流量还是瞬间打上去,优先检查健康检查的rise参数是否合理,以及服务器的初始权重是否被 Runtime API 改过。slowstart 是从当前有效权重开始增长的,如果有效权重本身已经是 100,它不会重新从 0 开始。另一个常见原因是,你调整的是default-server中的 slowstart,但单个 server 上没有显式继承,结果覆盖了默认值。配置检查时用haproxy -c -f 你的配置无法发现这种覆盖问题,需要实际观察统计页里的 weight 字段变化。
5.4 用统计页确认算法是否按预期工作
动态算法的调试,最关键的是确认“调度器眼中的状态”和“实际后端状态”是否一致。我会在本地固定一个查看命令:
echo "show servers state dynamic_demo" | socat stdio /var/run/haproxy.sock输出信息里重点看cur_sess、qcur、weight、state字段。比如weight字段在 slowstart 期间会持续变化,如果你看到权重未按时间递增,说明 slowstart 配置或触发生效条件有问题。还有一个非常有用的手段是开启 HAProxy 的日志分析,%b、%s、%si字段能记录后端名、服务器名和客户端 IP,按时间线聚合新连接落在各节点的数量,再和权重比例对照,就能判断算法是否按预期在跑。
6. 我个人在算法选型时的一些体会
动态算法虽然选项不多,但每个后面都挂了一串隐含参数,真正决定成败的往往不是算法本身,而是你到底有没有搞清楚业务的连接模型。短连接高频率就优先 roundrobin,长连接和慢接口就靠 leastconn,扛突发流量用 random,强并发约束选 first——这个顺序我用了很多年,很少失手。
比起花大量时间对比算法差异,我更建议先花时间观察线上连接的存活时长和请求速率。你有多少连接在 100 毫秒内结束,有多少连接要维持几分钟甚至更长,这个数据才是选型的第一依据。另一个建议是,无论选了哪种动态算法,都把 Runtime API 用起来,把发布流程中的“权重调整—drain—ready”固化到自动化脚本里。静态配置的优雅只存在于文档里,动态算法只有配合动态运维动作,才算真正发挥出设计价值。