Nginx主动健康检查

一、引言:被动健康检查的"致命盲区"

在绝大多数Nginx配置中,upstream的健康保障依赖于max_failsfail_timeout这两个参数。这是一种被动健康检查(Passive Health Check) 机制:只有当真实用户请求打到某个后端节点并失败时,Nginx才会将其标记为不可用。

这种"用真实流量试错"的模式在生产环境中存在三个致命缺陷:

  • 首请求必损:节点刚恢复或刚上线时,第一批请求必然命中尚未被标记的故障节点,用户体验直接受损;
  • 故障感知滞后:若某节点流量占比低(如权重1/10),可能需要数十秒甚至数分钟才能积累足够的失败次数触发摘除;
  • 恢复探测粗暴fail_timeout到期后,Nginx直接将节点重新放入池中,没有渐进式验证,若节点未完全恢复,新一轮真实请求再次成为"炮灰"。

主动健康检查(Active Health Check) 正是为解决这些问题而生。它由Nginx独立发起周期性探测请求,与业务流量完全隔离,实现:

  • ✅ 故障提前发现,用户请求零损伤;
  • ✅ 新节点上线前预检,通过后才接入流量;
  • ✅ 恢复过程可控,支持慢启动和渐进放量;
  • ✅ 多维度判定,不仅看TCP连通性,还验证HTTP状态码、响应体内容、响应时间等。

本文将从开源与商业版的方案对比出发,深度拆解主动健康检查的配置语义、高级策略和生产级落地模板,帮你构建真正"用户无感"的后端容错体系。


二、方案选型:三条技术路线的全景对比

本文后续内容聚焦OpenResty方案 ,因其是开源生态中最接近Nginx Plus能力的生产级选择,且原理可迁移至其他方案。📌 选型建议

  • K8s环境:优先使用Ingress Controller的原生健康检查,与Pod Readiness Probe联动;
  • 非K8s + 预算充足:Nginx Plus是最优解,功能完整、官方支持;
  • 非K8s + 开源需求:OpenResty + lua-resty-upstream-healthcheck是事实标准;
  • 极简场景/学习:原生被动检查足够,但务必理解其局限。

三、OpenResty主动健康检查核心架构

3.1 工作原理

复制代码
┌─────────────────────────────────────────────────────┐
│                  OpenResty Worker                    │
│                                                      │
│  ┌──────────────┐    ┌───────────────────────────┐  │
│  │ Timer Module │───▶│ Health Check Coroutine     │  │
│  │ (定时触发)    │    │ 1. 遍历upstream节点列表     │  │
│  └──────────────┘    │ 2. 发起HTTP/TCP探测请求     │  │
│                      │ 3. 校验响应(状态码/Body)    │  │
│  ┌──────────────┐    │ 4. 更新共享内存中的健康状态  │  │
│  │ Shared Dict  │◀──▶│                            │  │
│  │ (健康状态存储)│    └───────────────────────────┘  │
│  └──────┬───────┘                                   │
│         │ 读取                                      │
│  ┌──────▼───────┐                                   │
│  │ Balancer     │ ← 业务请求到达时,仅选择健康节点    │
│  │ (负载均衡器)  │                                   │
│  └──────────────┘                                   │
└─────────────────────────────────────────────────────┘

📌 关键设计

  • 健康检查运行在独立协程中,不阻塞业务请求处理;
  • 健康状态存储在shared dict中,跨worker共享,避免重复探测;
  • Balancer阶段只读取状态、不做探测,保证请求处理延迟不受影响。

3.2 核心组件安装

bash 复制代码
# 确保OpenResty已安装
# 安装lua-resty-upstream-healthcheck
luarocks install lua-resty-upstream-healthcheck

# 或使用opm(推荐)
opm get openresty/lua-resty-upstream-healthcheck

四、基础配置:从零搭建主动健康检查

4.1 最小可用配置

