“在前面放一个负载均衡器”是系统设计里最常听到的一句话,而它背后藏着几个在生产环境中至关重要的决定:它工作在哪一层、如何选择后端、如何判断后端是否健康,以及摘除一个后端时,正在处理的请求会怎样。
本文逐一讲解这些决定,并画出大多数大型部署最终都会采用的两层架构:在一组七层代理前面放一个四层负载均衡器。
四层与七层
| 四层 | 七层 | |
|---|---|---|
| 能看到 | IP 地址、端口、TCP 或 UDP 连接 | HTTP 请求:主机、路径、请求头、Cookie |
| 均衡的对象 | 连接 | 请求 |
| TLS | 透传,或者终止 | 通常会终止 |
| 可以按什么路由 | 端口、源地址 | 路径、主机、请求头、方法 |
| 还能做什么 | 极高吞吐、低延迟 | 重试、超时、改写、认证、限流 |
| 示例 | 云厂商的网络负载均衡器、IPVS、Maglev 式均衡器 | Envoy、NGINX、HTTP 模式下的 HAProxy、云厂商的应用负载均衡器 |
最容易踩坑的区别是:四层均衡器均衡的是连接,而不是请求。使用 HTTP/2 或 gRPC 时,一个客户端只打开一条长连接,并在上面发送成千上万个请求,而这些请求全都会落到同一个后端。要把 gRPC 均衡好,需要七层代理或客户端负载均衡。
为什么要两层
大型架构通常把两者叠起来。四层负责持有公网 IP 地址、吸收连接洪峰,并按每条流的哈希把连接分发给一组七层代理。代理终止 TLS、应用路由规则,并把每个请求均衡到各个后端池。
每一层都可以独立扩容:四层按数据包和连接扩容,代理集群按请求量和 TLS 握手扩容。分层还能限制影响范围:一次错误的代理配置变更,影响的是某一层后面的代理,而不是用户连接的地址。而且由于代理是无状态的、共享同一份配置,增加一台代理就只是增加容量。
均衡算法
| 算法 | 如何选择 | 适用场景 | 注意事项 |
|---|---|---|---|
| 轮询 | 依次选择下一个后端 | 后端相同、请求相似 | 慢请求会在慢后端上堆积 |
| 加权轮询 | 按权重比例轮询 | 实例规格混用、逐步切换 | 权重是静态的 |
| 最少连接 | 打开连接数最少的后端 | 长连接 | 需要均衡器维护全局状态 |
| 最少请求(二选一) | 随机挑两个,发给较空闲的那个 | 大多数 HTTP 服务 | 需要每个后端的请求数 |
| 一致性哈希 | 按某个键(如用户 ID)的哈希 | 缓存、会话亲和 | 热点键会导致负载不均 |
随机二选一的最少请求算法,是 HTTP 服务的一个好默认值。Envoy 的负载均衡器文档 是这样描述的:随机挑选两台主机(默认是两台),把请求发给活跃请求较少的那一台。Mitzenmacher 等人的研究表明,这几乎和检查所有后端一样好,却没有那样的开销。一致性哈希(如 ring hash 或 Maglev 算法)让同一个键始终落到同一个后端,后端变化时只移动一小部分键,这正是缓存所需要的。
健康检查
均衡器必须停止向无法提供服务的后端发送流量。两种机制配合使用:
- 主动检查: 均衡器调用健康检查端点,比如每 5 秒请求一次
GET /healthz,连续失败达到一定次数后摘除该后端。 - 被动检查(异常检测): 均衡器观察真实的响应,把返回错误或超时的后端踢出去,冷却一段时间后再放回来。
健康检查端点要诚实,但也要轻量。如果它检查每一个依赖,只要一个数据库变慢,所有后端就会同时变成不健康,均衡器就无处可发流量了。只检查这个实例处理请求所需的东西,把共享依赖留给应用自身的错误处理。
连接排空与会话保持
当某个后端因为部署或缩容而被摘除时,它应该停止接收新请求,但要把正在处理的请求处理完。配置一个比最慢的正常请求还要长的排空时间(云负载均衡器称之为注销延迟),并让应用在关闭时不再接收新的工作。
会话保持(sticky session)通常借助 Cookie,让同一个用户每次都被发往同一个后端。当状态保存在服务器内存里时它有用,但对其他一切都有害:负载不均、后端宕机时会话丢失、部署更困难。把会话状态移到共享存储里,能关掉会话保持的地方就关掉。
跨越单个区域
上面的一切都发生在单个站点之内。把用户分散到多个区域是另一层:基于 DNS 的路由,返回最近的健康区域(按延迟或地理位置路由);或者 anycast,即多个站点宣告同一个 IP 地址,由互联网路由把每个用户送到附近的站点。全局负载均衡选择区域,本文的两层选择服务器。要让健康信号向上传递,这样当某个区域的代理连不上后端池时,它会停止接收用户,而不是向用户返回错误。
架构图
图表内容
- Clients
- DNS: one name
- Layer 4 load balancer
- Layer 7 proxy 1
- Layer 7 proxy 2
- Orders pool: 6 instances
- Search pool: 4 instances
- Static files bucket
- Proxy fleet, same config
- resolve
- TCP 443
- flow hash
- /api/orders
- /api/search
- /static
这张图的读法如下:
- 左侧: 客户端解析同一个 DNS 名称,并在 443 端口与四层负载均衡器建立 TCP 连接。
- 中间: 四层按流哈希把连接分发到代理集群。框架表示每台代理都运行同一份配置。
- 右侧: 代理终止 TLS,并按路径路由每个请求:
/api/orders和/api/search发往各自的池,/static发往存储桶。 - Markdown 块 写明了路由表、每个池使用的算法和健康检查设置,这些细节平时通常只存在于配置文件里。
负载均衡器图上应该标注什么
- 每一跳的层级和协议: TCP 443、HTTP/2、gRPC。
- TLS 在哪里终止。 它决定了每一层能看到什么,以及证书放在哪里。
- 每个池使用的算法, 如果用哈希,还要写出哈希键。
- 健康检查的路径、间隔和阈值。
- 连接排空和会话保持的设置, 如果有的话。
API 网关架构图 介绍了建立在路由之上的七层功能,Kubernetes Ingress 图 展示了集群内部的同样思路,网络拓扑图 则把负载均衡器放到更大的网络中来看。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加 C4 模型节点包中的一个 人员 表示客户端,再添加网络节点包中的一个 DNS 和一个 负载均衡器。
- 添加基础节点包中的一个 框架 表示代理集群,并在其中放入两个 容器 节点。
- 添加云节点包中的两个 计算实例 节点和一个 对象存储 节点,分别表示后端池和存储桶。
- 在连线上标注协议和路由,再添加一个基础节点包中的 Markdown 块,写明路由表和健康检查。
这个示例共用 10 个图表节点,在 Free 套餐的限制之内。
常见问题
四层负载均衡器和七层负载均衡器有什么区别? 四层均衡器分发 TCP 或 UDP 连接,不查看连接内部的内容。七层均衡器理解 HTTP,因此可以按路径或主机路由,并对单个请求进行均衡。
应该使用哪种负载均衡算法? 随机二选一的最少请求算法是 HTTP 的一个可靠默认值。当同一个键需要落到同一个后端时(比如缓存),使用一致性哈希。
为什么我的 gRPC 服务把所有流量都发到了一个 Pod? 因为 gRPC 使用长期存在的 HTTP/2 连接,而四层均衡器均衡的是连接,不是请求。在中间加一个七层代理,或者使用客户端负载均衡。
健康检查端点应该检查什么? 检查这个实例能否处理请求:进程正在运行,并且它自己的关键资源可用。避免检查那些会让所有后端同时失败的共享依赖。
会话保持是个好主意吗? 只有在状态无法离开服务器时才是。它会导致负载不均,并在故障时丢失会话;共享的会话存储通常更好。