切换语言
切换主题

Nginx 动态 upstream:Lua 实现实时服务发现

凌晨3点,生产环境告警。

Docker容器重启了。IP变了。你的nginx.conf里还写着旧地址。

你得爬起来,手动改配置,执行nginx -s reload。线上QPS抖了一下,监控曲线掉了个坑。运气好的话,几秒钟恢复。运气不好,用户投诉电话打进来。

说实话,这种事我干过不止一次。每次都在想:有没有办法让Nginx自己发现后端服务?像Consul那样,后端IP变了自动更新,不用我半夜爬起来改配置?

其实OpenResty早就能做到。它的Lua脚本可以在运行时修改upstream配置,完全不用reload。Cloudflare就这么干的,他们的CDN边缘节点全靠这套机制动态调度流量。

这篇文章讲怎么用三层架构(ngx.balancer + lua-resty-balancer + 健康检查)实现动态upstream,对比两种主流健康检查库的优劣,给出Consul、Nacos、etcd三种服务发现集成方案的完整代码。读完你就能把这套东西部署到生产环境。

为什么需要动态 upstream

Nginx的upstream配置是静态的。你写在nginx.conf里的server地址,启动时加载一次,之后想改?只能reload。

容器化环境里这事儿很烦。Docker容器重启,IP地址变了。K8s的Pod重新调度,IP也变了。你总不能每次都手动改nginx.conf吧?猪八戒网的技术团队踩过这个坑,他们从手工配置进化到模板渲染,最后不得不上Consul动态服务发现,就是被这个问题逼的。

有人说用NGINX Plus啊,商业版支持动态upstream。没错,但一年几万美元的授权费,代码还不开源。出了问题只能等官方修。对大多数团队来说,这不是个好选择。

OpenResty给了另一条路。它在Nginx基础上嵌入了LuaJIT虚拟机,你可以用Lua脚本在运行时修改upstream配置。完全不用reload,请求处理过程中就能动态切换后端服务器。

这套机制的杀手特性是balancer_by_lua_block。它在Nginx选择upstream服务器的阶段介入,你用Lua代码决定这次请求转发给哪个后端。后端IP列表可以存在共享内存里、Redis里、或者Consul里。后端挂了,Lua代码自动摘除。新服务上线,Lua代码自动发现。

适用场景挺多的

  • K8s入口网关:Pod IP频繁变化,Nginx作为Ingress需要动态感知
  • 微服务灰度发布:新老版本共存,根据请求头、Cookie动态路由
  • 故障自动摘除:后端服务响应慢或挂掉,Nginx主动探测并移出流量池
  • 跨机房调度:根据用户地理位置或延迟,动态选择最近的数据中心

Cloudflare的CDN边缘节点就靠这套机制。全球几百个节点,每秒处理千万级请求,全靠OpenResty动态控制流量调度。他们开源了部分实现,你可以在GitHub上看到相关代码。

三层架构与核心组件

OpenResty的动态upstream不是单点突破,而是三层协作:

┌─────────────────────────────────────────┐
│  第三层:健康检查                         │
│  lua-resty-healthcheck                   │
│  - 主动探测后端存活状态                   │
│  - 更新共享内存中的upstream状态           │
└──────────────┬──────────────────────────┘
               │ 状态同步
┌──────────────▼──────────────────────────┐
│  第二层:负载均衡算法                     │
│  lua-resty-balancer                      │
│  - resty.roundrobin(轮询)              │
│  - resty.chash(一致性哈希)             │
│  - 从共享内存读取健康的后端列表           │
└──────────────┬──────────────────────────┘
               │ 选择结果
┌──────────────▼──────────────────────────┐
│  第一层:底层API                          │
│  ngx.balancer                            │
│  - set_current_peer(host, port)         │
│  - get_last_failure()                   │
│  - set_more_tries(n)                    │
│  - 在balancer_by_lua阶段调用             │
└─────────────────────────────────────────┘

ngx.balancer:底层API

这一层最贴近Nginx内核。ngx.balancer模块提供了三个核心API:

  • set_current_peer(host, port):指定本次请求转发到哪个后端
  • get_last_failure():获取上一次尝试的失败信息(用于重试逻辑)
  • set_more_tries(n):设置额外重试次数

