切换语言
切换主题

Nginx 负载均衡实战:upstream 配置与健康检查

凌晨两点,手机疯狂震动。打开监控面板,backend1 的状态栏一片红——单台应用服务器挂了。

那年双十一大促,我们只有两台后端服务器。Nginx 配置里写着 upstream backend { server backend1; server backend2; },看起来挺对称。但 backend1 承担了 70% 的流量,因为它在配置文件里排第一位,而我们的 Nginx 用的是默认轮询——没配置权重,也没配置健康检查。

backend1 宕机的那一刻,用户的订单请求还在往这台死掉的服务器上发。Nginx 不知道它挂了,继续把请求往那儿转发。用户看到的?500 错误页面。等到运维手动把 backend1 从 upstream 里摘掉,已经过去了 15 分钟。

这事之后我补了功课:Nginx upstream 不只是列出服务器地址那么简单。权重分配、健康检查、故障转移,这些才是生产环境真正需要的。这篇文章把我踩过的坑和后来学到的配置方法整理出来,包括开源 Nginx 怎么实现主动健康检查(不用花钱买 NGINX Plus)。

1. upstream 基础配置:从单机到集群

upstream 块的核心作用很简单:把多台服务器打包成一个逻辑组,让 Nginx 知道请求该往哪儿转。但它的参数比很多人想象的要丰富。

upstream backend {
  zone backend 64k;
  server backend1.example.com weight=3 max_fails=2 fail_timeout=30s;
  server backend2.example.com;
  server backup1.example.com backup;
}

逐行解释一下:

zone backend 64k:共享内存区域。Nginx 的 worker 进程之间需要共享后端服务器的状态信息(谁还活着、谁挂了)。64k 是起步值,服务器数量多的话可以适当加大。没有这行配置,worker 之间各管各的状态,会出问题。

weight=3:权重。backend1 的权重是 3,backend2 默认是 1。这意味着每 4 个请求,3 个去 backend1,1 个去 backend2。适合后端服务器配置不均匀的场景——比如 backend1 是 8 核 16G,backend2 是 4 核 8G。

max_fails=2:失败次数阈值。在 fail_timeout 时间窗口内,如果对这个服务器的请求失败了 2 次,Nginx 就会把它标记为不可用。默认值是 1 次,这个太敏感了——一次网络抖动就触发,不合适。生产环境建议设成 2 或 3。

fail_timeout=30s:双重含义。第一,失败计数的时间窗口是 30 秒;第二,服务器被标记为不可用后,Nginx 会在 30 秒后再次尝试连接它。默认 10 秒,对于某些慢启动的服务可能不够。

backup:备用服务器。只有当所有主服务器都不可用时,backup 服务器才会接收请求。适合用配置较低的服务器做兜底。

还有一个参数 down,用于手动标记服务器离线,常用于维护:

server backend3.example.com down;  # 临时下线维护

实际使用时,我发现很多人忽略 zone 配置。结果是 worker 进程各自维护状态,某个 worker 发现服务器挂了,其他 worker 还在往那儿发请求。加上 zone 之后,状态同步问题就解决了。

2. 五种负载均衡策略:何时用哪种?

默认的 round-robin 轮询策略够用吗?看情况。

我见过不少项目用默认轮询跑了好几年,也没出问题。但当你遇到 WebSocket 长连接、购物车 session、或者缓存穿透的场景,就会发现默认策略不太合适。下面这张表是我总结的选择指南:

场景推荐策略理由
无状态 APIround-robin均匀分配,无需特殊处理
WebSocket 服务least_conn连接数动态监控,避免某台过载
电商购物车ip_hash同一用户请求去同一服务器
缓存代理hash key=$uri减少缓存失效和穿透
测试环境random快速验证,配置简单

round-robin(默认轮询)

什么都不配置就是轮询。请求按顺序依次发给每台服务器:

upstream backend {
  server backend1.example.com;
  server backend2.example.com;
  server backend3.example.com;
}

适合无状态服务。每个请求独立,不依赖之前的请求状态。大多数 REST API 都适用。

least_conn(最少连接)

优先把请求发给当前连接数最少的服务器:

upstream websocket_app {
  least_conn;
  server ws1.example.com:8080;
  server ws2.example.com:8080;
}