复制代码
http {
    # ===== 共享内存:存储健康状态 =====
    lua_shared_dict healthcheck 10m;

    # ===== 初始化健康检查器 =====
    init_worker_by_lua_block {
        local hc = require "resty.upstream.healthcheck"
        local ok, err = hc.spawn_checker({
            shm = "healthcheck",
            upstream = "api_backend",
            type = "http",
            http_req = "GET /health HTTP/1.1\r\nHost: api-backend\r\n\r\n",
            interval = 2000,       -- 每2秒探测一次
            timeout = 1000,        -- 探测超时1秒
            fall = 3,              -- 连续3次失败 → 标记不健康
            rise = 2,              -- 连续2次成功 → 标记健康
            valid_statuses = {200}, -- 仅200视为健康
        })
        if not ok then
            ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
        end
    }

    # ===== Upstream定义 =====
    upstream api_backend {
        server 10.0.1.10:8080;
        server 10.0.1.11:8080;
        server 10.0.1.12:8080;
    }

    server {
        location /api/ {
            proxy_pass http://api_backend;
        }

        # ===== 健康检查状态查看接口 =====
        location = /upstream_health {
            content_by_lua_block {
                local hc = require "resty.upstream.healthcheck"
                local status = hc.get_status("api_backend")
                ngx.say(status)
            }
        }
    }
}

4.2 核心参数详解

参数 类型 默认值 说明 生产建议
shm string 必填 shared dict名称 与lua_shared_dict一致
upstream string 必填 upstream块名称 必须精确匹配
type string "http" 探测协议:http/tcp API用http,DB/TCP服务用tcp
http_req string 必填 原始HTTP请求报文 包含完整Header,以\r\n\r\n结尾
interval number 1000 探测间隔(ms) 2000~5000,过短增加后端负担
timeout number 1000 单次探测超时(ms) ≤interval/2,避免探测堆积
fall number 3 连续失败阈值 2~5,过小误判,过大延迟
rise number 2 连续成功阈值 2~3,防止抖动节点反复上下线
valid_statuses table {200} 健康状态码列表 按需添加204/301等
concurrency number 1 并发探测数 节点多时调大,避免串行延迟

⚠️ 关键注意http_req必须是完整的原始HTTP请求 ,包括方法、路径、协议版本、Host头和空行。缺少任何部分都会导致探测失败。推荐使用string.format动态构造:

Lua 复制代码
http_req = string.format(
    "GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: nginx-healthcheck\r\nConnection: close\r\n\r\n",
    "/health", "api-backend"
)

五、高级策略:超越"通/不通"的精细化治理

5.1 多维度健康判定

Lua 复制代码
-- 自定义校验函数:状态码 + 响应体 + 响应时间三重验证
local function custom_checker(resp_status, resp_body, resp_time)
    -- 条件1:状态码必须200
    if resp_status ~= 200 then return false end
    
    -- 条件2:响应体必须包含"OK"
    if not resp_body or not string.find(resp_body, '"status"%s*:%s*"ok"') then
        return false
    end
    
    -- 条件3:响应时间不超过500ms
    if resp_time > 500 then return false end
    
    return true
end

hc.spawn_checker({
    -- ... 其他参数
    checker = custom_checker,  -- 替代valid_statuses
})

📌 价值:后端返回200但实际处于降级状态(如数据库连接池耗尽、缓存全miss)时,传统状态码检查无法识别。内容+延迟双重校验能捕获这类"假健康"节点。

5.2 差异化探测策略

不同后端服务的健康特征不同,应为每个upstream定制探测参数:

服务类型 interval timeout fall rise 校验重点
核心API 2s 1s 3 2 状态码+响应体+延迟
内部微服务 3s 2s 2 2 状态码即可
数据库代理 5s 3s 3 3 TCP连通+SELECT 1
第三方API 10s 5s 5 3 状态码(宽松)
静态资源源站 5s 2s 2 2 HEAD 200

5.3 与新节点上线联动

Lua 复制代码
-- 新节点加入upstream后,先执行预检再放行流量
local function pre_check_new_node(host, port)
    local hc = require "resty.upstream.healthcheck"
    local ok = hc.single_check("api_backend", host, port, {
        timeout = 2000,
        valid_statuses = {200},
    })
    if ok then
        ngx.log(ngx.INFO, "new node ", host, ":", port, " passed pre-check")
        -- 调用服务发现API注册节点
    else
        ngx.log(ngx.WARN, "new node ", host, ":", port, " failed pre-check, skipping")
    end
end

📌 零停机发布的关键 :新Pod/容器启动后,先通过主动健康检查验证就绪,再注册到upstream。彻底消除"刚上线就被打挂"的经典问题

5.4 慢启动与渐进放量

OpenResty原生不支持slow_start,可通过自定义Balancer实现:

Lua 复制代码
local node_recovery_time = {}  -- shared dict记录节点恢复时间

