负载均衡,本质上就是在多个可提供相同服务的后端节点之间,合理分配流量

它的目标通常有三个:

  1. 提升吞吐量:让多台机器一起承担请求
  2. 提高可用性:某个节点坏了,流量可以绕开它
  3. 改善响应时间:避免部分节点过载、部分节点空闲

负载均衡分为硬件负载均衡和软件负载均衡

硬件负载均衡通常是专门的网络设备,比如早年常见的 F5 一类产品。
它们通常具有:

  • 专用硬件
  • 高性能转发能力
  • 完整的网络能力和商业支持
  • 较成熟的高可用方案

它的优点很明显:性能强、稳定、功能全。
缺点也很明显:贵、重、灵活性相对差、运维和扩展成本高

软件负载均衡就是用软件程序在通用服务器上完成流量分配。常见的有:

  • LVS
  • Nginx
  • HAProxy
  • Envoy

它的优势在于:

  • 部署灵活
  • 成本低
  • 易于自动化
  • 容易与容器、微服务、Kubernetes 等体系结合
  • 便于定制路由策略、熔断、限流、观测等功能

现在的多数互联网系统、云原生平台、内部服务网格,更关心的其实都是软件负载均衡
因为它不只是“分发流量”,还经常承担协议代理、服务治理、故障隔离、灰度发布这些职责。

所以本章重点讲软件。

软件负载均衡工作流程

从工程实现上看,软件负载均衡做的事情并不只是“把请求分出去”这么简单。
它通常至少要完成下面几件事:

  • 接收客户端请求或连接
  • 在多个后端节点中选择一个合适的目标
  • 将流量转发过去
  • 持续检查后端节点是否健康
  • 在节点异常时停止分发流量
  • 在节点恢复后重新接入
  • 统计连接数、延迟、错误率等运行指标

也就是说,软件负载均衡本质上是一个流量分发与运行时调度系统
算法只是其中一部分,健康检查、故障摘除、状态维护、流量控制同样重要。

软件负载均衡通常可以工作在两个层次:四层和七层。

四层负载均衡(L4)

四层负载均衡基于传输层信息进行转发,主要看:

  • 源 IP、目标 IP
  • 源端口、目标端口
  • TCP、UDP

它并不关心 HTTP 的 URL、Header、Cookie 这些应用层内容。
也就是说,它只负责把连接转发到某个后端,而不理解业务请求本身。

它的特点通常是:

  • 性能高
  • 转发开销小
  • 更适合大流量 TCP/UDP 场景

四层负载均衡的典型代表是 LVS/IPVS。,以及很多内核态、高性能网络设备。

七层负载均衡(L7)

七层负载均衡基于应用层协议做转发,最常见的是 HTTP/HTTPS,也包括 gRPC 等。
它能看到并利用更多信息,比如:

  • Host
  • URL Path
  • Header
  • Cookie
  • 请求方法
  • TLS 信息

因此它不仅能做“分发”,还可以做更细粒度的路由和治理,例如:

  • 按域名转发
  • 按路径转发
  • 按请求头转发
  • 灰度发布
  • 金丝雀发布
  • 鉴权、限流、熔断

特点是:

  • 更灵活
  • 可以做更复杂的流量治理
  • 但开销通常高于四层

Nginx、HAProxy、Envoy 则通常以七层代理能力见长,不过它们也都支持一定的四层转发能力,只是在工程实践中更常被作为七层代理来使用。

可以简单理解为:

  • L4 更像高性能交通分流
  • L7 更像懂业务规则的调度员

负载均衡算法

负载均衡不只是“平均分请求”
1 号请求给 A,2 号请求给 B,3 号请求给 C;这是最简单的场景。

现实系统里,后端节点往往不是完全等价的:

  • 有的机器配置更强
  • 有的机器当前连接更多
  • 有的请求处理时间更长
  • 有的节点刚恢复,不能立刻灌满流量
  • 有些业务要求同一用户尽量命中同一节点
  • 有些服务是长连接,连接数和请求数并不等价
  • 有些集群会动态扩缩容

所以“怎么选后端”这件事,最后就演变成了各种负载均衡算法

轮询(Round Robin)

轮询是最经典、最直观的算法。
它的做法很简单:请求依次分给每个后端节点。

