从单机到 upstream
应用从一台机器扩到多台,最直接的做法是在前面挂一个 Nginx,把请求分发到后端。Nginx 1.24 之后,upstream 模块的能力已经覆盖大多数中小规模场景:轮询、权重、最少连接、IP 哈希、一致性哈希、备份节点、被动健康检查,都只需要改配置。
但“加了 Nginx 就不再有单点”是个误解。Nginx 自己也可能成为单点,后端节点的健康判断也可能误杀。负载均衡配置的核心,是让流量在节点故障时平滑转移,而不是简单地写几个 server 地址。
负载策略怎么选
| 策略 | 行为 | 适用场景 |
|---|---|---|
| 默认轮询 | 按顺序分发,可配 weight | 后端性能相近、无状态服务 |
| weight | 按权重分配 | 机器配置不同,按比例分流 |
| least_conn | 优先发给当前连接数最少的节点 | 请求耗时差异大、长连接多 |
| ip_hash | 同一客户端 IP 固定到同一节点 | 需要简单会话保持,但扩缩容不友好 |
| hash $key | 按自定义 key 做一致性哈希 | 缓存分片、按用户固定路由 |
无状态服务优先用轮询或 least_conn。需要会话保持时,先考虑把会话放到 Redis,而不是用 ip_hash——后者在 NAT、CDN 后面会把大量用户映射到同一台机器,扩缩容时也会打乱映射。
一段可用的配置
upstream order_backend {
least_conn;
server 10.0.0.11:8080 weight=3 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 weight=1 max_fails=3 fail_timeout=10s;
server 10.0.0.13:8080 backup;
keepalive 64;
}
server {
listen 80;
server_name order.example.com;
location / {
proxy_pass http://order_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_connect_timeout 2s;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
}
}
keepalive 64 是 Nginx 到后端的长连接池,必须配合 proxy_http_version 1.1 和 Connection "" 才能生效。少了这两行,Nginx 每个请求都新建 TCP 连接,压测时能看到大量 TIME_WAIT。proxy_next_upstream 表示遇到哪些错误时换下一个节点重试,tries 限制重试次数,避免故障时把请求放大。
注意 POST 请求的重试语义。默认情况下,Nginx 不会对非幂等请求(POST、LOCK、PATCH)做重试,即使 proxy_next_upstream 里配了 http_502;只有显式加上 non_idempotent 参数才会重试。所以不要为了“多试一次”随手打开它,下单这类接口一旦重复提交,代价很高。
健康检查的真相
开源版 Nginx 只有被动健康检查:请求失败时累加 max_fails,达到阈值后在 fail_timeout 时间内不再把请求发给该节点。它没有主动探测能力,节点挂掉后要等第一个请求失败才会被摘除。max_fails=0 会关闭失败计数,fail_timeout 同时表示统计窗口和摘除时长。
主动健康检查是 NGINX Plus 的能力,开源版通常用这几种替代方案:在 Nginx 前面放一个外部健康检查脚本,动态改写 upstream 并 reload;用 OpenResty 的 lua-resty-healthcheck;或者干脆把服务发现交给 Kubernetes,Nginx 只做入口。
健康检查接口本身也要设计好。如果用 /actuator/health 聚合了数据库和 Redis,数据库抖动时所有节点一起被判定为不健康,Nginx 会把流量全部摘掉。负载均衡用的检查应该只看进程能否处理请求,依赖状态放到 readiness 或独立的检查分组里。
常见坑
1. 没配 keepalive,端口被 TIME_WAIT 占满
Nginx 到后端短连接,QPS 一高,ss -s 里 TIME_WAIT 成千上万。加上 keepalive 和 HTTP/1.1 复用,能显著降低连接建立开销。
2. proxy_next_upstream 重试了非幂等请求
超时后自动换节点重试,订单可能被创建两次。重试只对查询、幂等写操作开放;支付、下单这类接口要么不重试,要么在业务层做幂等键。
3. ip_hash 在代理后面失真
请求经过 CDN 或公司出口 NAT,Nginx 看到的 remote_addr 是代理 IP,所有用户被哈希到同一台后端。要么让代理透传真实 IP 并配置 real_ip 模块,要么改用 Redis 共享会话。
4. 节点恢复后流量瞬间打满
fail_timeout 结束后,节点重新接收流量,如果它刚启动、缓存还是冷的,很容易被第一波流量压垮。可以先用低权重接入,或者配置慢启动(开源版不支持,需要借助动态 upstream)。
5. DNS 解析结果被缓存
proxy_pass http://backend.example.com 在启动时解析一次,后端 IP 变了不会自动更新。用 resolver 配合变量形式的 proxy_pass,或者变更后 reload;Kubernetes 环境更适合用 Service 做服务发现。
排查步骤
先确认配置语法和实际加载的 upstream:
nginx -t
nginx -T | grep -A 10 'upstream order_backend'
再带 Host 头请求,确认命中的是哪个后端:
curl -i -H 'Host: order.example.com' http://127.0.0.1/actuator/health
在 access log 里加上 upstream 字段,能直接看到每次请求打到了哪台机器、状态码和耗时:
log_format upstream_log '$remote_addr "$request" $status '
'upstream=$upstream_addr status=$upstream_status '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/upstream.log upstream_log;
如果 urt 很高但后端应用日志显示很快,检查是不是连接复用没生效、或者 Nginx 到后端的网络有问题。error.log 里的 connect() failed、upstream timed out 能区分连接失败和读超时。
小结
负载均衡配置的关键不是策略多高级,而是连接复用、失败摘除、重试语义这三件事。先保证 keepalive 和超时参数合理,再根据业务选择轮询或最少连接。健康检查要区分“进程活着”和“依赖可用”,否则一次数据库抖动就会演变成全站不可用。
评论