OpenResty 不直接管理 TCP 长连接生命周期,而是依托 Nginx 事件驱动模型,通过 keepalive_timeout/keepalive_requests 控制连接物理存续,结合 Lua 各阶段钩子(如 access_by_lua、log_by_lua)与 shared dict 实现连接级状态跟踪和资源安全清理。

OpenResty 本身不直接管理 TCP 层的“长连接生命周期”,它依托 Nginx 的事件驱动模型和连接复用机制,在 HTTP 层(尤其是 keepalive 连接)与请求处理阶段提供精细化控制能力。真正起作用的是:Nginx 的连接池管理 + Lua 在各处理阶段的介入 + ngx_lua 提供的协程感知 I/O 原语。关键不是“维持连接”,而是“在连接存活期间,按需干预请求、复用上下文、安全清理资源”。
理解 Nginx 的连接复用基础
Nginx 默认启用 HTTP/1.1 keepalive,客户端可复用 TCP 连接发送多个请求。每个 worker 进程维护自己的连接池,连接空闲超时由 keepalive_timeout 控制(默认 65s),最大请求数由 keepalive_requests 控制(默认 100)。这些参数决定了连接物理生命周期的上限,是 Lua 干预的前提。
- 连接未关闭前,所有同连接的请求共享同一个 socket 句柄,但每个请求仍独立走完整的 Nginx phase 流程
- worker 内部每个请求由一个 Lua 协程处理,协程间数据隔离,但可通过 shared dict 或 lua-resty-core 提供的
ngx.ctx在单次请求内跨 phase 共享数据 - 注意:
ngx.ctx生命周期仅限于单个请求;若想跨请求(即同 TCP 连接下的多个请求)传递状态,必须使用shared dict,并自行设计 key(如基于 client_addr + connection_id)
在请求阶段嵌入生命周期感知逻辑
OpenResty 提供了多个钩子,可在不同阶段响应连接行为。对长连接场景,重点关注以下三类:
- init_worker_by_lua*:worker 启动时初始化全局资源(如 Redis 连接池、LRU 缓存),适用于连接复用前的准备
-
set_by_lua* 或 rewrite_by_lua*:在请求解析后、路由前,可读取 header(如
Connection: keep-alive)、检查 client IP、提取 token,决定是否允许复用或标记会话特征 -
log_by_lua*:请求结束时执行,是清理资源最安全的位置。例如记录本次请求耗时、更新 shared dict 中该连接的活跃计数、判断是否达到
keepalive_requests上限并主动 close socket(需配合ngx.exit(ngx.HTTP_CLOSE))
用 shared dict 实现连接级状态跟踪
要实现“某客户端在当前长连接中已发起 8 次请求,第 9 次触发限流”,不能依赖变量或 ngx.ctx,必须用共享内存字典:
- 在 nginx.conf 的 http 块中定义:
lua_shared_dict conn_stats 10m; - Lua 中生成唯一连接标识(推荐组合
ngx.var.remote_addr .. ":" .. ngx.var.connection) - 在
access_by_lua_block中递增计数:local conn_key = ngx.var.remote_addr .. ":" .. ngx.var.connection; local stats = ngx.shared.conn_stats; local n = stats:incr(conn_key, 1, 0); if n > 10 then ngx.exit(429) end - 在
log_by_lua_block中清理:stats:delete(conn_key)(注意:不是每次请求都删,而是在连接关闭或超时时删;更稳妥做法是设置过期时间stats:set(conn_key, true, 70),略长于 keepalive_timeout)
避免常见陷阱
长连接 + Lua 容易引发资源泄漏或状态错乱,需特别注意:
- 不要在
init_by_lua*中创建 socket 连接(如 redis:new()),它运行在 master 进程,不可用于 worker 请求处理;应改用init_worker_by_lua* - 不要在
content_by_lua_block中执行阻塞操作(如 os.execute、io.open),会卡住整个 worker;必须用 ngx.socket.tcp 或 resty-redis 等异步封装库 - shared dict 不是数据库,没有事务和 TTL 自动回收;高频写入需加锁(
stats:lock(key))或采用 CAS 操作,否则并发递增可能丢失 - HTTP/2 下连接复用更复杂,header 压缩、流多路复用会影响
connection变量语义;建议优先用ngx.var.http2判断协议,再选择策略


