假设有三个节点 A、B、C,请求分配顺序可能是:
A → B → C → A → B → C

优点:

  • 实现简单
  • 开销低
  • 节点性能接近时效果不错

缺点:

  • 不考虑节点当前负载
  • 不考虑请求差异
  • 长连接场景下效果未必理想

因此,轮询适合这样一些场景:

  • 节点配置差不多
  • 请求处理耗时比较接近
  • 系统规模不大,追求简单稳定

加权轮询(Weighted Round Robin)

现实里节点性能往往不同。比如:

  • A:16 核
  • B:8 核
  • C:8 核

如果还是简单轮询,A 的能力就浪费了。加权轮询就是给不同节点不同权重,例如:

  • A:权重 4
  • B:权重 2
  • C:权重 2

流量分布大致就会变成 4:2:2。

优点:

  • 允许异构机器按能力分担流量
  • 实现仍然比较直接

缺点:

  • 仍然不感知实时负载
  • 权重设置不准时,效果会打折

所以它更适合:

  • 节点能力不一致
  • 请求分布相对平稳
  • 不需要强实时负载感知

随机(Random)

随机选择一个后端节点。
表面上看很粗糙,但在节点多、请求量足够大时,随机分布并不会太差。
某些系统里甚至会偏爱简单随机,因为它足够轻量。

优点:

  • 实现极简
  • 并发下容易扩展
  • 在大样本下分布通常还可以

缺点:

  • 小样本下波动大
  • 不考虑节点差异和实时状态

适用场景:

  • 大规模集群
  • 对绝对均匀性要求没那么高
  • 更看重低开销

两次随机选择(Power of Two Choices)

这是一个非常值得认真理解的算法。它比“纯随机”多走一步,但效果往往明显更好。

做法是:

  1. 随机选两个节点
  2. 比较它们当前负载
  3. 选择更轻的那个(当前活跃连接数、当前未完成请求数、平均响应时间…)

这个方法非常有名,因为它用极低的代价,往往就能接近复杂调度算法的效果。
纯随机可能碰巧一直打到忙节点。而“两次随机”会给系统一点“纠偏能力”,把请求偏向更空闲的节点。

优点:

  • 成本低
  • 效果通常显著优于纯随机
  • 很适合大规模分布式系统

缺点:

  • 需要一个“负载指标”,比如连接数、并发请求数、队列长度
  • 指标不准确时会影响效果

适用场景:

  • 节点多
  • 需要低成本又不错的均衡效果
  • 常用于现代分布式系统中的客户端负载均衡或代理层调度

最少连接(Least Connections)

这类算法认为:当前连接数更少的节点,通常更空闲。所以每次把请求分给活跃连接数最少的节点。

优点

  • 比轮询更能反映实时负载
  • 对长连接场景更友好
  • 在连接持续时间差异较大时优于轮询

缺点:

  • “连接数少”不一定意味着“真的轻”
  • 一个连接可能很闲,也可能很重
  • 对 HTTP/2、多路复用场景,需要重新理解“连接”的意义

适用场景:

  • TCP 长连接服务
  • 请求处理时间差异较大
  • 连接数与资源占用存在较强相关性

实现思路:负载均衡器维护每个后端的活动连接数:

  • 新连接建立:计数 +1
  • 连接关闭:计数 -1

每次调度选择计数最小的节点。
如果是请求级转发而不是连接级转发,也可能维护“活跃请求数”而不是“活跃连接数”。

加权最少连接(Weighted Least Connections)

这是对最少连接的扩展。因为节点能力不同,不能只看绝对连接数。

例如:

  • A 权重 2,当前连接 20
  • B 权重 1,当前连接 12

直觉上,A 并不一定比 B 更忙。
所以通常比较的是类似下面这样的指标:

1
active_connections / weight

谁更小,就优先选谁。

优点:

  • 同时考虑了实时负载和节点能力
  • 比简单最少连接更实用

缺点:

  • 仍然依赖“连接数能够代表负载”这个假设
  • 权重和实际处理能力未必线性对应

实现思路:
维护节点的 active_connweight,调度时选择 active_conn / weight 最小的节点。

实际实现可能会做整数化或近似计算,避免浮点开销。

最短响应时间 / 最低延迟(Least Response Time)