function balanced_peer(premature, upstream_name)
    local peers = get_healthy_peers(upstream_name)
    local now = ngx.now()
    
    for _, peer in ipairs(peers) do
        local recovery_ts = node_recovery_time[peer.id]
        if recovery_ts then
            local elapsed = now - recovery_ts
            if elapsed < 30 then  -- 30秒慢启动窗口
                -- 按时间比例降低权重
                peer.weight = math.floor(peer.base_weight * (elapsed / 30))
            else
                node_recovery_time[peer.id] = nil  -- 恢复正常
            end
        end
    end
    
    return select_peer_by_weight(peers)
end

📌 价值:节点恢复后立即承受全量流量可能导致二次崩溃(如JIT未预热、连接池为空、缓存冷启动)。慢启动让流量线性增长,给后端充分的"热身"时间。


六、可观测性:健康检查本身的监控

6.1 暴露健康状态API

复制代码
location = /nginx_upstream_status {
    content_by_lua_block {
        local cjson = require "cjson.safe"
        local hc = require "resty.upstream.healthcheck"
        
        local result = {}
        local upstreams = {"api_backend", "auth_backend", "cache_backend"}
        
        for _, name in ipairs(upstreams) do
            result[name] = hc.get_status(name)
        end
        
        ngx.header.content_type = "application/json"
        ngx.say(cjson.encode(result))
    }
}

6.2 Prometheus指标导出

Lua 复制代码
-- 在/content_metrics中输出
local hc = require "resty.upstream.healthcheck"
local status = hc.get_status("api_backend")

-- 解析status字符串,提取各节点状态
for node, state in pairs(parse_status(status)) do
    ngx.say(string.format(
        'nginx_upstream_health{upstream="api_backend",node="%s"} %d',
        node, state == "healthy" and 1 or 0
    ))
end

6.3 必采监控指标

指标 含义 告警阈值
健康节点数 当前可用后端数量 < 总数×50% P1
节点频繁翻转 1小时内健康状态变化次数 >5次 P2
探测成功率 成功探测 / 总探测 <90% P2
平均探测延迟 探测请求P99耗时 >timeout×80% P2
全部节点不健康 持续时长 >30s P0
新节点预检失败率 上线前检查失败占比 >10% P2

七、生产安全检查清单

检查项 状态 说明
shared dict大小充足 按节点数×256B估算,预留2倍余量
探测路径专用且轻量 /health不应查库/调外部服务
timeout < interval/2 防止探测任务堆积
fall ≥ 2, rise ≥ 2 避免网络抖动导致误判
探测请求含Connection: close 避免占用后端长连接
新节点上线前有预检 杜绝"上线即故障"
健康状态API已暴露 供监控和运维排查使用
探测日志独立记录 不与业务日志混合
多upstream差异化配置 核心服务更敏感,边缘服务更宽松
定期演练故障切换 验证健康检查实际生效

八、常见踩坑速查表

现象 根因 解决方案
健康检查始终失败 http_req格式错误 补全HTTP/1.1、Host头、空行
节点健康但请求仍502 Balancer未读取shared dict 确认balancer_by_lua中使用hc API
探测超时频发 timeout过短或后端/health过重 增大timeout或简化健康接口
节点频繁上下线 fall/rise=1 调整为fall=3, rise=2
shared dict报错 内存不足 增大lua_shared_dict容量
新节点上线即被打挂 无预检或无慢启动 添加pre-check + 渐进放量
探测占用大量后端连接 未加Connection: close 修改http_req添加该Header
多worker重复探测 未使用shared dict 确认shm参数正确
健康状态API返回空 upstream名称不匹配 检查spawn_checker中的upstream参数
Reload后健康状态丢失 shared dict未持久化 正常行为,reload后自动重建

九、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
ITyunwei09872 小时前
内存溢出报错,实战处理记录
运维·服务器·企业微信
Elastic 中国社区官方博客2 小时前
你的 AI agent 不需要你的 API 密钥:使用 OAuth 2.1 对 Elasticsearch MCP 服务器进行身份验证
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索
FreeTinker2 小时前
AP AC组网中的远程运维挑战及内网穿透技术应用分析
运维
Huangjin007_4 小时前
【Linux 系统篇(十五)】进程 (三) :进程状态深度详解
linux·运维
RisunJan4 小时前
Linux命令-talk(终端实时对话)
linux·运维·服务器
整点bug4 小时前
AI 运维该不该自动执行命令?
运维·ssh·openai
Kurisu_红莉栖4 小时前
关于docker的使用心得
运维·docker·容器
三言老师5 小时前
Rocky Linux 8.6 整机系统备份与迁移方案文档文档用途
linux·运维·服务器·网络
「PlanA」5 小时前
自动化流水线CSDN自动发布文章
运维·自动化