ElIauk

Nginx 负载均衡与健康检查

从单机到 upstream 应用从一台机器扩到多台,最直接的做法是在前面挂一个 Nginx,把请求分发到后端。Nginx 1.24 之后,upstream 模块的能力已经覆盖大多数中小规模场景:轮询、权重、最少连接、IP 哈希、一致性哈希、备份节点、被动健康检查,都只需要改配置。 但“加了 Ngin

评论
Nginx 负载均衡与健康检查

从单机到 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 和超时参数合理,再根据业务选择轮询或最少连接。健康检查要区分“进程活着”和“依赖可用”,否则一次数据库抖动就会演变成全站不可用。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者