这类算法关注的是延迟表现
如果某个节点响应更快,就让更多流量去它那边。

比起“连接数”,延迟往往更贴近用户体验。
但它也更难处理,因为延迟会波动,而且容易被瞬时噪声影响。

优点:

  • 更关注真实服务质量
  • 适合对响应时间敏感的系统

缺点:

  • 指标抖动大
  • 容易形成“越快越给流量,越给流量越变慢”的震荡
  • 需要平滑和限流机制

适用场景:

  • 对时延敏感
  • 监控指标采集完善
  • 能接受更复杂的调度逻辑

实现思路:
常见做法不是直接用“最近一次响应时间”,而是用平滑后的指标,例如:

  • 滑动平均
  • 指数加权移动平均(EWMA)

记某节点最近观察到的延迟为 rtt,平滑值为:

1
score = alpha * new_rtt + (1 - alpha) * old_score

调度时选 score 最小的节点。

有些系统还会结合并发数,构造更稳健的负载分数,比如:

1
score = latency * active_requests

这更接近“预估排队时间”。

基于哈希的负载均衡(Hash-Based)

这一类算法不追求“每个请求都均匀分发”,而是追求同一类请求尽量命中同一节点
可哈希的键可以是:

  • 客户端 IP
  • 用户 ID
  • Session ID
  • URL
  • 某个业务字段

优点

  • 天然支持粘性会话
  • 有利于缓存命中
  • 某些业务分片很自然

缺点

  • 容易分布不均
  • 节点增减时映射变化可能很大
  • 热点用户或热点 key 会导致热点节点

IP Hash

对客户端 IP 做哈希,映射到某个后端。
这样同一个客户端通常总会落到同一台机器上。

URL Hash

对 URL 做哈希,常用于缓存系统,让相同内容尽量命中同一缓存节点。

适合需要会话粘性、租户隔离或业务路由的场景。

实现思路:

1
index = hash(key) % N

但这个方法在节点数量变化时问题很大。
比如从 4 个节点变成 5 个节点,大量 key 的映射都会改变,缓存命中率会骤降。

所以工程上通常会继续使用一致性哈希

一致性哈希(Consistent Hashing)

一致性哈希是分布式系统里非常经典的思想。
它的核心目标不是“最均匀”,而是:当节点增减时,只让少量 key 重新分配

可以把哈希空间想成一个环。节点和请求 key 都映射到这个环上:

  • 节点映射成若干个点
  • key 映射到环上的某个位置
  • key 顺时针找到的第一个节点,就是它的目标节点

当一个节点下线或新增时,只会影响环上相邻的一部分 key,
而不会像 hash(key) % N 那样让全局大量 key 洗牌。

真实系统里,直接把每个节点只放一个点到环上,通常不够均匀。
所以会给每个物理节点分配多个虚拟节点。能力更强的物理节点,可以分配更多虚拟节点,从而承担更多流量。

优点:

  • 节点增减时扰动小
  • 适合缓存、分布式存储、会话路由
  • 能自然支持权重

缺点:

  • 实现比普通哈希复杂
  • 仍要考虑热点 key
  • 对请求实时负载不敏感

适用场景

  • 缓存集群
  • 分布式 KV
  • 需要粘性路由且节点动态变化的系统

最少请求 / 最短队列(Least Requests / Shortest Queue)

有些系统更关心当前“还没处理完的请求数”,而不是 TCP 连接数。
尤其在 HTTP/1.1 keep-alive、HTTP/2、多路复用场景下,连接数和负载并不总一致。

优点:

  • 比连接数更接近真实服务压力
  • 适合代理层按请求粒度调度

缺点:

  • 需要更细粒度的统计
  • 指标采集开销略高
  • 对请求重量差异仍然无法完全覆盖

实现思路:
维护每个后端节点:

  • 正在处理的请求数
  • 请求完成时递减
  • 新请求到来时选择最小值节点

很多现代代理会把这个指标作为基础负载信号之一。

算法的场景总结

讨论负载均衡算法时,一个常见误区是:总想找一个“最先进”的算法。
其实大多数时候并不存在统一最优解。

