负载均衡
这一课你将能做到
- 在
upstream 中声明多个 server,并说明默认轮询、least_conn、ip_hash、权重各自适合什么
- 解释 CodeRoadMap 在 Sa-Token + Redis 会话 下为何通常 不需要
ip_hash
- 分清开源 Nginx 的 被动
max_fails/fail_timeout 与 K8s readiness 主动探针的差异
为什么要学
单机双进程够入门;流量上来要水平扩展。Nginx 的 upstream 多后端是最直观入口:改配置即可把请求洒到多台 Spring 或多台 Next。
选错策略会把有状态请求打飞(若会话粘在单机内存),或把慢实例打满。
本课建立「无状态 API + 共享会话」心智,并连到后续 Ingress/集群健康检查。
先建立图像
Nginx
│
┌─────────┼─────────┐
▼ ▼ ▼
backend-1 backend-2 backend-3
:8080 :8080 :8080
│ │ │
└─────────┴─────────┘
│
Redis 会话 / MySQL
| 策略 | 行为 | 典型场景 |
|---|
| 默认轮询 | 依次分发 | 无状态 REST |
least_conn | 最少活跃连接 | 慢请求、长连接多 |
ip_hash | 同 IP 粘滞 | 简易会话粘滞(次优) |
weight | 按权重比例 | 机器规格不均 |
人话:负载均衡是「多个柜台办理同一业务」;会话放 Redis,就像「叫号系统在云端」,换柜台也不丢号。
跟着做
步骤 1:读官方 upstream
打开 ngx_http_upstream_module,浏览 server 参数:weight、max_fails、fail_timeout、down 等。
步骤 2:写多实例 backend 草稿
upstream coderoadmap_backend {
least_conn;
# weight:大机器多接;max_fails/fail_timeout:被动摘除
server 10.0.1.11:8080 weight=3 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
location /api/ {
proxy_pass http://coderoadmap_backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
笔记:「当前生产可仍是单 127.0.0.1:8080;扩展时只改 upstream。」
步骤 3:platform 水平扩展注意点
upstream coderoadmap_platform {
server 10.0.1.21:3000;
server 10.0.1.22:3000;
keepalive 64;
}
- 各实例 同一构建版本(RSC/chunk hash 一致)
- 无依赖单机内存的会话(或会话外置)
- 静态资源路径一致
步骤 4:为何优先 Redis 而非 ip_hash
Spring 多实例 + Sa-Token Redis 时,任意节点可校验 token,不必把同一用户钉死在一台机。
ip_hash 的问题:NAT 出口大、换 IP、不均。
结论:本站推荐 无状态应用 + 共享会话存储。
步骤 5:健康检查现实
开源 Nginx:没有企业版那种主动 HTTP 健康检查;主要靠请求失败计数被动标记。
生产常见补位:
# 探的是 Nginx 自己是否活着,不是 upstream 成员
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
步骤 6:Compose scale 思维实验
docker compose up -d --scale backend=3
DNS 可能返回多 A 记录;裸机 Nginx 常 显式列出 各容器 IP,或使用支持 resolve 的方案。先理解「列表里每一行是一个后端」,再上动态发现。
步骤 7:把「无状态」写成上线条件
- 会话在 Redis(或等价共享存储),不是单机内存 Dao
- 上传/临时文件不落在单实例本地磁盘当唯一真相
- 定时任务有分布式锁或只在一个角色实例跑
- 配置与密钥通过环境变量/配置中心注入,各节点一致
缺任何一条就先补齐,再改 upstream 加机器——否则负载均衡只是把故障放大到更多节点。
步骤 8:观察与回滚
# 连续请求,观察 access 或应用日志是否落到不同实例(需在应用日志打 hostname)
for i in 1 2 3 4 5 6; do curl -sS -o /dev/null -w "%{http_code}\n" https://test.example/api/tracks; done
若新节点异常:先在 upstream 对该 server 加 down;,nginx -t 后 reload,流量回到健康节点;修好后再去掉 down。
这比「整台 Nginx restart」更可控,也是多后端运维的基本动作。
讲清楚要点
- 因为 upstream 可列多个 server,所以水平扩展入口往往只改这一块——location 语义不变。
- 因为默认轮询假设实例同质,所以机型不均时用 weight——大机器多接。
- 因为会话进 Redis,所以多 Spring 实例通常不需要 ip_hash——避免粘滞副作用。
- 因为开源版偏被动失败检测,所以关键服务要有外层健康机制——K8s 或运维探针。
- 因为 Next 多实例要共享同一构建,所以发布需同步滚动——避免 chunk 对不上。
- 因为 keepalive 与多后端同时存在,所以池大小要按总连接估算——别只抄数字。
- 因为官方策略指令有限但够用,所以先轮询/least_conn,再谈复杂一致性哈希——够用即止。
你可能会懵的地方
-
max_fails 失败是什么失败?
与上游通信失败(连不上、超时等)会累计;应用返回 500 不一定算进(取决于版本与配置)。不要把它当成完整业务健康检查。
-
能不能一半流量去新版本?
可用 weight 做简易金丝雀;更细的灰度常在网关/Ingress/服务网格层做。
-
ip_hash 和 sticky cookie?
开源 Nginx 的会话粘滞能力有限;共享会话存储仍是本站首选。
-
一个 upstream 挂了会怎样?
被动摘除后流量打到剩余节点;若全挂,客户端看到 502。监控与容量规划要提前。
-
weight 与 least_conn 能一起用吗?
可以组合使用,但推理成本上升。入门阶段:机器同质用默认轮询或 least_conn;规格差异明显再用 weight。先保证会话外置与健康检查,再微调比例。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 多实例仍用内存会话 | 登录随机失效 | Sa-Token Redis 等共享会话 |
| 滥用 ip_hash 当银弹 | 不均、NAT 问题 | 无状态 + Redis |
| 只扩容不监控 | 坏节点仍接流 | 外层健康检查 + 告警 |
| platform 版本不一致 | 前端资源错乱 | 同步发布同一构建 |
把 /nginx-health 当 upstream 探针 | 测错对象 | 健康检查打到应用 readiness |
| 新节点异常却不标记 down | 部分用户持续 502 | 临时 down + reload,修好再摘 |
| 有本地上传目录当权威存储 | 文件随机 404 | 对象存储或共享卷 + 无状态节点 |
自测三问
- 默认轮询与
least_conn 各自适合什么?
- 为什么 CodeRoadMap 优先 Redis 会话而不是 ip_hash?
- 开源 Nginx 的
max_fails 与 K8s readiness 差在哪里?
小结一句话
upstream 多 server 即水平扩展入口;会话靠 Redis,健康检查交给 K8s 或运维,而不是神话 ip_hash。
官方延伸