WebSocket 服务的典型场景。一个用户建立一个长连接,连接数波动大。如果用轮询,可能某台服务器积压了大量长连接,新请求还继续往它那儿发。least_conn 会实时监控连接数,把新请求发给负载最低的那台。

ip_hash(IP 哈希)

根据客户端 IP 地址计算哈希值,同一 IP 的请求总是发给同一服务器:

upstream shopping_cart {
  ip_hash;
  server cart1.example.com;
  server cart2.example.com;
}

适合需要 session 一致性的场景。比如电商购物车,用户在 cart1 上添加了商品,如果下一个请求被轮询到 cart2,购物车数据就找不着了(除非你用了分布式 session 存储)。ip_hash 解决了这个问题。

但 ip_hash 有个局限:如果某台服务器挂了,原本 hash 到这台服务器的用户会被重新分配。这部分用户的 session 会丢失。所以 ip_hash 适合 session 数据不那么关键的场景,或者配合 session 共享存储使用。

hash(一致性哈希)

自定义哈希键,支持一致性哈希算法:

upstream cache_proxy {
  hash $uri consistent;
  server cache1.example.com;
  server cache2.example.com;
}

缓存代理场景首选。$uri 是请求路径作为哈希键。consistent 参数启用一致性哈希——当服务器增减时,只有部分 key 被重新映射,而不是全部洗牌。这样缓存命中率不会大幅下降。

random

简单随机分配:

upstream test_backend {
  random;
  server test1.example.com;
  server test2.example.com;
}

测试环境够用了。生产环境不建议用,因为缺乏控制力。

说实话,我的经验是:大多数 Web 应用用 round-robin 或 least_conn 就够了。ip_hash 和 hash 属于特定场景的解决方案,不要为了”显得高级”而强行使用。

3. 被动健康检查:max_fails 与 fail_timeout

“被动”的意思是:Nginx 不主动探测后端服务器健康状态,而是通过观察实际请求的成败来判断。就像你不会主动去敲门问邻居”你还好吗”,而是看他有没有出门遛狗、有没有收快递——通过日常行为间接判断。

max_fails 和 fail_timeout 这两个参数,就是在配置这种”观察机制”:

upstream backend {
  server backend1.example.com max_fails=3 fail_timeout=30s;
  server backend2.example.com max_fails=3 fail_timeout=30s;
}

location / {
  proxy_pass http://backend;
  proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
}

proxy_next_upstream 指定了哪些情况算”失败”。error 是连接错误,timeout 是超时,http_500 到 http_504 是各种 HTTP 错误状态码。遇到这些情况,Nginx 会把请求转发给下一台服务器,同时给当前服务器记一次失败。

30 秒内失败 3 次,服务器被标记为不可用。接下来的 30 秒,Nginx 不再往这台服务器发请求。30 秒后,Nginx 会尝试一次——如果成功,服务器恢复;如果失败,继续等待 30 秒。

这套机制的问题是:故障响应慢。至少要等到真实用户请求失败 3 次,服务器才会被摘掉。这 3 个用户已经收到错误响应了,体验受损。

更极端的情况:服务器刚启动还在初始化,健康状态不稳定。max_fails=1 的配置可能因为一次启动期间的失败就把服务器标记为不可用,导致它一直被排除在外。

我的建议:

  • max_fails 设成 2 或 3,容忍偶尔的网络抖动
  • fail_timeout 设成 30 秒以上,给服务器恢复的机会
  • proxy_next_upstream 配置完整的错误类型列表,避免漏掉某些失败情况

被动检查的优点是配置简单,开源 Nginx 直接支持。缺点是依赖用户请求触发,用户体验会先受损。如果你需要更快发现故障、主动探测后端状态,那就得用主动健康检查了。

4. 主动健康检查:NGINX Plus 与开源替代方案

主动健康检查的逻辑是:Nginx 定期向后端服务器发送探测请求(比如 GET /health),根据响应状态判断服务器健康与否。不需要等用户请求失败,Nginx 自己就能发现故障服务器并提前摘掉。

官方的方案是 NGINX Plus(商业版),年费 $3,675/实例。10 个实例一年就要 $36,750。说实话,这个价格对不少公司来说偏高。

开源方案是 nginx_upstream_check_module,淘宝技术团队开发的。需要重新编译 Nginx 加入这个模块,但功能相当完整:

特性NGINX Plusnginx_upstream_check_module
价格$3,675/年开源免费
HTTP 检查支持支持
TCP 检查支持支持
MySQL 检查不支持支持
FastCGI 检查不支持支持
状态页面支持支持(check_status)