可以粗略地这样理解:

  • 轮询 / 加权轮询:简单稳定,适合节点和请求都比较均匀的场景
  • 最少连接 / 最少请求:适合负载会波动、请求持续时间差异较大的场景
  • 最短响应时间 / EWMA:适合更关注时延质量的场景
  • 哈希 / 一致性哈希:适合需要粘性、缓存命中、分片稳定性的场景
  • 两次随机选择:适合规模大、想要低成本高收益的场景

真正的工程实践常常不是只用一种算法,而是算法 + 规则 + 健康检查 + 限流 + 观测共同构成一个可工作的系统。

软件负载均衡在工程上的实现

因为真正的负载均衡器不是一个简单的“选节点函数”,而是一套完整的运行时机制。往往由下面这些模块组成。

后端节点管理

首先要维护一个后端节点池。每个节点至少会有这些信息:

  • IP、端口
  • 权重
  • 健康状态
  • 当前连接数 / 请求数
  • 延迟指标
  • 失败次数
  • 最近一次健康检查时间
  • 是否处于 drain(摘流)状态

这本质上是一份持续变化的运行时状态表。

健康检查

健康检查是负载均衡实现里最关键的部分之一。
因为再好的算法,如果还在把流量发给故障节点,整个系统一样会出问题。

健康检查一般分为两种:

主动健康检查
由负载均衡器主动探测后端,例如:

  • TCP 连接是否能建立
  • HTTP /health 是否返回 200
  • gRPC 健康检查接口是否正常
  • 检查响应时间、返回内容

被动健康检查
根据真实业务请求结果判断,例如:

  • 连续连接失败
  • 连续 5xx
  • 超时比例异常升高

实际系统中,通常会把两者结合使用。
主动检查能较早发现问题,被动检查能更贴近真实业务状态。

实现要点一般会考虑:连续失败多少次才标记为 unhealthy?连续成功多少次才恢复?
不能太频繁,也不能太慢,避免健康检查本身变成负担。

这背后其实体现的是一个原则:状态切换不要太敏感,否则系统会抖动。

连接处理模型

软件负载均衡器通常是高并发网络程序,所以需要一个高效的 IO 模型。

常见实现方式包括:

  • select
  • poll
  • epoll
  • kqueue
  • io_uring(更现代)
  • 用户态网络栈 / DPDK(更极致性能场景)

在 Linux 世界里,很多成熟实现主要基于:

  • epoll + 非阻塞 socket
  • 多进程 / 多线程 worker
  • 事件驱动模型

这类模型的核心思想是:
不要为每个连接都分配一个阻塞线程,而是通过事件循环统一管理大量连接。

如果负载均衡器本身效率不高,它就会成为新的瓶颈。
所以软件负载均衡不仅要“分得对”,还要“转得快”。

转发模式

软件负载均衡不只有一种转发方式。

反向代理模式

这是七层负载均衡最常见的方式。
客户端连接的是 LB,LB 再去连接后端。

流程是:

1
Client -> Load Balancer -> Backend

优点:

  • 控制力强
  • 能做 HTTP 路由、Header 改写、TLS 终止、压缩、鉴权等
  • 适合应用层治理

缺点:

  • LB 需要真正参与数据收发
  • 性能开销更大

Nginx、HAProxy、Envoy 多属于这类思路。

NAT / DR / TUN 等内核转发模式

这是四层高性能转发的常见路径。
以 LVS 为代表,它有几种经典模式:

  • NAT
  • DR(Direct Routing)
  • TUN(IP Tunnel)

这里不展开协议细节,只讲直观理解:

  • 有些模式下,回包也经过 LB、
  • 有些模式下,请求经过 LB,但响应直接由后端回客户端,从而减轻 LB 压力

这也是为什么四层方案常能做到很高性能。

会话保持(Session Persistence)

有些业务并不是“任何请求落到任何节点都一样”。例如:

  • 会话状态存在本地内存
  • WebSocket 需要保持长连接
  • 本地缓存命中很重要
  • 某些用户相关数据在单节点上更热

这时候需要“粘性会话”,即尽量让同一用户落到同一节点。

实现方式通常有:

  • IP Hash
  • Cookie 插入或识别
  • Session ID Hash
  • 一致性哈希

但需要注意,会话保持和负载均衡本身是有张力的。

粘性越强,请求分布就越不自由;
分布越不自由,越容易出现热点。

