排查lua脚本导致的文件句柄泄漏,需重点检查io.open未close、ngx.pipe未清理、c模块连接池未归还三类问题,结合lsof、strace和debug日志快速定位并修复。

排查第三方 Lua 或 OpenResty 脚本引发的“文件句柄未关闭”问题,核心在于识别脚本中打开但未显式释放的资源,并确认其是否同时拖累内存与句柄——这类泄漏往往不是单点崩溃,而是缓慢堆积、重启后快速复现。
盯住 Lua 中三类高危文件操作
Lua 本身不直接管理 fd,但通过 io.open、ngx.pipe、C 模块(如 resty.mysql、resty.redis)或自定义 FFI 调用,极易引入未关闭句柄:
-
io.open() 打开的文件未调用 close():尤其在异常分支(如
pcall失败后)遗漏fh:close();建议统一用local fh = io.open(...) ; if fh then ...; fh:close() end模式,或改用io.open(...):read("*a")等一次性读取方式 -
ngx.pipe() 创建的管道未清理:用于子进程通信时,若未在
content_by_lua*或log_by_lua*中显式调用pipe:close(),fd 会滞留整个请求生命周期甚至更久 -
C 模块连接池未正确归还:例如
resty.redis:new()后未调用redis:set_keepalive()或redis:close();注意set_keepalive是归还到连接池,close是彻底释放 fd;若连接池已满或超时未配置,fd 就卡在ESTABLISHED状态不释放
快速验证是否为 Lua 脚本导致
不用翻代码也能初步锁定范围:
- 临时注释所有
access_by_lua*、content_by_lua*、log_by_lua*块,仅保留基础 proxy_pass;观察lsof -p $(pgrep -f "nginx: worker" | head -1) | wc -l是否停止上涨,若明显回落,基本可断定是 Lua 层问题 - 启用
lua_code_cache off;+error_log logs/error.log debug;,触发一次请求后检查 error.log 是否出现lua io stream相关 warning,或大量failed to set keepalive日志 - 用
strace -p [worker_pid] -e trace=open,openat,close,closeat实时抓系统调用,重点看是否有openat成功但无对应close的记录(注意过滤掉 Nginx 自身日志/缓存等正常操作)
定位具体脚本与行号的实操方法
结合 OpenResty 提供的调试能力,精准到行:
- 在疑似脚本开头插入:
local dbg = require "debug",并在关键 open 操作后加ngx.log(ngx.DEBUG, "open at:", dbg.getinfo(2).short_src, dbg.getinfo(2).linedefined),让每处打开都打点日志 - 利用
lua-resty-core的ngx.ctx记录打开的句柄对象(如ngx.ctx.open_files = ngx.ctx.open_files or {}),在log_by_lua*中遍历并打印未 close 的条目(需配合getmetatable(fh).__gc判断是否已释放) - 若使用 LuaJIT,可借助
jit.off()+collectgarbage("count")对比前后内存变化,再结合lsof看 fd 数是否同步增长;两者同向飙升,说明是 Lua 对象持有 fd 引用未释放
修复与预防的关键动作
不只是加 close(),更要建立资源生命周期意识:
- 所有
io.open必须配对pcall(fh.close, fh),避免异常中断导致跳过关闭 - 数据库/Redis 客户端优先走
set_keepalive(max_idle_timeout, pool_size),而非close();确保max_idle_timeout小于 upstream 的keepalive_timeout - 禁用
lua_code_cache off用于生产排查后,务必切回on;否则每个请求重编译 Lua,会额外产生大量临时字符串和闭包,加剧内存压力 - 在
init_by_lua*阶段预热连接池,但不要在该阶段执行io.open等阻塞操作;文件操作一律放在content_by_lua*内,并严格限定作用域











