分布式理论06:负载均衡
从 DNS、Nginx 到服务网格和客户端负载均衡,理解每一层负载均衡的职责、算法选择和失败转移边界。
负载均衡不是一个东西
很多人聊负载均衡,第一反应是 Nginx 或 LVS。实际上,一个请求从用户到应用实例,通常要经过多层负载均衡。
| 层级 | 常见组件 | 作用 |
|---|---|---|
| DNS 层 | 域名解析、多 A 记录、CDN | 把用户引到就近机房或入口 |
| 四层负载 | LVS、F5、云 SLB | 基于 IP 和端口转发,吞吐高 |
| 七层负载 | Nginx、HAProxy、云 ALB | 基于 HTTP 路径、Host、Header 路由 |
| 服务间负载 | Ribbon、Dubbo LB、Istio | 微服务内部实例选择和重试 |
每一层的目的、算法、故障转移速度都不一样。把所有问题都推给某一层,要么性能不好,要么故障恢复太慢。
每一层解决什么问题
DNS 层
DNS 负载均衡的特点是简单、成本低,但控制力弱。
它能做的主要是:
- 给一个域名配置多个 A 记录,客户端随机或按策略选一个
- 配合 CDN 把静态资源就近分发
- 按地理位置解析到不同机房
缺点也很明显:DNS 缓存不可控,故障转移慢,没法按应用层信息路由。所以 DNS 层通常只负责”用户先到哪个入口”,不负责精细化流量控制。
四层负载
四层负载均衡工作在传输层,看的是 IP + 端口。
它的优势是性能高、协议无关,TCP、UDP 都能转发。缺点是看不到 HTTP 层的信息,没法按 URL、Header、Cookie 做路由。
LVS、F5、云厂商的 SLB 都属于这一层。很多架构里,四层负载后面再挂一层七层负载,由七层做精细路由。
七层负载
七层负载均衡能看到完整的 HTTP 请求,所以能干的事更多:
- 按路径路由:
/api/order去订单服务,/api/user去用户服务 - 按 Host 路由:同一个 IP 承载多个域名
- 按 Header、Cookie 路由:灰度发布、AB 测试
- 连接池、TLS 卸载、压缩、缓存
- 健康检查和故障摘除
Nginx、HAProxy、Envoy、云 ALB 都在这一层。很多中小团队的流量入口,主要就是七层负载。
服务间负载
微服务内部,服务之间调用也需要负载均衡。
常见两种模式:
| 模式 | 代表 | 特点 |
|---|---|---|
| 客户端负载 | Ribbon、Dubbo 内置 | 客户端自己选实例,省一跳,但语言绑定 |
| 代理模式 | Envoy、Istio sidecar | 业务代码无感知,语言无关,但多了一层转发 |
服务间负载的重点不只是”选哪个实例”,还包括连接池、健康检查、重试、熔断、超时这些能力。
常见算法怎么选
| 算法 | 思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 轮询 Round Robin | 依次分配 | 简单、公平 | 不考虑节点性能差异 | 各节点能力相当的场景 |
| 加权轮询 | 按权重分配 | 可以适配不同配置的机器 | 权重需要合理设置 | 机器配置不一致的场景 |
| 随机 Random | 随机选一个 | 简单 | 小流量下可能不均 | 大量请求时效果接近轮询 |
| 最少连接 Least Connections | 选当前连接数最少的 | 能感知负载 | 实现复杂,需要维护连接数 | 长连接、处理时长差异大的场景 |
| IP Hash | 按客户端 IP 哈希 | 同一客户端落到同一节点 | 节点变化时命中率下降 | 需要会话保持,但不建议依赖 |
| 一致性哈希 | 哈希环 + 虚拟节点 | 节点增减时影响面小 | 实现稍复杂 | 缓存、有状态服务的路由 |
| 最短响应时间 | 选响应最快的节点 | 体验好 | 需要持续统计延迟 | 对延迟敏感的场景 |
没有最好的算法,只有最适合场景的算法。默认用轮询或加权轮询,大多数场景都够用。
负载均衡不是只负责转发
一个好用的负载均衡层,通常还要做这些事。
健康检查
定期探测后端实例是否可用,异常节点自动摘除,恢复后自动加回。健康检查的频率、超时、连续失败次数都要结合业务设置:太敏感会误摘除,太迟钝会扩大故障。
连接池和长连接
七层负载和后端之间如果用短连接,每次请求都要建连,性能会差很多。长连接 + 连接池是常规操作。
重试和幂等的配合
负载均衡层遇到 5xx 或超时,要不要重试?答案取决于后端接口是否幂等。
非幂等接口重试,可能导致重复写。所以通常策略是:
- 读接口:可以适当重试
- 写接口:默认不重试,或只在连接失败时重试
熔断和降级
某个后端集群持续报错,负载均衡层可以直接熔断,快速返回降级结果,避免把请求都打到已经有问题的后端。
慢启动
新实例刚上线时,缓存冷、JIT 未预热、连接池未建立。直接打满流量可能制造抖动。更稳的是先给小流量,逐步放量。
一致性哈希不是为了平均
一致性哈希经常被误解成一种更高级的平均算法。它真正解决的是节点变化时的迁移成本。
缓存场景里,如果普通 hash 按节点数取模:
node = hash(key) % nodeCount
节点从 4 台变成 5 台时,大量 key 会重新映射,缓存命中率瞬间下降。用一致性哈希加虚拟节点,可以让新增或删除节点时只有一部分 key 迁移。
但一致性哈希也不是万能的。热点 key 仍然会热点,虚拟节点太少会不均匀,节点权重不同也要额外处理。对于超热点数据,还需要本地缓存、请求合并、热点拆分,而不是指望哈希算法自动解决。
一个线上排查场景
某个订单接口偶发慢,平均耗时还好,但 P99 很高。看服务实例指标时发现,三台订单服务里只有一台 CPU 和线程池长期偏高。
继续查发现,这台实例刚好被更多用户会话命中,因为网关按 Cookie 做了会话亲和;而这批用户里有大量复杂订单查询,导致流量并不均匀。
这时问题不是”机器不够”,而是负载策略和业务流量特征不匹配。可能的修复包括:取消不必要的会话亲和、按接口隔离线程池、把复杂查询拆到单独服务,或者对重接口做限流。
负载均衡排查要看实例级指标,不要只看服务整体平均值。
灰度发布和负载均衡连在一起
灰度发布本质上也是一种流量分配。比如 5% 用户进新版本,指定租户进新版本,或者只让内部账号进新版本。
如果负载均衡只会平均分流,就很难做精细灰度。网关或服务调用层需要识别灰度标记,并保证同一个用户在一次流程里尽量命中同一版本,否则会出现兼容性问题。
所以负载均衡不只是性能问题,也是发布治理问题。
常见误区
误区一:以为轮询一定平均
实例健康、请求耗时、连接复用、长连接、会话亲和都会让真实负载偏斜。
误区二:只做入口负载均衡,忽略服务内部调用
网关分得很均匀,但订单服务调用库存服务时如果实例选择不合理,下游仍然会热点。
误区三:扩容后立刻放满流量
新实例缓存冷、JIT 未预热、连接池未建立,直接打满可能制造抖动。更稳的是预热和逐步放量。
误区四:健康检查只看端口
端口活着不代表服务健康,线程池满、数据库连接耗尽、核心依赖不可用时,都应该影响摘除策略。
误区五:会话保持是必需品
很多老系统依赖 Session Sticky,把同一个用户绑到同一台机器。这会带来几个问题:节点故障时用户会话丢失、负载不均、灰度发布复杂。更推荐把会话放到集中存储(Redis、数据库),让应用节点无状态。
项目里怎么判断
设计或排查负载均衡时,可以看:
- 实例规格是否一致,需不需要权重?
- 请求耗时差异大不大,是否需要最少连接或自适应策略?
- 是否存在会话亲和、缓存 key、租户流量导致热点?
- 扩容实例是否有预热过程?
- 健康检查能不能反映真实处理能力?
- 灰度流量是否能稳定命中目标版本?
负载均衡的目标不是数学平均,而是让系统容量被稳定、可控地使用。