所以现代系统更推荐把状态外置,例如放到 Redis、数据库、分布式缓存中,尽量减少对强粘性路由的依赖。

动态权重与慢启动(Slow Start)

一个节点刚刚恢复时,如果立刻把满额流量打过去,常常会再次被压垮。
因此很多系统会使用慢启动机制。

思路:

节点恢复后,不立即给它完整权重,而是:

  • 先给少量流量
  • 再逐渐提升到目标权重

这样可以让:

  • JVM 预热
  • 缓存回温
  • 连接池逐步建立
  • 局部状态恢复

实现方式:

恢复节点的有效权重 effective_weight 从较小值开始,按时间或成功请求数逐步增加,直到达到配置权重。

连接摘除与优雅下线(Draining)

节点下线时,不能粗暴地直接杀掉。
否则会中断现有连接,影响用户请求。

更合理的方式是:

  • 先停止给该节点分配新请求
  • 允许已有连接自然结束
  • 等活动连接清零后再真正移除

这就是 drain。
很多负载均衡器和容器编排系统都会支持这类能力。

重试、超时、熔断

软件负载均衡在应用层经常还会负责一定程度的容错。

超时:
请求不能无限等。连接超时、读超时、写超时都要有边界。

重试:
某个后端失败后,是否允许重试到另一个节点?
这很常见,但要谨慎。 因为重试会放大流量,如果系统已经过载,盲目重试可能雪上加霜。

熔断:
如果某节点或某服务整体异常,应该暂时减少甚至停止向它发送请求。这可以避免故障扩散。
所以现代七层负载均衡器经常不只是 LB,还兼有部分服务治理代理的角色。

几种典型软件负载均衡器的风格差异

这里不做产品清单式介绍,只简单说一下各自气质。

LVS

  • 更偏四层
  • 高性能
  • 靠近内核网络层
  • 适合做大流量入口转发

Nginx

  • 七层能力成熟
  • HTTP 生态强
  • 配置和部署普及度很高
  • 反向代理、静态资源、网关、负载均衡常一起做

HAProxy

  • 在代理和负载均衡领域非常成熟
  • 性能、稳定性、调度策略都很强
  • 在很多高要求场景里很常见

Envoy

  • 面向现代微服务
  • 对 xDS、动态配置、可观测性、服务网格更友好
  • 在云原生体系里地位很高

简单来说:

  • 如果更关注高性能四层转发,常会想到 LVS
  • 如果更关注 Web 入口与 HTTP 路由,常会想到 Nginx
  • 如果更关注专业代理能力,常会想到 HAProxy
  • 如果更关注微服务治理和云原生体系,常会想到 Envoy

分层分布式流量调度

前面讨论的很多内容,默认都是一种比较经典的结构:
一个负载均衡器,对应一组后端节点。

也就是说,客户端请求先到 LB,再由 LB 决定把流量分给哪台后端机器。
这种方式在单体应用、传统集群或者规模不太大的系统里很好理解,也很常见。

但当系统继续变大之后,负载均衡往往就不再只是“前面放一个 LB”这么简单。
原因确实有一部分是为了避免单个 LB 本身成为性能瓶颈,但这并不是全部。
更重要的是,随着系统层次变多,流量分发这件事本身也会分散到不同位置,由多个组件共同完成。

换句话说,大型系统里的负载均衡,往往不是一个单点动作,而是一组分层完成的动作。

负载均衡的分层

为什么单机式的负载均衡模型会逐渐不够?

最直接的原因,是单个 LB 可能会成为瓶颈。
如果所有请求都必须先经过同一个负载均衡器,那么即使后端有很多机器,前面的 LB 仍然可能先达到极限。这时系统的上限,不再取决于后端整体能力,而取决于入口这一层能承受多少流量。

但更深一层的原因在于,大型系统中的流量并不只发生一次分发。

在一个规模较小的系统里,通常只有“用户请求进入应用服务器”这一层流量调度。
而在复杂系统中,请求往往会经历多个阶段,例如:

  • 用户先被导向某个地域或机房
  • 进入机房后,再由入口网关分发到接入集群
  • 接入层继续调用内部服务
  • 内部服务再调用下游服务

也就是说,“应该把请求发给谁”这个问题,会在系统中反复出现。
既然如此,负载均衡自然也就不再只是入口处那一个独立设备的职责。

