它是什么
负载均衡器把请求分散到一池服务器上,让没有哪台过载,并且跳过已经挂掉的。服务发现回答的是先于它的那个问题:*此刻有哪些服务器存在、它们在哪里?*在一个实例每隔几分钟就来去一次的系统里,二者合起来,才让客户端不必知道任何地址,也能找到”订单服务”。
Important
核心要点: 选择层(L4 追求原始吞吐和非 HTTP;L7 按内容做路由),按请求的不均匀程度选择算法(请求差异大时,least-loaded 或 power-of-two-choices 优于 round robin),并且一定要搭配健康检查和连接排空。保留余量,让丢失一个实例不会压垮其余的。
何时需要它
每个设计里都有一个负载均衡器的方框,追问是”服务器挂了会怎样”、“怎么让用户停留在同一台服务器上”、“服务之间怎么互相找到”。经典的陷阱是在 L4 均衡器后面跑长连接的 HTTP/2(gRPC)。
Note
面试信号: 说出 gRPC 的陷阱(L4 均衡器按连接路由,但 gRPC 在一个长连接上复用大量请求,所以所有流量都落到同一个后端),说明你有实战经验。解决办法是 L7 代理或客户端侧的负载均衡。
Layer 4 与 Layer 7
| Layer 4(传输层) | Layer 7(应用层) | |
|---|---|---|
| 依据 | IP 地址和端口 | 请求本身:host、path、header、cookie |
| 速度 | 非常快;每个请求几乎没有额外工作 | 较慢:要解析请求 |
| TLS | 可以透传 | 通常终止 TLS |
| 路由 | 按连接 | 按请求:按路径、按 header、金丝雀分流 |
| 适合 | 吞吐、非 HTTP 协议、固定地址 | HTTP 和 gRPC API、微服务路由、WebSocket |
| 例子 | 网络负载均衡器、TCP 模式的 HAProxy | 应用负载均衡器、Envoy、NGINX |
gRPC 陷阱。 gRPC 运行在 HTTP/2 上,它在一个长连接 TCP 连接上复用许多调用。L4 均衡器在连接建立时选择一个后端,所以那个客户端的全部流量都去了同一台服务器,增加服务器也没有用。要使用第 7 层代理对每个请求做均衡,或者用客户端侧的负载均衡,让客户端持有到所有后端的连接。
均衡算法
| 算法 | 做法 | 适用 | 弱点 |
|---|---|---|---|
| Round robin | 依次轮流每个后端 | 请求均匀且很短 | 忽略负载和请求成本 |
| Weighted round robin | 按权重成比例分配 | 后端规格不同 | 权重是静态的 |
| Least connections | 打开请求数最少的后端 | 请求时长差异大 | 需要共享状态;多个均衡器时数据过期 |
| Least response time(延迟感知,EWMA) | 偏向最近最快的后端 | 后端异构或有噪声 | 可能涌向一个快但正在出错的节点 |
| Power of two choices | 随机挑两个,发给负载更轻的那个 | 有很多均衡器、没有共享状态 | 比全局视角稍欠最优 |
| 一致性哈希 | 把一个 key(用户、session、缓存 key)哈希到某个后端 | 缓存局部性或粘性 | 热点 key 会导致负载不均 |
算例:为什么两次选择就够了。 把 n 个请求随机分配给 n 台服务器时,最忙的服务器大约会拿到 log n ÷ log log n 个请求(在 n 很大时是平均值的好几倍)。随机挑两台服务器,发给负载更轻的那台,会把最大值降到大约 log log n,一次额外的探测换来指数级的改善,而且不需要任何全局共享状态,这就是它适合由独立均衡器组成的集群的原因。
粘性(同一个用户总是到同一个后端)只在状态存放在服务器上时才需要:使用 cookie 或对某个 key 做一致性哈希。更好的是让服务器无状态、状态放在共享存储里;粘性会和均匀负载作对,也会让排空变复杂。
健康检查、排空与余量
- 主动健康检查按间隔探测每个后端(一个 HTTP 路径、一次 TCP 连接),失败 N 次后把它摘除;成功 M 次后再加回来。被动检查观察真实流量,在出现错误或超时时剔除后端。两者都用。
- 健康检查端点应检查让实例能够提供服务的东西(它的关键依赖),但要小心:太浅的检查会在应用已经坏掉时仍然通过,太深的检查会在某个共享依赖抖动时让所有实例一起失败(级联故障)。
- 连接排空: 摘除一个后端时(部署、缩容),停止向它发新请求,但让在途的请求在时限内完成。
- 慢启动: 逐步加大新后端的流量,这样冷缓存或还没预热的运行时不会被全量负载直接冲击。
算例:余量。 六个实例,利用率 70%。如果有一个挂了,其余五个要承担它的负载:0.70 × 6 ÷ 5 = 84%,而在 84% 时排队延迟已经很陡。如果集群原来就运行在 85% 的利用率,丢失一个会把其余的推到 85% × 6 ÷ 5 = 102%,超出了容量,于是一次故障就变成了一次宕机。要按 N + 1(能容忍丢失一个)或跨可用区的 N + 2 来规划,而不是按平均负载。
全局负载均衡
跨区域时,流量由 DNS(基于地理或延迟的应答,由健康检查把故障区域摘掉)和 anycast(同一个 IP 在许多地方同时宣告,让网络路由到最近的那个)来引导。DNS 的应答会按生存时间被缓存,所以故障切换的速度和 TTL 一样慢;故障切换记录使用短 TTL,是用 DNS 负载换敏捷性。
服务发现
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| DNS | 名称解析到当前的实例(headless service,或每个实例一条记录) | 通用,不需要客户端库 | 缓存延迟更新;客户端可能钉住一个地址 |
| 服务端(前面放一个负载均衡器) | 客户端调用一个稳定地址;由均衡器掌握实例池 | 客户端简单 | 多一跳;均衡器成为关键基础设施 |
| 客户端侧 | 客户端查询一个注册中心(Consul、etcd、Eureka)并自己做均衡 | 没有额外跳数;客户端可以更聪明(延迟感知、可用区感知) | 每种语言都需要客户端库;依赖注册中心 |
| Service mesh(sidecar 代理) | 每个服务旁边的代理负责发现、均衡、重试、双向 TLS | 无需修改应用就有统一的行为 | 运维复杂、增加延迟 |
实例会注册自己(或由平台代为注册),带上健康状态和心跳;过期的条目会失效。注册中心本身是一个小而关键、由共识支撑的存储。在客户端缓存它的应答,这样注册中心抖动一下不会让流量停下来,并且要绕开在本地失败的实例,而不是只绕开注册中心已经移除的。
Warning
生产实践: 除非你把它们做成冗余的,否则均衡器和注册中心就是单点故障。把均衡器部署在多个可用区,让客户端能容忍过期的注册信息,并且测试注册中心不可用时会发生什么。一种常见的真实事故是:一个健康检查把所有实例同时标记为不健康,在这之后,“fail open”(对所有实例都提供服务)的策略好于对任何一个都不提供服务。
如何选择
| 情形 | 选择 | 原因 |
|---|---|---|
| HTTP API、按路径路由、金丝雀发布 | L7 均衡器 | 按请求做决定 |
| 最大吞吐、非 HTTP、固定 IP | L4 均衡器 | 快速、简单 |
| gRPC 或其他 HTTP/2 复用 | L7 或客户端侧均衡 | 避开单连接陷阱 |
| 请求成本差异很大 | Least connections 或 power of two choices | Round robin 忽略成本 |
| 缓存层或 session 亲和 | 一致性哈希 | 同一个 key 落在同一节点 |
| 服务互相发现 | 注册中心加客户端侧均衡,或 mesh | 实例不断变化 |
| 能承受丢失一个实例 | 按 N + 1 余量规划 | 决定性的是故障后的利用率 |
Caution
常见错误: 按平均负载规划容量。丢失一个实例(或一个可用区)之后集群还剩多少容量,才决定了一次故障是不是一次事故。另外:依赖某个共享下游的健康检查,可能会把所有实例一起踢掉,把局部故障变成全面故障。
相关
- 演练:设计聊天系统 — 网关背后的长连接,以及把消息路由到正确 socket 的注册中心。
- 演练:设计分布式缓存 — 当每个 key 必须落到同一个节点时,一致性哈希就是均衡算法。
- 参考:韧性模式 — 均衡器把调用路由到一个慢的或正在失败的后端之后,调用方该怎么做。