Nginx 与 OpenResty 在高并发网关中的取舍
去年做网关选型时,我们在原生 Nginx、OpenResty 和 Envoy 之间纠结了将近三周。最终选了 OpenResty,但这个结论并不通用——它高度依赖"你的网关需要多频繁地变更路由规则"这一个变量。这里把当时的对比和压测数据整理出来。
核心分歧:配置是静态的还是动态的
原生 Nginx 的配置模型是启动时确定。你可以 nginx -s reload,但 reload 的实质是启动一批新 worker、优雅关闭旧 worker。当你的连接是长连接(WebSocket、gRPC stream)时,旧 worker 会一直挂着直到 worker_shutdown_timeout,配置变更的代价随连接数线性增长。
OpenResty 通过 Lua 把一部分逻辑挪到了运行时。路由表可以放在共享内存 lua_shared_dict 里,由一个定时任务从配置中心拉取更新,不需要 reload。
location / {
access_by_lua_block {
local routes = ngx.shared.routes
local upstream = routes:get(ngx.var.host .. ngx.var.uri)
if not upstream then
return ngx.exit(404)
end
ngx.ctx.upstream = upstream
}
proxy_pass http://$ctx_upstream;
}
(实际实现比这复杂,要处理前缀匹配和优先级,但思路是这样。)
压测数据
环境:4C8G × 3 台,wrk 从另外 6 台机器打流,后端是一个返回固定 256 字节 JSON 的 Go 服务。全部走 keepalive。
| 场景 | 原生 Nginx | OpenResty | 差异 |
|---|---|---|---|
| 纯反向代理,1k 并发 | 48,200 QPS | 47,600 QPS | -1.2% |
| 纯反向代理,5k 并发 | 51,300 QPS | 50,100 QPS | -2.3% |
| + Lua 动态路由(dict 命中) | — | 46,800 QPS | -3.1% |
| + Lua JWT 校验(纯 Lua 实现) | — | 31,400 QPS | -34.9% |
| p99 延迟(1k 并发,动态路由) | 4.1 ms | 4.6 ms | +12% |
结论很清楚:Lua 本身不慢,慢的是你在 Lua 里干了什么。查一次共享内存字典的开销可以忽略(-3%),但做一次 JWT 签名校验就吃掉了三分之一的吞吐。
那些没写进压测表的成本
选 OpenResty 之后,我们额外承担了这些东西:
- 可观测性变差。原生 Nginx 的
stub_status和 access log 是标准化的,接入现成的监控方案很省事。Lua 逻辑里的错误得自己埋点,否则出了问题只能看到一片 500。 - 调试门槛高。Lua 的错误栈在 nginx error log 里,行号经常对不上,尤其是用了
pcall之后。团队里只有两个人能比较顺畅地排查 OpenResty 层的问题。 - 升级要跟两个上游。Nginx 版本和 OpenResty 版本不是一一对应的,安全补丁的发布节奏也不一致。CVE 出来时得等 OpenResty 跟进。
如果重来一次
我会先问一个问题:路由规则的变更频率是多少?
如果是每天几次,那原生 Nginx + reload 完全够用,reload 在几百个连接规模下是无感的,为此引入 Lua 是过度设计。如果是每分钟都有变更、或者有数千个长连接不能断,那 OpenResty 的动态能力就是刚需,那 3% 的性能损耗和额外的运维复杂度是值得付的。
我们的场景是后者(多租户,租户自助配置路由),所以选了 OpenResty。但如果只是做一个内部的 API 网关,我会直接选原生 Nginx——少一层运行时,少一堆说不清楚的问题。
至于 Envoy,它的 xDS 动态配置确实更优雅,但当时团队的 C++/Go 技术栈储备不足以支撑,运维侧也没有经验。技术选型里"团队能不能 hold 住"这一项的权重,往往比性能数据高得多。