这些API必须在balancer_by_lua_block里调用。这个阶段是Nginx选择upstream服务器的时机,你的Lua代码介入后,就能完全接管路由决策。

一个最小示例

upstream backend {
    server 0.0.0.1;  # 占位地址,必须有一个server指令
    balancer_by_lua_block {
        local balancer = require "ngx.balancer"

        -- 动态选择后端
        local host = "192.168.1.10"
        local port = 8080

        local ok, err = balancer.set_current_peer(host, port)
        if not ok then
            ngx.log(ngx.ERR, "failed to set peer: ", err)
            return ngx.exit(500)
        end
    }
}

注意:server 0.0.0.1是个占位符。Nginx要求upstream块至少有一个server指令,但我们用Lua动态选择真正的后端,所以这个地址根本不会被访问。

lua-resty-balancer:负载均衡算法

直接用ngx.balancer太原始了。你得自己写轮询、自己写哈希、自己维护后端列表。lua-resty-balancer封装了这些算法,开箱即用。

它提供两种负载均衡器:

  • resty.roundrobin:轮询,依次选择后端服务器
  • resty.chash:一致性哈希,同一客户端请求总是路由到同一后端(适合会话保持)

使用前,先在init_worker_by_lua_block里初始化:

init_worker_by_lua_block {
    local roundrobin = require "resty.roundrobin"
    local chash = require "resty.chash"

    -- 后端服务器列表(可从Consul/Nacos动态获取)
    local servers = {
        { "192.168.1.10", 8080, weight = 10 },
        { "192.168.1.11", 8080, weight = 5 },
        { "192.168.1.12", 8080, weight = 3 },
    }

    -- 创建轮询负载均衡器
    local rr_upstream = roundrobin:new(servers)

    -- 存入共享内存,balancer阶段读取
    local shared_dict = ngx.shared.upstreams
    shared_dict:set("backend_rr", rr_upstream)
}

然后在balancer_by_lua_block里使用:

upstream backend {
    server 0.0.0.1;
    balancer_by_lua_block {
        local shared_dict = ngx.shared.upstreams
        local rr_upstream = shared_dict:get("backend_rr")

        -- 选择下一个服务器
        local host, port = rr_upstream:select()

        local balancer = require "ngx.balancer"
        balancer.set_current_peer(host, port)
    }
}

运行阶段详解

Nginx处理请求有一套严格的阶段顺序。理解这些阶段,才能正确放置Lua代码:

1. init_by_lua_block     → Nginx master进程启动时
2. init_worker_by_lua    → 每个worker进程启动时
3. ssl_certificate_by_lua → SSL握手阶段
4. set_by_lua            → 处理变量赋值
5. rewrite_by_lua        → URL重写阶段
6. access_by_lua         → 访问控制阶段
7. balancer_by_lua       → 选择upstream服务器(核心)
8. header_filter_by_lua  → 处理响应头
9. body_filter_by_lua    → 处理响应体
10. log_by_lua           → 日志记录阶段

balancer_by_lua_block在第7阶段。这时候请求还没转发,你可以决定发给谁。如果是重试场景(后端返回错误),get_last_failure()能告诉你上次为什么失败,你据此选择另一个后端。

健康检查实现对比

动态upstream的最后一层是健康检查。后端服务器可能随时挂掉,你得主动探测,而不是等请求失败才被动发现。

OpenResty社区有两种主流方案:官方的lua-resty-upstream-healthcheck和更完善的lua-resty-healthcheck。我踩过坑后,强烈推荐后者。

lua-resty-upstream-healthcheck:官方方案

这是OpenResty官方维护的健康检查库。它提供主动检查能力,后台定时发送HTTP请求探测后端状态。

配置示例:

-- 在nginx.conf的http块配置共享内存
lua_shared_dict healthcheck 1m;

-- 在init_worker_by_lua_block启动健康检查
init_worker_by_lua_block {
    local hc = require "resty.upstream.healthcheck"

    local ok, err = hc.spawn_checker{
        shm = "healthcheck",             -- 共享内存名称
        upstream = "backend",            -- upstream名称
        type = "http",                   -- 检查类型(http或tcp)

        -- 健康检查请求内容
        http_req = "GET /health HTTP/1.0\r\nHost: backend\r\n\r\n",

        interval = 2000,   -- 探测间隔:2000毫秒(2秒)
        timeout = 1000,    -- 单次探测超时:1秒
        fall = 3,          -- 连续3次失败标记为down
        rise = 2,          -- 连续2次成功标记为up

        valid_statuses = { 200, 302 },   -- 认为成功的HTTP状态码
    }

    if not ok then
        ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
    end
}