开源模块支持 MySQL 和 FastCGI 检查,这是 NGINX Plus 没有的功能。如果你的后端是 PHP-FPM 或 MySQL,这个模块更合适。

nginx_upstream_check_module 配置示例

upstream backend {
  server backend1.example.com:8080;
  server backend2.example.com:8080;

  check interval=3000 rise=2 fall=5 timeout=1000 type=http;
  check_http_send "GET /health HTTP/1.0\r\n\r\n";
  check_http_expect_alive http_2xx http_3xx;
}

server {
  location / {
    proxy_pass http://backend;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

参数解释

  • interval=3000:每 3 秒发送一次探测请求
  • rise=2:连续 2 次成功后,服务器标记为健康(刚启动的服务器可能不稳定,需要连续成功确认)
  • fall=5:连续 5 次失败后,服务器标记为不可用(容忍偶尔的超时)
  • timeout=1000:探测请求超时时间 1 秒
  • type=http:使用 HTTP 协议探测(还支持 tcp、ssl_hello、mysql、ajp、fastcgi)

check_http_send 定义了探测请求的内容。这里发送一个简单的 GET /health 请求。后端需要实现 /health 接口,返回 200 或 3xx 状态码。

check_http_expect_alive 指定哪些状态码算”健康”。http_2xx 和 http_3xx 表示 200-299 和 300-399 的状态码都算成功。

check_status 提供状态监控页面。json 格式输出,方便对接 Prometheus 或 Zabbix。后面那段 allow/deny 是访问控制——这个页面不能随便暴露。

模块安装方法

nginx_upstream_check_module 需要编译安装。大致步骤:

# 下载模块源码
git clone https://github.com/yaoweibin/nginx_upstream_check_module.git

# 下载 Nginx 源码
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz

# 打补丁(根据 Nginx 版本选择)
cd nginx-1.24.0
patch -p1 < ../nginx_upstream_check_module/check_1.20.1+.patch

# 编译
./configure --add-module=../nginx_upstream_check_module
make && make install

如果你用 Docker 部署,可以自己构建一个包含模块的镜像,或者找社区现成的镜像。

5. 生产环境实战:安全与监控

健康检查的配置在生产环境有几个细节容易踩坑。我总结了三条原则:

安全配置:三个要点

1. check_status 页面必须加访问控制

状态页面暴露了后端服务器列表和健康状态。如果被外部访问,攻击者能看到你的内部拓扑信息,还能知道哪台服务器当前不可用——这正是攻击的好时机。

location /upstream_status {
  check_status json;
  allow 127.0.0.1;       # 本地访问
  allow 10.0.0.0/8;      # 内网 IP
  deny all;              # 拒绝其他
}

或者更严格,只允许特定监控服务器的 IP。

2. 使用独立健康检查端口

健康检查请求是高频的(每 3-5 秒一次),如果直接探测业务端口,后端日志会记录大量 /health 请求。日志文件膨胀,影响性能。

建议后端服务监听两个端口:业务端口(如 8080)和健康检查端口(如 8888)。健康检查端口只返回简单的状态码,不处理业务逻辑,也不写日志。

check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;

port=8888 指定探测专用端口。

3. 健康检查接口不返回敏感信息

/health 接口只需要返回状态码,不要返回版本号、配置信息、内存使用等内部数据。攻击者会利用这些信息定位漏洞。

# 后端实现示例
@app.route('/health')
def health():
    return '', 200   # 只返回状态码

监控集成:JSON 输出

check_status 支持多种格式。json 格式适合对接监控系统:

curl http://127.0.0.1/upstream_status

输出示例:

{
  "servers": {
    "total": 3,
    "generation": 12,
    "server": [
      {"index": 0, "name": "10.0.0.1:8080", "status": "up", "rise": 5, "fall": 0, "type": "http"},
      {"index": 1, "name": "10.0.0.2:8080", "status": "up", "rise": 3, "fall": 0, "type": "http"},
      {"index": 2, "name": "10.0.0.3:8080", "status": "down", "rise": 0, "fall": 5, "type": "http"}
    ]
  }
}

generation 是配置变更计数器。每次修改 upstream 配置并 reload,generation 值会增加。监控脚本可以对比这个值,确认配置已生效。

参数调优建议

interval 不低于 3000ms

探测频率太高会给后端造成压力。3-5 秒的间隔够用了,故障发现延迟在秒级,不影响用户体验。

rise 和 fall 阈值的平衡

  • rise 值太小(比如 1),服务器刚启动可能因为初始化未完成而被误判为不健康,然后被快速恢复,反复震荡
  • fall 值太小(比如 1),一次网络抖动就会触发摘除,过于敏感

我的经验值:rise=2,fall=3 或 fall=5。容忍瞬时故障,确认持续故障后再摘除。

完整生产配置

upstream web_app {
  zone web_app 64k;
  server 10.0.0.1:8080 weight=3;
  server 10.0.0.2:8080;
  server 10.0.0.3:8080 backup;

  check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;
  check_http_send "GET /health HTTP/1.1\r\nHost: app.example.com\r\n\r\n";
  check_http_expect_alive http_2xx;
}

server {
  listen 80;
  server_name app.example.com;

  location / {
    proxy_pass http://web_app;
    proxy_set_header Host $host;
    proxy_next_upstream error timeout http_502 http_503 http_504;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

这套配置:

  • zone 共享内存保证 worker 状态同步
  • 主动健康检查每 5 秒探测一次专用端口
  • 状态页面只允许内网访问
  • proxy_next_upstream 保证故障服务器上的请求被转发到健康的后端

结论

说回那年双十一的事故。后来我们给 upstream 加了 zone 共享内存,配置了 max_fails 和 fail_timeout,再后来编译安装了 nginx_upstream_check_module 做主动健康检查。服务器宕机时,Nginx 在 5 秒内就能发现并摘掉它,用户基本不会收到错误响应。

负载均衡策略的选择,其实就一句话:无状态服务用 round-robin 或 least_conn,有状态服务用 ip_hash 或 hash。健康检查的选择:生产环境必须有,开源方案用 nginx_upstream_check_module,别忘了给 check_status 页面加访问控制。

如果你的 Nginx 还在用默认轮询、没配置健康检查,建议先从被动检查(max_fails + fail_timeout)起步。这个改动最小,效果立竿见影。等验证稳定后,再考虑升级到主动健康检查。测试环境先试一遍,确认配置没问题再上线生产。

常见问题

Nginx upstream 的 zone 配置有什么作用?
zone 配置创建共享内存区域,让多个 worker 进程共享后端服务器状态信息。没有 zone,各 worker 独立维护状态,可能出现某个 worker 发现服务器故障但其他 worker 还在往那儿发请求的情况。
被动健康检查和主动健康检查有什么区别?
被动检查通过观察实际请求成败判断服务器状态,依赖用户请求触发,故障发现慢。主动检查由 Nginx 定期发送探测请求,不依赖用户请求,故障发现快,可在用户受影响前摘除故障服务器。
nginx_upstream_check_module 和 NGINX Plus 哪个更好?
功能上各有优势:NGINX Plus 有官方支持和完整文档,nginx_upstream_check_module 开源免费且支持 MySQL/FastCGI 检查(NGINX Plus 不支持)。预算敏感的场景推荐开源模块,需要稳定性和官方支持的选择 NGINX Plus。
负载均衡策略 round-robin 和 least_conn 怎么选?
无状态 API 服务用 round-robin 均匀分配即可。WebSocket 等长连接服务用 least_conn,因为它实时监控连接数,能把新请求发给负载最低的服务器,避免某台积压过多长连接。
健康检查的 rise 和 fall 参数怎么设置?
rise 控制连续成功多少次后标记为健康,建议设 2 防止服务器刚启动时反复震荡。fall 控制连续失败多少次后标记为不可用,建议设 3 或 5 容忍瞬时故障。过于敏感的设置会导致误判。
check_status 页面为什么必须加访问控制?
状态页面暴露了后端服务器列表和健康状态,攻击者能看到内部拓扑信息,还能知道哪台服务器当前不可用——这正是攻击的好时机。建议只允许内网 IP 或特定监控服务器访问。

14 分钟阅读 · 发布于: 2026年4月27日 · 修改于: 2026年4月29日

当前属于系列阅读 第 4 / 4 篇

Nginx 实战指南

如果你是从搜索进入这篇文章,建议顺手补上上一篇或继续下一篇,这样更容易把同一主题读完整。

查看系列总览

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

使用 GitHub 账号登录后即可评论