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-healthcheck | lua-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天然集成,这是个不错的选择。
三种方案对比
| 对比项 | Consul | Nacos | etcd |
|---|---|---|---|
| 原生健康检查 | 支持(HTTP/TCP) | 支持 | 不支持(需自建) |
| 权重动态调整 | 支持 | 支持(可视化) | 需自行实现 |
| Spring Cloud集成 | 支持 | 默认集成 | 需额外配置 |
| 控制台 | 有Web UI | 有Web UI(更完善) | 无(需第三方) |
| 配置管理 | 支持(KV存储) | 支持(更强大) | 支持 |
| 国内社区活跃度 | 中等 | 高 | 高(K8s生态) |
| 适用场景 | 通用微服务 | Spring Cloud Alibaba | K8s环境 |
我的选择:如果用Spring Cloud,直接Nacos。如果用K8s,etcd方便。如果想要独立完整的服务发现平台,Consul最成熟。
实战场景与性能优化
三层架构搭好了,服务发现也集成了。接下来看几个典型场景的实际应用。
场景1:Kubernetes入口网关
K8s的Pod生命周期很短。扩容、缩容、升级都会导致Pod重新创建,IP地址跟着变。用静态upstream根本没法管理。
OpenResty可以动态感知Pod变化。思路是:
- 在
init_worker_by_lua_block里启动定时器,每隔5秒查询K8s API或CoreDNS - 解析服务对应的Pod IP列表
- 更新到共享内存
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。如果配置不当,可能拖慢整体响应速度。
几个实测经验:
-
探测间隔:建议2-10秒。太快会消耗资源,太慢响应不及时。高并发场景用2秒,低并发可以用5秒。
-
共享内存大小:
lua_shared_dict healthcheck至少1MB。每个upstream占用约100KB。如果你有10个upstream,分配2MB比较安全。 -
keepalive连接池:后端服务启用keepalive,减少连接建立开销:
upstream backend {
server 0.0.0.1;
keepalive 64; -- 保持64个连接池
}
-
异步健康检查:健康检查用的是ngx.timer,本身异步执行,不会阻塞请求处理。但探测请求本身会消耗HTTP连接。如果后端服务很多,可以适当减少探测频率。
-
状态缓存:服务发现查询结果缓存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: 安装依赖模块
安装 OpenResty 及所需 Lua 库:
• 安装 OpenResty(包含 ngx.balancer)
• 安装 lua-resty-balancer(负载均衡算法)
• 安装 lua-resty-healthcheck(健康检查)
• 安装 lua-resty-http(HTTP 客户端,用于服务发现 API 调用) - 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: 实现健康检查
在 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: 集成服务发现
选择一种服务发现方案:
• Consul:调用 /v1/catalog/service/{name} API
• Nacos:调用 /nacos/v1/ns/instance/list API
• etcd:调用 /v3/kv/range API
使用 ngx.timer.every 每 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: 测试与调优
部署到测试环境验证:
• 健康检查:停止一个后端服务,观察 Nginx 是否自动摘除
• 服务发现:重启容器,观察 IP 是否自动更新
• 性能测试:使用 wrk 或 ab 测试 QPS 和响应时间
• 调优参数:探测间隔(2-10秒)、连接池大小(64-128)、重试次数(2-3次)
常见问题
lua-resty-upstream-healthcheck 和 lua-resty-healthcheck 有什么区别?
如何在 Nginx 中动态更新 upstream 而不 reload?
Consul、Nacos、etcd 哪个更适合服务发现?
• Consul:最成熟,功能完整,适合通用微服务架构
• Nacos:Spring Cloud Alibaba 默认集成,控制台完善
• etcd:轻量级,Kubernetes 原生集成,适合 K8s 环境
动态 upstream 对性能有影响吗?
如何在 Kubernetes 环境中实现动态服务发现?
健康检查的探测间隔应该设置为多少?
15 分钟阅读 · 发布于: 2026年5月7日 · 修改于: 2026年5月14日


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