一、Nginx 到底有没有「健康检查」?先把概念掰正
几乎每个讲 Nginx 的教程都会提到「健康检查」,但绝大多数教程把两件完全不同的事情混为一谈,导致很多人配了半天发现行为跟预期不符。先把这两个概念分开:
- 被动健康检查(Passive Health Check):开源 Nginx 原生就有。原理是「等真实请求失败」,失败次数达到阈值就把这台后端摘掉,标记为不可用,等一段时间后再放进去试。它不需要额外模块,靠
max_fails和fail_timeout两个参数控制。 - 主动健康检查(Active Health Check):Nginx 主动周期性地向后端发探针请求,不看真实流量。这是 Nginx Plus 的商业功能,开源版没有。要在开源版实现,得用
nginx_upstream_check_module(淘宝开源的那个)重新编译,或者用 OpenResty + Lua 自己写。
所以当你看到某篇文章说「在 upstream 里写 check interval=3000 rise=2 fall=3」,那一定是编译了第三方模块的版本。如果你用的是普通 Nginx,加了这几行只会报 unknown directive "check",配置根本加载不了。本文只讲开源 Nginx 原生可用的被动机制,因为这是 99% 个人站长实际面对的东西。
二、`max_fails` 的工作机制比你想的复杂
先看一段最常见的配置:
upstream backend {
server 127.0.0.1:9000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:9001 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}字面理解是「失败 3 次后摘除 30 秒」。但实际规则有几层,光看文档很容易误解:
2.1 `max_fails` 统计的是「时间窗口内的失败次数」
它不是累计计数。fail_timeout 同时承担了两个职责:
- 它是统计窗口的长度——在这段时间内失败次数达到
max_fails就摘除; - 它同时是摘除的持续时长——被摘除后,经过这段时间再重新放回候选列表。
这两件事共用一个参数,是 Nginx 设计上一个相当反直觉的地方。也就是说 fail_timeout=30s 的含义是「30 秒内累计失败 3 次就摘掉,摘掉 30 秒」。
更绕的是:一旦某台后端在窗口内失败达到阈值被摘除,这个计数器会重置。所以它不是「30 秒后自动清零重来」,而是「达到阈值就摘除并清零」。理解这一点,才能解释为什么有时候后端恢复了却还要再等一会儿才被重新使用。
2.2 什么算「一次失败」?
这是最关键的问题,答案取决于 proxy_next_upstream。默认值在 Nginx 1.9.13 之后是:
proxy_next_upstream error timeout;也就是说默认只有两种情况算失败:error(连接后端出错,比如连接被拒绝、连接被重置)和 timeout(建立了连接但读取超时)。后端返回 500、502、503、504 这些 HTTP 状态码,默认是不算失败的——Nginx 会直接把 500 返回给客户端,不会重试,也不会累加失败计数。
这是个巨大的坑。很多人配置了 max_fails=3,然后后端 PHP 一直报 500,却发现 Nginx 从来没把后端摘掉。原因就在这里。要让它把 5xx 也算作失败,必须显式配置:
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;三个参数配合使用:proxy_next_upstream 定义「哪些情况触发重试」,_tries 限制「最多试几台(含首次)」,_timeout 限制「整个重试过程最长耗时」。不加后面两个,重试可能把总响应时间拖到不可接受的程度。
三、重试的黑暗面:不是所有请求都该重试
这是最容易被忽略、后果最严重的一节。
想象一个支付回调接口。请求已经到达后端 A,A 处理完了业务逻辑、扣了款、写了数据库,然后在返回响应时连接断了。此时 Nginx 认为这是一次 error,按配置把请求转发给后端 B。后端 B 收到同一个回调,又扣了一次款。
这不是理论风险,这是真实事故的常见形态。所以 Nginx 从设计上就做了保护:只要请求已经被发送给后端(即已经开始发送请求体),就不会重试。 更准确地说,Nginx 的重试条件是:请求还没有向后端发送任何字节(包括请求头和请求体)时,才允许换一台后端。
具体到实际配置,有一个参数能显著提升安全性:
# 如果没有开启,Nginx 会把请求体缓冲起来再发,重试机会更多
proxy_request_buffering on;
# 关闭后请求体边收边转发,一旦开始发送就无法重试(更安全)
proxy_request_buffering off;很多人为了性能把 proxy_request_buffering 关掉,顺带得到了「上传类请求不会被重试」这个副作用,反而更安全。但这也意味着大文件上传从「先缓冲到磁盘再转发」变成了直通流式转发,各有取舍。
更稳妥的做法是按 location 分开配置,把危险接口的重试彻底关掉:
# 普通页面:允许重试,容忍偶发失败
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
# 回调/支付/写操作:绝不重试
location /api/callback/ {
proxy_pass http://backend;
proxy_next_upstream off;
}proxy_next_upstream off 是 Nginx 1.9.13 之后支持的值,一行彻底关掉重试。凡是涉及状态变更、幂等性无法保证的接口,都应该显式加上这一行。 默认配置对它们来说是危险的。
四、`fail_timeout` 设太长还是太短?真实取舍
这两参数没有放之四海皆准的值,取决于你的后端数量和恢复特征。分三种情况讨论。
4.1 单台后端的场景(个人站长最常见)
如果你只有一台后端,那 max_fails 机制其实毫无意义。摘除一台不存在的「其他后端」,请求还是得打回唯一那台。此时更好的思路是:
upstream backend {
# 单机场景,直接设 0 表示不参与被动摘除计数
server 127.0.0.1:9000 max_fails=0;
}max_fails=0 会禁用该后端的被动健康检查。配置成 0 的好处是避免 Nginx 在唯一的后端上来回摘除又放回,让行为更可预测。真正需要做的是让上游更快失败,用超时参数控制:
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;4.2 多台后端的通用建议值
以 3 台后端、日常 QPS 几十的个人站为例,一组经验值:
upstream backend {
server 10.0.0.11:8080 max_fails=2 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=2 fail_timeout=10s;
server 10.0.0.13:8080 max_fails=2 fail_timeout=10s;
keepalive 32;
}为什么 fail_timeout=10s 而不是默认的 10s(默认确实是 10s)?其实默认值就是 10,很多人以为是 30。Nginx 官方默认是 max_fails=1 fail_timeout=10s——也就是说默认情况下失败一次就摘 10 秒,这个默认值相当激进,容易被瞬时抖动误伤。所以更常见的调优方向是调大 max_fails(2~3)来抵抗抖动,而不是改 fail_timeout。
4.3 用 `keepalive` 减少失败本身
注意上面配置里的 keepalive 32;。这一行能显著降低「连接错误」的发生率,因为 Nginx 会复用与后端的连接,省掉每次重新建连的开销和失败概率。但光加这一行是不够的,必须配套改 HTTP 版本和 Connection 头,否则 keepalive 完全无效:
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
upstream backend {
server 10.0.0.11:8080;
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 1000;
}proxy_set_header Connection "" 清空 Connection 头是最关键的一步。默认 Nginx 会发 Connection: close,后端收到就直接关连接,连接池根本建立不起来。这是「配了 keepalive 却没效果」的第一大原因。
五、如何验证被动健康检查真的生效了
不能只看配置,必须实测。下面是可复现的验证方法。
5.1 用负载均衡状态模拟一台后端挂掉
假设本机起了两个测试后端,故意停掉其中一个:
# 启动两个最简单的后端做测试
python3 -m http.server 9000 &
python3 -m http.server 9001 &
# 观察 Nginx 错误日志(关键证据都在这里)
tail -f /var/log/nginx/error.log然后杀掉 9001,用循环请求打 Nginx:
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1/
done; echo你会观察到:前几次请求报 502,之后 Nginx 不再尝试那台挂掉的后端,开始连续返回 200(全部由 9000 处理)。在 error.log 里能看到类似这样的记录:
connect() failed (111: Connection refused) while connecting to upstream,
client: 127.0.0.1, server: _, request: "GET / HTTP/1.1",
upstream: "http://127.0.0.1:9001/", host: "127.0.0.1"日志里 upstream 字段会明确写出是哪台后端失败了,这是定位问题最直接的线索。等到 fail_timeout 过期后,Nginx 会再试一次 9001,日志里会重新出现它的报错——这就证明「摘除 → 恢复」的循环在工作。
5.2 用响应头确认请求打到了哪台后端
在多后端环境里,最有效的调试手段是让后端把身份信息回传。以 PHP-FPM 为例,加一个自定义头:
// 后端 PHP 页面里加
header('X-Backend-Node: ' . gethostname());或者直接在 Nginx 里用 add_header 暴露上游地址(仅限调试,生产建议关掉,避免信息泄露):
location / {
proxy_pass http://backend;
add_header X-Upstream-Addr $upstream_addr always;
add_header X-Upstream-Status $upstream_status always;
add_header X-Upstream-Time $upstream_response_time always;
}这三个变量信息量极大:$upstream_addr 显示实际处理请求的后端地址(重试时会是一个逗号分隔的序列),$upstream_status 显示每个后端的返回码(对应上面的序列),$upstream_response_time 显示每个后端的耗时。
举个例子,如果看到:
X-Upstream-Addr: 10.0.0.12:8080, 10.0.0.13:8080
X-Upstream-Status: 502, 200
X-Upstream-Time: 0.003, 0.087一眼就能读出:12 号后端快速返回 502(这属于「连上了但谈崩」,很可能是后端应用自身错误),Nginx 按 proxy_next_upstream http_502 换到 13 号,13 号正常返回 200。如果没有配置 http_502 到 proxy_next_upstream,你只会看到单独一个 10.0.0.12:8080 和状态 502,客户端直接收到错误。
六、把健康检查接进监控
被动健康检查是「事后补救」,真正的可靠性来自能提前知道后端不健康。最小成本的方案是加一个本机探针 + 告警:
#!/bin/bash
# /root/upstream_probe.sh —— 探测每个后端,异常则记录
NODES="127.0.0.1:9000 127.0.0.1:9001"
for node in $NODES; do
code=$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://$node/health" 2>/dev/null)
if [ "$code" != "200" ]; then
echo "[$(date '+%F %T')] 后端 $node 异常,HTTP=$code" >> /var/log/upstream_probe.log
fi
done配合 crontab 每分钟跑一次,就能在 Nginx 开始摘除之前发现苗头。-m 3 这个超时参数必须加,否则后端 hang 住时脚本自己也会挂住。
七、一张速查表
max_fails=1 fail_timeout=10s→ Nginx 默认值,失败一次就摘 10 秒,偏激进,建议max_fails=2~3。max_fails=0→ 禁用该后端的被动摘除,单机场景推荐。proxy_next_upstream默认只有error timeout→ 5xx 不会触发重试也不计数,要显式加上http_500 http_502 http_503 http_504。proxy_next_upstream off→ 回调/支付等写接口必须加,防重复提交。- 请求体一旦开始发给后端就不会重试 →
proxy_request_buffering off更安全。 keepalive必须配proxy_http_version 1.1+proxy_set_header Connection "",否则无效。- 调试看
$upstream_addr/$upstream_status/$upstream_response_time三兄弟,重试链一览无余。
回过头看,Nginx 的「健康检查」之所以让人困惑,根本原因是开源版只有被动机制,而被动机制的语义又由重试策略决定——健康检查的判定标准其实是 proxy_next_upstream,不是 max_fails。把这两个参数的关系想清楚,配置里的绝大多数困惑就自动解开了。对于个人站长来说,最值得马上做的一件事,是检查一下你的回调接口有没有 proxy_next_upstream off;如果没加,那是一个随时可能引爆的隐患。