启动后,库会每隔2秒向每个后端服务器的/health路径发请求。如果连续3次失败,就把这个服务器标记为down,后续负载均衡就不会选它。等它恢复后,连续2次成功才重新标记为up。

它的状态数据存在你配置的共享内存里(lua_shared_dict healthcheck)。你在balancer_by_lua_block阶段可以读取这些状态,决定是否选择某个后端。

lua-resty-healthcheck:生产级推荐

官方库能用,但功能不够完善。它只支持主动检查,不支持被动检查(根据实际请求失败情况动态调整)。而且在某些边缘场景下有bug。

lua-resty-healthcheck是社区增强版,功能更全:

  • 主动检查:定时发送HTTP/TCP探测请求
  • 被动检查:根据balancer_by_lua_block的失败信息自动调整状态
  • 更灵活的配置:支持自定义检查逻辑、回调函数
  • 更稳定:经过Apache APISIX等项目大规模生产验证

配置示例

-- 同样需要共享内存
lua_shared_dict healthcheck 2m;

init_worker_by_lua_block {
    local healthcheck = require "resty.healthcheck"

    local checker = healthcheck.new({
        name = "backend_checker",
        shm_name = "healthcheck",

        checks = {
            active = {
                type = "http",
                http_path = "/health",
                healthy = {
                    interval = 2,     -- 2秒探测一次
                    successes = 2,    -- 连续2次成功标记up
                },
                unhealthy = {
                    interval = 1,     -- 1秒探测一次(down后探测更频繁)
                    tcp_failures = 1, -- TCP连接失败立即标记down
                    http_failures = 3, -- HTTP失败3次标记down
                },
            },
            passive = {
                healthy = {
                    successes = 3,    -- 正常请求成功3次自动标记up
                },
                unhealthy = {
                    tcp_failures = 2, -- TCP失败2次自动标记down
                    http_failures = 3, -- HTTP失败3次自动标记down
                },
            },
        },
    })

    -- 添加要检查的后端服务器
    checker:add_target("192.168.1.10", 8080, "backend", true)
    checker:add_target("192.168.1.11", 8080, "backend", true)
    checker:add_target("192.168.1.12", 8080, "backend", true)
}

被动检查的威力在于:即使你主动探测没发现问题,但如果大量真实请求失败,健康检查器也能自动把后端标记为down。这能更快响应突发故障。

两种方案对比

对比项lua-resty-upstream-healthchecklua-resty-healthcheck
维护者OpenResty官方社区(经过APISIX验证)
主动检查支持支持
被动检查不支持支持
配置灵活度较低高(回调、自定义逻辑)
生产稳定性一般(有已知bug)高(大规模验证)
文档质量官方文档详细,有示例
推荐度入门可用生产推荐

说实话,我开始用的是官方库。后来在生产环境遇到一个问题:某个后端服务返回200状态码但响应体是错误信息(服务内部故障),官方库无法识别这种”假健康”。换成lua-resty-healthcheck后,我自定义了检查逻辑,解析响应体内容判断是否真正健康,问题解决。

我的建议:直接用lua-resty-healthcheck。它的代码也更清晰,Apache APISIX就是基于它实现的,你可以参考APISIX的健康检查配置。

服务发现集成实战

健康检查解决了”后端挂了怎么办”的问题。但还有个前置问题:后端列表从哪来?

容器化环境里,后端服务IP频繁变化。你不能硬编码在配置里。得有个服务注册中心,告诉Nginx哪些服务正在运行。

常见的方案有三种:Consul、Nacos、etcd。我分别给出集成代码。

Consul集成:最成熟的方案

Consul是HashiCorp的服务发现工具,广泛用于微服务架构。它提供服务注册、健康检查、KV存储等功能。

Nginx集成的思路:后台定时从Consul API拉取服务列表,更新到共享内存。

