放两台服务器,和扛得住故障,不是一回事
一句话总结
负载均衡的首要目的不是把请求平均分成两半,而是防止一台服务器的故障扩散成整个服务的故障。因此真正需要选择的不是算法,而是何时判定失败,以及何时重新加入。
为什么需要它
启动两台后端并放入 nginx upstream,并不代表高可用已经完成。如果不决定何时排除故障服务器、是否复用连接、会话存放在哪里,用户就会在每次请求时交替看到成功与失败。
最常见的事故是这样开始的:一台后端在部署过程中持续 30 秒返回 500。nginx 并不知道这一点,仍把一半请求发过去。在用户看来,就是“刷新后有时成功、有时失败”。监控只看平均值,50% 的错误率还可能被稀释成 25%,告警因此延迟。
如何选择算法
| 方式 | 分配依据 | 适用场景 | 注意事项 |
|---|---|---|---|
round_robin(默认) |
请求顺序 | 处理时间均匀的 API | 混入慢请求后偏差会增大 |
least_conn |
正在处理的连接数 | 响应时间波动较大的服务 | 长连接 SSE、WebSocket 会扭曲判断 |
ip_hash |
客户端 IP 哈希 | 把会话留在服务器内存中的遗留系统 | 增减服务器会整体改变分配 |
hash $key consistent |
任意键的一致性哈希 | 重视缓存命中率的场景 | 键设计不当会使流量偏向一侧 |
只有服务器性能不同时才使用 weight。相同规格却设置不同权重,会让容量计算失真,故障时其中一侧更早崩溃。
故障发现其实是被动的
max_fails 和 fail_timeout 不是主动健康检查。 它们通过观察真实请求失败,暂时排除服务器,因此会产生三个后果。
- 没有流量就发现不了故障。 凌晨宕机的服务器,要等早晨第一个请求成为牺牲品后才会被排除。
- 每个 worker 进程独立判断。 若
worker_processes 4,每个 worker 都有自己的计数器。max_fails=3时,最坏可能出现 12 次失败请求。 fail_timeout到期后,不做确认就重新加入。 服务器尚未恢复时会再次失败,如此循环。
哪些情况计为失败,由 proxy_next_upstream 决定。默认值是 error timeout,因此后端返回的 500 不算失败。 即使应用已经出错并持续返回 500,nginx 仍会向它发请求。必须显式加入 http_500 http_502 http_503 才会计数。但若没有同时谨慎处理 non_idempotent,POST 可能被执行两次,因此重试对象必须限定为幂等请求。
upstream Keep-Alive 必须同时满足三个条件
复用连接可以省去 TCP 握手与 TLS 协商,但必须同时完成三项配置。缺少任何一项,系统都会静默地为每次请求新建连接。
upstream app {
server 127.0.0.1:8080;
server 127.0.0.1:8082;
keepalive 32; # 1) 워커당 유지할 유휴 연결 수
}
location / {
proxy_pass http://app;
proxy_http_version 1.1; # 2) 기본값 1.0 은 keep-alive 를 못 쓴다
proxy_set_header Connection "";# 3) 클라이언트가 보낸 Connection 헤더를 지운다
}
keepalive 不是“并发处理量”,而是“保留的空闲连接数”。它乘以 worker 数量后,不应超过后端最大并发连接设置。若 Tomcat 的 maxThreads 为 200,而 4 个 nginx worker 各保留 100 条连接,就会有 400 条连接堆积等待线程。
会话粘滞是最后手段
使用 ip_hash 固定会话时,只要增减服务器,哈希空间就会改变,大量用户会被重新分配到其他服务器,而新服务器内存中没有其会话,于是同时退出登录。 每次部署都可能发生。而公司或学校网络中的用户常位于同一 NAT 后方,IP 相同,流量还会集中到一台服务器。
优先级应如下。
- 把会话移出服务器——放到 Redis 或 DB,任何服务器都能处理
- 把状态放进令牌——使用签名 JWT 时,服务器无需记忆任何内容
- 实在不行再粘滞——此时基于 cookie 的 sticky 也优于
ip_hash
在实际项目中
故障只偶尔出现时,先在 access log 中记录 $upstream_addr 与 $upstream_status,按后端拆分结果。
log_format lb '$remote_addr [$time_local] "$request" $status '
'$upstream_addr $upstream_status '
'$upstream_response_time $request_time';
关键是会读日志。如果 $upstream_addr 中出现由逗号分隔的两个地址,说明发生过重试(127.0.0.1:8080, 127.0.0.1:8082)。只有特定地址返回 5xx 时,应先检查部署或配置差异,而不是分配算法;所有地址同时变慢时,再调查公共 DB 或下游服务。
$upstream_response_time 与 $request_time 的差异也用于诊断。两者相近,说明后端慢;只有 $request_time 较大,说明客户端网络或响应传输较慢。
下一实验要做什么
亲自启动两个后端,对比轮询、权重、会话粘滞、故障排除与连接复用。在日志中记录 $upstream_addr,统计请求发往哪一侧;再故意关闭一台,确认 max_fails 实际在多少次失败后生效。最后整理每种算法应何时使用、何时避免的运维标准。