bigywater.tech
微服务 · 20

被动健康检查与网关熔断:max_fails、fail_timeout 与摘除失败比例高的节点

nginx 的 max_fails 和 fail_timeout 到底怎么摘除节点?什么算失败?偶发失败的节点会被怎么对待?恢复要多久?这篇用 kill 进程和返回 503 两种故障把被动健康检查逐项量出来。

75 对 0
A 回 503 时,proxy_next_upstream 默认与加 http_503,client 看到的 503 数(150 个请求中)
50% → 21%
只有 2% 失败率的节点 A,在 max_fails=3、fail_timeout=5s 下的份额(摘除约 10.5 s / 20 s)
7.4 到 7.9 s
fail_timeout = 10 s 时,3 秒的故障结束后 A 重新成功应答用的时间

什么算失败

实测 · 本机执行输出
  故障                                   | 第 1 到 4 秒 client 看到的 5xx | 其中 nginx 的 connect() failed 行数 | 第 1 到 4 秒各自应答的请求(键:返回的内容或状态码)
  -------------------------------------+--------------------------+--------------------------------+------------------------------
  (a) A 被 kill,连接被拒                    | 0                        | 5                              | {'B': 150}                   
  (b) A 回 503,默认 next_upstream         | 75                       | 见右列                            | {'503': 75, 'B': 75}         
  (c) A 回 503,next_upstream 含 http_503 | 0                        | 见右列                            | {'B': 150}                   

连接被拒算 error;503 默认不算,要加 http_503;不算失败就不会摘除,也不会重试。

偶发失败

实测 · 本机执行输出
  A 的失败率 p | A 的份额 | client 看到的 5xx | A 被摘除的总时长(估计) | 摘除的次数
  ---------+-------+----------------+---------------+------
  0%       | 50%   | 0              | 0.0 s         | 0    
  2%       | 21%   | 0              | 10.5 s        | 2    
  10%      | 6%    | 0              | 16.1 s        | 3    
  30%      | 2%    | 0              | 17.4 s        | 3    
  70%      | 0%    | 0              | 17.5 s        | 1    
  失败率只有 2% 的节点也会被偶尔摘除:nginx 数的是 5 秒内累计 3 次失败,而 100 个/秒、一半流量给 A 时 5 秒里 A 会收到约 250 个请求,2% 就是约 5 个失败;所以只看“次数”的被动检查对高流量的节点非常敏感,对低流量的节点又很迟钝

nginx 数 fail_timeout 内的失败次数,不看比例,高流量的节点即使失败率很低也会被反复摘除。

恢复

实测 · 本机执行输出
  fail_timeout | 3 次的恢复用了(故障起点 1.0 / 1.4 / 1.8 秒) | 故障期间 A 收到的失败请求的时刻(3 次,秒)                                          
  -------------+----------------------------------+-------------------------------------------------------------------
  2 s          | 2.7 s / 2.5 s / 2.3 s            | [1.0, 1.0, 1.1, 3.7] | [1.4, 1.4, 1.5, 3.9] | [1.8, 1.8, 1.9, 4.1]
  5 s          | 2.3 s / 3.1 s / 2.9 s            | [1.0, 1.0, 1.1] | [1.4, 1.4, 1.5] | [1.8, 1.8, 1.9]               
  10 s         | 7.9 s / 7.6 s / 7.4 s            | [1.0, 1.0, 1.1] | [1.4, 1.4, 1.5] | [1.8, 1.8, 1.9]               
  看右列:fail_timeout 较短时,有的运行里故障期间 A 多收到了一个失败请求(摘除到期后放过去试的那个请求,A 还没恢复,试失败就再摘除一个完整的 fail_timeout);有的运行里试探恰好发生在 A 恢复之后,恢复很快。fail_timeout 越长,故障结束后越晚才重新用上 A,而且同一个 fail_timeout 下恢复用的时间随故障与摘除时刻的相位而变

摘除到期后放一个请求去试,A 仍失败就再摘除一个 fail_timeout;fail_timeout 越长恢复越晚。

分步演示

被动健康检查与网关熔断
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6(单 worker 进程);后端 B 与大部分 A 是本进程里的 Python 服务;第一节 (a) 的 A 是一个独立的 Python 进程(vtmp/v20/backend.py),用 kill() 杀掉并在同一端口重新拉起。我最初用 Svc.stop() 来“关掉”A,结果 nginx 仍然能从 A 得到应答——nginx 1.31.6 默认保持到上游的长连接,而 Python 的 http.server 在监听 socket 关闭之后继续服务已经建立的连接;所以改成杀进程。第三节每个 fail_timeout 只跑了 3 次
    • 没有验证:nginx 源码里 max_fails / fail_timeout / checked 的细节(上面是从日志观察到的行为);多个 worker 进程之间 upstream 的状态是否共享(需要 zone);商业版 nginx plus 的主动检查与 slow_start;上游长连接对“连接被拒”出现时间的影响(见第 23 篇)
    京ICP备2026067502号-1京公网安备11010502063487号