完整实现代码

-- 配置共享内存(存储服务列表)
lua_shared_dict upstream_servers 5m;

-- 定时从Consul拉取服务列表
init_worker_by_lua_block {
    local timer = require "ngx.timer"
    local http = require "resty.http"
    local cjson = require "cjson.safe"

    -- Consul服务发现API地址
    local consul_host = "consul.service.consul"
    local consul_port = 8500
    local service_name = "backend"

    -- 更新服务列表的函数
    local function update_upstream(premature)
        if premature then return end

        local httpc = http.new()
        httpc:set_timeout(1000)  -- 1秒超时

        -- 调用Consul Catalog API获取服务列表
        local res, err = httpc:request_uri(
            "http://" .. consul_host .. ":" .. consul_port ..
            "/v1/catalog/service/" .. service_name,
            {
                method = "GET",
                headers = { Accept = "application/json" }
            }
        )

        if not res then
            ngx.log(ngx.ERR, "failed to query consul: ", err)
            return
        end

        -- 解析Consul返回的服务列表
        local services = cjson.decode(res.body)
        if not services or #services == 0 then
            ngx.log(ngx.WARN, "no backend services found in consul")
            return
        end

        -- 构建后端服务器列表
        local servers = {}
        for _, svc in ipairs(services) do
            -- Consul返回的服务信息包含Address和ServicePort
            -- 只有健康的服务才会被返回(Consul自己的健康检查)
            servers[#servers + 1] = {
                svc.ServiceAddress or svc.Address,
                svc.ServicePort,
                weight = 10  -- 默认权重
            }
        end

        -- 存入共享内存
        local shared_dict = ngx.shared.upstream_servers
        local packed = cjson.encode(servers)
        shared_dict:set("backend_servers", packed)

        ngx.log(ngx.INFO, "updated upstream servers: ", #servers, " instances")
    end

    -- 每5秒更新一次服务列表
    timer.every(5, update_upstream)

    -- 启动时立即执行一次
    update_upstream(false)
}

balancer_by_lua_block里读取这些数据:

upstream backend {
    server 0.0.0.1;
    balancer_by_lua_block {
        local cjson = require "cjson.safe"
        local roundrobin = require "resty.roundrobin"
        local shared_dict = ngx.shared.upstream_servers

        -- 从共享内存读取服务列表
        local packed = shared_dict:get("backend_servers")
        if not packed then
            ngx.log(ngx.ERR, "no upstream servers available")
            return ngx.exit(503)
        end

        local servers = cjson.decode(packed)

        -- 创建轮询负载均衡器
        local rr = roundrobin:new(servers)
        local host, port = rr:select()

        -- 设置后端
        local balancer = require "ngx.balancer"
        local ok, err = balancer.set_current_peer(host, port)
        if not ok then
            ngx.log(ngx.ERR, "failed to set peer: ", err)
            return ngx.exit(500)
        end
    }
}

这套方案有个优点:Consul自带健康检查。你在服务注册时可以配置HTTP健康检查路径,Consul会自动探测。查询Catalog API时,只有健康的服务才会返回。Nginx拿到的已经是经过筛选的列表。

Nacos集成:国内常用方案

Nacos是阿里巴巴开源的服务发现和配置管理平台,在国内微服务社区很流行。Spring Cloud Alibaba默认就用Nacos。

Nacos的服务发现API和Consul类似,但格式略有不同。

集成代码

lua_shared_dict upstream_servers 5m;

init_worker_by_lua_block {
    local timer = require "ngx.timer"
    local http = require "resty.http"
    local cjson = require "cjson.safe"

    -- Nacos配置
    local nacos_host = "nacos.service.nacos"
    local nacos_port = 8848
    local namespace_id = "public"  -- Nacos命名空间
    local service_name = "backend-service"
    local group_name = "DEFAULT_GROUP"

    local function update_from_nacos(premature)
        if premature then return end

        local httpc = http.new()
        httpc:set_timeout(2000)

        -- Nacos服务发现API
        local url = "http://" .. nacos_host .. ":" .. nacos_port ..
                    "/nacos/v1/ns/instance/list?serviceName=" .. service_name ..
                    "&groupName=" .. group_name ..
                    "&namespaceId=" .. namespace_id

        local res, err = httpc:request_uri(url, { method = "GET" })

        if not res then
            ngx.log(ngx.ERR, "failed to query nacos: ", err)
            return
        end

        local data = cjson.decode(res.body)
        if not data or not data.hosts then
            ngx.log(ngx.WARN, "no instances found in nacos")
            return
        end

        -- Nacos返回的hosts字段包含服务实例列表
        local servers = {}
        for _, instance in ipairs(data.hosts) do
            -- 只有healthy=true的实例才应该使用
            if instance.healthy then
                servers[#servers + 1] = {
                    instance.ip,
                    instance.port,
                    weight = instance.weight or 10
                }
            end
        end

        local shared_dict = ngx.shared.upstream_servers
        shared_dict:set("backend_servers", cjson.encode(servers))

        ngx.log(ngx.INFO, "updated from nacos: ", #servers, " instances")
    end

    timer.every(5, update_from_nacos)
    update_from_nacos(false)
}

Nacos有个特色:支持服务权重动态调整。你在Nacos控制台修改某个实例的权重,Nginx下次拉取时就能感知,流量分配比例会跟着调整。这很适合灰度发布场景——你想让新版本服务先承担少量流量,逐步放量。

etcd集成:轻量级方案

etcd是CoreOS开发的分布式KV存储,Kubernetes就用它存储集群状态。如果你的后端服务注册信息存在etcd里,可以直接从etcd读取。

集成代码

lua_shared_dict upstream_servers 5m;

init_worker_by_lua_block {
    local timer = require "ngx.timer"
    local http = require "resty.http"
    local cjson = require "cjson.safe"

    -- etcd配置
    local etcd_host = "etcd.service.etcd"
    local etcd_port = 2379
    -- 服务注册信息的key(自定义格式)
    local service_key = "/services/backend"

    local function update_from_etcd(premature)
        if premature then return end

        local httpc = http.new()
        httpc:set_timeout(1000)

        -- etcd V3 API(需要POST请求)
        local url = "http://" .. etcd_host .. ":" .. etcd_port .. "/v3/kv/range"
        local body = cjson.encode({ key = service_key, range_end = service_key .. "/" })

        local res, err = httpc:request_uri(url, {
            method = "POST",
            body = body,
            headers = { ["Content-Type"] = "application/json" }
        })

        if not res then
            ngx.log(ngx.ERR, "failed to query etcd: ", err)
            return
        end

        local data = cjson.decode(res.body)
        if not data or not data.kvs then
            ngx.log(ngx.WARN, "no services found in etcd")
            return
        end

        -- 解析etcd返回的键值对
        local servers = {}
        for _, kv in ipairs(data.kvs) do
            -- kv.value是服务实例信息(base64编码)
            local value = ngx.decode_base64(kv.value)
            local instance = cjson.decode(value)

            if instance and instance.healthy then
                servers[#servers + 1] = {
                    instance.host,
                    instance.port,
                    weight = instance.weight or 10
                }
            end
        end

        local shared_dict = ngx.shared.upstream_servers
        shared_dict:set("backend_servers", cjson.encode(servers))
    end

    timer.every(5, update_from_etcd)
    update_from_etcd(false)
}

etcd的优势是简单轻量。但它不像Consul或Nacos那样有完整的服务发现生态,你需要自己设计服务注册机制。如果你的团队已经在用Kubernetes,etcd天然集成,这是个不错的选择。

三种方案对比

对比项ConsulNacosetcd
原生健康检查支持(HTTP/TCP)支持不支持(需自建)
权重动态调整支持支持(可视化)需自行实现
Spring Cloud集成支持默认集成需额外配置
控制台有Web UI有Web UI(更完善)无(需第三方)
配置管理支持(KV存储)支持(更强大)支持
国内社区活跃度中等高(K8s生态)
适用场景通用微服务Spring Cloud AlibabaK8s环境

我的选择:如果用Spring Cloud,直接Nacos。如果用K8s,etcd方便。如果想要独立完整的服务发现平台,Consul最成熟。

实战场景与性能优化

三层架构搭好了,服务发现也集成了。接下来看几个典型场景的实际应用。

场景1:Kubernetes入口网关

K8s的Pod生命周期很短。扩容、缩容、升级都会导致Pod重新创建,IP地址跟着变。用静态upstream根本没法管理。

OpenResty可以动态感知Pod变化。思路是:

  1. init_worker_by_lua_block里启动定时器,每隔5秒查询K8s API或CoreDNS
  2. 解析服务对应的Pod IP列表
  3. 更新到共享内存
  4. balancer_by_lua_block根据Pod列表负载均衡

K8s API调用示例

local function watch_k8s_services(premature)
    local httpc = http.new()

    -- K8s API:获取Service的Endpoints(即Pod IP列表)
    local url = "https://kubernetes.default/api/v1/namespaces/default/endpoints/backend-service"

    -- K8s API需要认证,从ServiceAccount token文件读取
    local token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
    local token = read_file(token_file)  -- 自定义函数读取文件

    local res, err = httpc:request_uri(url, {
        headers = {
            Authorization = "Bearer " .. token
        }
    })

    if res then
        local endpoints = cjson.decode(res.body)
        -- endpoints.subsets包含Pod地址和端口信息
        -- 解析并存入共享内存...
    end
end

timer.every(5, watch_k8s_services)

当然,实际生产环境你可以用K8s的Ingress Controller,像NGINX Ingress Controller或Traefik都封装了这套逻辑。但如果你的需求特殊(比如自定义路由规则、灰度策略),自己写OpenResty更灵活。

场景2:灰度发布

假设你要发布新版本服务。老版本处理90%流量,新版本处理10%。如果新版本稳定运行一周,逐步提升到50%、100%。

用OpenResty可以实现”请求头路由 + 动态权重”:

upstream backend {
    server 0.0.0.1;
    balancer_by_lua_block {
        local cjson = require "cjson.safe"
        local chash = require "resty.chash"
        local shared_dict = ngx.shared.upstream_servers

        -- 从共享内存读取新老版本服务列表
        local old_servers = cjson.decode(shared_dict:get("old_version"))
        local new_servers = cjson.decode(shared_dict:get("new_version"))

        -- 灰度策略:根据请求头决定路由
        local version_header = ngx.req.get_headers()["X-Version"]

        if version_header == "new" then
            -- 强制路由到新版本(测试人员用)
            local host, port = select_random(new_servers)
            balancer.set_current_peer(host, port)
        else
            -- 根据权重随机选择
            -- 90%概率选老版本,10%选新版本
            local rand = math.random()
            if rand < 0.1 then
                local host, port = select_random(new_servers)
                balancer.set_current_peer(host, port)
            else
                local host, port = select_random(old_servers)
                balancer.set_current_peer(host, port)
            end
        end
    }
}

权重比例可以存在共享内存或Redis里,运维人员通过管理接口动态调整。比如提供一个HTTP API:POST /admin/traffic-weight { "old": 90, "new": 10 },OpenResty接收后更新权重配置。

场景3:故障自动摘除

后端服务突然挂了。你希望Nginx快速感知,不再往故障服务器转发请求。

这依赖健康检查模块。lua-resty-healthcheck会在后台持续探测。一旦发现连续3次失败,就把服务器标记为down。

balancer_by_lua_block里,你先检查健康状态:

balancer_by_lua_block {
    local checker = ngx.shared.healthcheck
    local servers = get_all_servers()  -- 从服务发现获取

    -- 过滤掉不健康的服务器
    local healthy_servers = {}
    for _, srv in ipairs(servers) do
        local key = srv[1] .. ":" .. srv[2]
        if checker:get(key) == "up" then  -- 查询健康状态
            healthy_servers[#healthy_servers + 1] = srv
        end
    end

    if #healthy_servers == 0 then
        return ngx.exit(503)  -- 所有后端都挂了
    end

    -- 从健康列表中选择
    local rr = roundrobin:new(healthy_servers)
    local host, port = rr:select()
    balancer.set_current_peer(host, port)
}

重试逻辑也很重要。如果请求转发后返回错误,你应该尝试另一个后端,而不是直接返回500给用户。

-- 设置重试次数
balancer.set_more_tries(2)

-- 如果本次失败,记录并重试
local last_failure = balancer.get_last_failure()
if last_failure then
    ngx.log(ngx.WARN, "request failed: ", last_failure.type, " to ", last_failure.host)
    -- 被动健康检查会记录这次失败
    -- 下次选择时会跳过这个服务器
end

性能调优建议

这套动态机制有开销。健康检查要发送探测请求,服务发现要查询远程API。如果配置不当,可能拖慢整体响应速度。

几个实测经验

  1. 探测间隔:建议2-10秒。太快会消耗资源,太慢响应不及时。高并发场景用2秒,低并发可以用5秒。

  2. 共享内存大小lua_shared_dict healthcheck至少1MB。每个upstream占用约100KB。如果你有10个upstream,分配2MB比较安全。

  3. keepalive连接池:后端服务启用keepalive,减少连接建立开销:

upstream backend {
    server 0.0.0.1;
    keepalive 64;  -- 保持64个连接池
}
  1. 异步健康检查:健康检查用的是ngx.timer,本身异步执行,不会阻塞请求处理。但探测请求本身会消耗HTTP连接。如果后端服务很多,可以适当减少探测频率。

  2. 状态缓存:服务发现查询结果缓存5秒,避免频繁调用Consul/Nacos API。大多数情况下,5秒的延迟是可接受的。

一个完整的生产配置示例

# 共享内存配置
lua_shared_dict healthcheck 2m;
lua_shared_dict upstream_servers 5m;

# HTTP块配置
http {
    # 启用连接池
    keepalive_timeout 60s;
    keepalive_requests 100;

    init_worker_by_lua_block {
        -- 健康检查(2秒探测)
        local healthcheck = require "resty.healthcheck"
        local checker = healthcheck.new({
            shm_name = "healthcheck",
            checks = {
                active = {
                    interval = 2,
                    healthy = { successes = 2 },
                    unhealthy = { tcp_failures = 1, http_failures = 3 }
                },
                passive = {
                    healthy = { successes = 3 },
                    unhealthy = { tcp_failures = 2, http_failures = 3 }
                }
            }
        })

        -- 服务发现(5秒更新)
        timer.every(5, update_upstream_from_consul)
    }

    upstream backend {
        server 0.0.0.1;
        keepalive 64;  -- 连接池

        balancer_by_lua_block {
            -- 选择健康的后端服务器
            local host, port = select_healthy_backend()
            balancer.set_current_peer(host, port)
            balancer.set_more_tries(2)  -- 最多重试2次
        }
    }
}

这套配置在我们的生产环境跑了半年,处理每秒5000请求,响应时间稳定在50毫秒以内。关键是参数要调到合适的值,别太激进也别太保守。

结论

动态upstream这套三层架构,核心是ngx.balancer API提供底层能力,lua-resty-balancer封装负载均衡算法,lua-resty-healthcheck实现健康检查。你把它们串联起来,就能在运行时动态选择后端服务器,完全不用reload Nginx。

服务发现这块,Consul最成熟,Nacos适合Spring Cloud用户,etcd适合K8s环境。根据你现有的技术栈选择,别盲目追求”最优方案”。

动手试试:先从lua-resty-healthcheck开始,把健康检查跑起来。看着后端服务挂掉、自动摘除、恢复、自动加回,这套流程跑顺了,再集成服务发现。Apache APISIX的balancer.lua只有400行代码,你可以直接参考,别从零开始写。

这套机制本质上是让Nginx”活”起来。静态配置变成动态感知,半夜爬起来改配置的日子,可以结束了。

实现 Nginx 动态 upstream

使用 OpenResty 三层架构实现动态服务发现与健康检查

⏱️ 预计耗时: 120 分钟

  1. 1

    步骤1: 安装依赖模块

    安装 OpenResty 及所需 Lua 库:

    • 安装 OpenResty(包含 ngx.balancer)
    • 安装 lua-resty-balancer(负载均衡算法)
    • 安装 lua-resty-healthcheck(健康检查)
    • 安装 lua-resty-http(HTTP 客户端,用于服务发现 API 调用)
  2. 2

    步骤2: 配置共享内存

    在 nginx.conf 的 http 块添加:

    ```nginx
    lua_shared_dict healthcheck 2m;
    lua_shared_dict upstream_servers 5m;
    ```

    • healthcheck:存储健康检查状态(每个 upstream 约 100KB)
    • upstream_servers:存储服务列表(从 Consul/Nacos/etcd 获取)
  3. 3

    步骤3: 实现健康检查

    在 init_worker_by_lua_block 启动健康检查:

    ```lua
    local healthcheck = require "resty.healthcheck"
    local checker = healthcheck.new({
    shm_name = "healthcheck",
    checks = {
    active = {
    type = "http",
    http_path = "/health",
    interval = 2,
    healthy = { successes = 2 },
    unhealthy = { http_failures = 3 }
    }
    }
    })
    ```

    • active:主动探测,每 2 秒发一次 HTTP 请求
    • unhealthy:连续 3 次失败标记为 down
  4. 4

    步骤4: 集成服务发现

    选择一种服务发现方案:

    • Consul:调用 /v1/catalog/service/{name} API
    • Nacos:调用 /nacos/v1/ns/instance/list API
    • etcd:调用 /v3/kv/range API

    使用 ngx.timer.every 每 5 秒更新一次服务列表,存入共享内存。
  5. 5

    步骤5: 配置动态 upstream

    在 upstream 块使用 balancer_by_lua_block:

    ```nginx
    upstream backend {
    server 0.0.0.1; # 占位符
    keepalive 64; # 连接池
    balancer_by_lua_block {
    local servers = get_healthy_servers()
    local rr = roundrobin:new(servers)
    local host, port = rr:select()
    balancer.set_current_peer(host, port)
    balancer.set_more_tries(2)
    }
    }
    ```

    • server 0.0.0.1 是占位符,实际后端由 Lua 动态选择
    • keepalive 64 保持 64 个连接池
    • set_more_tries(2) 最多重试 2 次
  6. 6

    步骤6: 测试与调优

    部署到测试环境验证:

    • 健康检查:停止一个后端服务,观察 Nginx 是否自动摘除
    • 服务发现:重启容器,观察 IP 是否自动更新
    • 性能测试:使用 wrk 或 ab 测试 QPS 和响应时间
    • 调优参数:探测间隔(2-10秒)、连接池大小(64-128)、重试次数(2-3次)

常见问题

lua-resty-upstream-healthcheck 和 lua-resty-healthcheck 有什么区别?
lua-resty-upstream-healthcheck 是 OpenResty 官方库,只支持主动检查。lua-resty-healthcheck 是社区增强版,支持主动+被动检查,经过 Apache APISIX 大规模生产验证。推荐直接用 lua-resty-healthcheck。
如何在 Nginx 中动态更新 upstream 而不 reload?
使用 OpenResty 的 balancer_by_lua_block 钩子,在运行时通过 Lua 代码动态选择后端服务器。后端列表可以存储在共享内存、Redis 或从服务发现 API 获取,完全不用执行 nginx -s reload。
Consul、Nacos、etcd 哪个更适合服务发现?
选择取决于技术栈:

• Consul:最成熟,功能完整,适合通用微服务架构
• Nacos:Spring Cloud Alibaba 默认集成,控制台完善
• etcd:轻量级,Kubernetes 原生集成,适合 K8s 环境
动态 upstream 对性能有影响吗?
有开销但可控。健康检查和服务发现都是异步执行,不会阻塞请求处理。实测:QPS 5000+ 响应稳定 &lt;50ms。关键是合理设置参数:探测间隔 2-10 秒、共享内存 2-5MB、连接池 64-128。
如何在 Kubernetes 环境中实现动态服务发现?
两种方案:1)直接调用 K8s API,读取 Endpoints 获取 Pod IP 列表;2)使用 CoreDNS 服务发现,通过 DNS 查询解析服务名。推荐方案 1 更灵活,可以实现灰度发布、自定义路由等高级功能。
健康检查的探测间隔应该设置为多少?
建议 2-10 秒。高并发场景用 2 秒快速感知故障,低并发场景用 5-10 秒减少资源消耗。间隔太短会增加后端压力和 Nginx 资源占用,太长会延迟故障发现时间。结合 fall(连续失败次数)和 rise(连续成功次数)参数一起调整。

15 分钟阅读 · 发布于: 2026年5月7日 · 修改于: 2026年5月14日

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

Nginx 实战指南

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

查看系列总览

相关文章

BetterLink

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

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

关注公众号

评论

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