在更大的系统里,流量分发通常会拆成几层。

DNS 负载均衡

最外层往往是 DNS。
它并不直接决定某个请求落到哪台应用机器,而是先决定用户应该被导向哪个地域、哪个机房,或者哪一个大入口。

用户先访问域名,DNS 服务器(分布式集群)返回不同机房的入口地址。例如:

  • 华东用户导向上海机房
  • 华北用户导向北京机房
  • 海外用户导向新加坡入口

这一层解决的是更粗粒度的流量分配问题。
它分的是“先去哪里”,而不是“最终进哪台应用实例”。

入口网关负载均衡

用户到达某个地域或机房之后,通常还要再经过入口网关层。
这时候才进入大家最熟悉的负载均衡场景:在一组接入节点或网关节点之间分配流量。

这一层更关心的是:

  • 哪个接入节点健康可用
  • 哪个节点当前更空闲
  • 新连接应该落到哪里

这一层通常会用到前面介绍过的轮询、最少连接、加权最少连接等算法。

服务间负载均衡

在微服务系统中,请求进入网关之后并不会立刻结束。
一个服务往往还要继续调用其他服务。

例如:

  • 订单服务调用库存服务
  • 库存服务调用支付服务
  • 支付服务调用风控服务

这时,负载均衡就不再只发生在最外层入口,而是开始进入系统内部。每一次服务调用,都需要在多个实例中选择一个目标节点。

从这个角度看,服务间调用本身也存在一个“应该把请求发给谁”的问题。
只是这里要分发的,不再是用户到接入层的流量,而是系统内部一层又一层的服务请求。

在这一层里,负载均衡有不同实现方式。

一种方式是,调用请求先经过一个中心代理,再由代理把请求转发给某个服务实例。
另一种方式则更常见于微服务体系:调用方先通过服务发现拿到实例列表,再由自己在本地决定把请求发给哪个实例。这个做法通常被称为客户端负载均衡

这里的“客户端”并不是浏览器或手机,而是发起调用的服务本身。比如订单服务,就是库存服务的客户端。

这样做的好处在于:

  • 少经过一次中间转发
  • 减少中心代理的压力
  • 更适合实例频繁变化的场景

也正因为如此,到了系统内部这一层,负载均衡往往已经不再表现为一个独立的前置设备。它可能存在于调用方的 SDK 中,存在于客户端库中,也可能由 Sidecar 代理或者 Service Mesh 的数据平面来完成。

换句话说,系统越往后走,负载均衡越不像一个放在前面的“盒子”,而更像一种分散在各处的流量分发能力。

分布式负载均衡解决的问题

因此,所谓“从单机负载均衡到分层分布式负载均衡”,并不是指前一种错误、后一种先进,而是指随着系统规模和层次增长,负载均衡这件事本身发生了变化。

它当然解决了“单个 LB 容易成为瓶颈”的问题,但更重要的是,它适应了这样一种事实:
流量分发不再只发生在入口,而是发生在系统的多个层次。

比如:

  • 用户访问时,要先决定进入哪个地域、哪个机房
  • 进入机房后,还要决定落到哪个入口节点
  • 服务调用服务时,还要继续决定请求发给哪个实例

而且,这些分发动作未必都由某一个独立设备完成。
有些发生在 DNS 层,有些发生在网关层,有些则发生在服务调用方、本地代理或者 Mesh 之中。

所以更准确地说,分布式负载均衡不是简单地“把一个 LB 变成很多个 LB”,而是把流量分发的职责拆散,放到系统不同层次分别完成。

软件负载均衡的误区

最后简单说几个很常见的问题。

以为算法决定一切

实际上,算法只是负载均衡的一部分。
健康检查、超时、重试、状态维护、节点上下线策略同样重要。

以为平均就是最优

平均分配并不总是最合理的。在很多场景里,真正重要的是:

  • 避免热点
  • 控制延迟
  • 保持稳定
  • 降低抖动

过度依赖会话保持

会话保持有时是必要的,但也会降低流量调度自由度,容易引入热点。
现代系统通常更倾向于把状态外置,而不是强依赖粘性路由。

忽略重试的副作用

重试虽然能提高成功率,但也可能在故障场景下放大流量,导致系统雪崩。
所以重试必须谨慎配置。