系统设计 05:负载均衡与网关——请求分配不等于连接有余量
两台后端均分请求,不能据此推断每台仍可接新连接。一次请求可以很快结束,也可以占住连接数秒;健康的实例也可能已用完连接名额。路由必须同时回答“往哪台送”和“这台现在能否接”,失败时还要说清拒绝与重试责任。
沿用先测瓶颈再扩容的分享元数据服务。本文只处理入口到无状态应用实例这一跳:POST /shares 写入、GET /shares/{id} 读取、所有者列表读取;权限和撤销仍由应用与权威库共同维护,不因为网关能缓存或改写请求而转移到入口。排除跨地域、会话黏性、TLS 证书生命周期和详细服务发现协议。
定义目标和连接负载
独立练习假设:每天 110 万次读和 10 万次写,读写比 11:1;峰值 360 次读/秒、40 次写/秒,共 400 次/秒(读、写各为日均的约 28.28 与 34.56 倍)。读响应平均有效载荷按 20 KiB,峰值约 360 × 20 KiB = 7.03 MiB/s ≈ 58.98 Mbit/s,不含 TLS、重试、连接升级和网关日志。写元数据平均 1 KiB,写入峰值约 0.039 MiB/s。
假设峰值请求中 80% 的连接平均占用 0.2 秒,20% 的连接平均占用 5 秒,且处于稳定到达与完成的条件下,平均同时活跃的连接约 320/s × 0.2s + 80/s × 5s = 464。这是 Little 的平均量关系 的练习推算,不是测出的连接数或 p99;HTTP 复用、一连接多请求、实际到达分布都可能让它失准。普通短请求的服务端 p99 目标暂定 200 ms,长请求须另设时长目标;网关因健康/容量拒绝的请求占比暂定小于 0.1%。没有真实样本,本篇不宣称达成。
两台上游各预留 250 条活跃连接时,练习预算总计 500 条,看起来大于 464;若一台下线,另一台的 250 条小于 464。只把长连接平均占用从 5 秒改成 10 秒,推算变为 320 × 0.2 + 80 × 10 = 864 条,即使两台都在也不够。这个敏感性比“峰值 QPS 不变,容量就不变”更能指导测量:要看活跃连接、空闲 keepalive、文件描述符、队列和每台上游的耗时分布,而不是只查 CPU。
为了不把网关开销藏进应用预算,按每次写入 1000 B 元数据与索引、365 天三份计 109.5 GB;每次读写各一条 200 B 网关诊断记录,7 天一份计 1200000 × 200 × 7 = 1.68 GB,两项合计 111.18 GB。若练习价格为 0.02 货币单位/(GB·月),这两项稳态容量成本约 2.22 货币单位/月,没有包括网关计算与流量、实际数据库日志、连接保持成本或备份。记录不应无节制地包含用户敏感数据。
网关路由不是无限排队
最小架构是两台无状态应用、一个权威数据库和入口网关。网关校验请求基本格式、施加总体与单上游预算、选择健康且有余量的目标;应用仍做授权和事务确认。GET /shares/{id} 可重试前要知道是否到达过上游;POST /shares 超时后即使网关换节点也不得无条件重放,必须由应用的幂等键维持同一写入归属。
flowchart LR
C[客户端] -->|GET / POST,带追踪与幂等键| G[网关]
G -->|健康且有连接名额| A[应用 A]
G -->|健康且有连接名额| B[应用 B]
A -->|读写| D[(权威数据库)]
B -->|读写| D
A -->|完成 / 连接释放 / 错误| G
B -->|完成 / 连接释放 / 错误| G
G -->|响应或限时拒绝| C
轮询分流结构简单,却看不见某条连接占用五秒的差异;按活跃连接数选择目标有助于解释这类差异,却仍不等于按 CPU、内存或数据库等待时间选。两种策略都要有健康判断和容量上界。以 NGINX upstream 文档为尚未部署的实现候选,官方提供 least_conn、max_fails/fail_timeout 与 max_conns,但 max_conns 在不使用共享内存时按 worker 生效;多 worker、共享内存与空闲 keepalive 组合下总连接数也可能超过设定值(NGINX upstream 文档)。不能把“填了一个配置项”当成全局严格连接上限,本篇没有 NGINX 实测。
模型 examples/system-design/labs/05/route.py 只把连接计数与健康状态分开:a、b 各最多两条活跃连接;前四次选择 a,b,a,b,第五次因都满而拒;将 a 标为不健康、释放 b 一个名额后,第六次仍可路由到 b,再一次则拒绝。如果启动 --ignore-capacity,b 活跃数变成 3,反例以 2 退出;正常命令以 0 退出。原始输出和限制见 examples/system-design/evidence/05/routing.md。此脚本没有打开任何网络连接;绝不能拿它证明 NGINX 的故障摘除、健康探针或真实连接上限。
1 | |
故障从检测到恢复要有顺序
时间线上先是 a 不再通过健康判断,但它原有连接还没自然结束;网关应阻止新请求发往 a。随后 b 的名额逐步被占满,网关不能因另一台机器还留着既有连接就继续放行。容量耗尽时可按策略返回 HTTP 503,并在合理情况下给 Retry-After;这是 RFC 9110 对状态与建议等待时间的规定,不是探针算法(503、Retry-After)。如果失败实例恢复,先做少量试探流量、核对错误率与活跃连接,再逐步放量;探测结果过期或故障反复,应收回流量而不是连续无界重试。
sequenceDiagram
participant C as 客户端
participant G as 网关
participant A as 应用 A
participant B as 应用 B
C->>G: 请求 1 至 4
G->>A: A 获得两个活跃名额
G->>B: B 获得两个活跃名额
Note over G,A: A 被标记不健康;旧连接仍占名额
B-->>G: 一个请求结束,B 释放名额
C->>G: 新请求 6
G->>B: 转发到 B
C->>G: 新请求 7
G-->>C: B 已满,返回 503 或有界等待
Note over G,B: 别用无限重试把单机故障扩大到 B
面试的追问不应停在“两台应用 + 网关”:先列读写峰值和连接持有时间,再说明健康、容量与拒绝是三项独立判断,最后问写请求在网关超时后如何确认结果。一个恢复方案是观测名额回落后逐步重新纳入 a;另一个是让 b 接管全部流量,只有实测单实例能承受时才成立。本文没有真实网关运行、浏览器 Mermaid 渲染或负载压测,不能把模型路径升级为生产承诺。
参考资料
- NGINX HTTP upstream module:连接限制、失败次数及路由算法。本篇未部署或验证 NGINX,引用范围限于官方文档的选项语义及限制。
- RFC 9110 §15.6.4 与 §10.2.3:503 与 Retry-After 的 HTTP 语义。
- Little, 1961, A Proof for the Queuing Formula: L = λW:平均在途量与平均到达/占用关系;不替代本篇欠缺的连接测量。




