2012/11/27 杨虎:
> 我现在尝试使用了下nginx lua模块编写业务模块,感觉挺爽的,能用同步的方式编写非阻塞的代码,感觉挺好的,对人类是一个伟大的贡献。。。
>
多谢鼓励! :)
> 我的业务代码模型是这样的,通过nginx tcp
> sock链接一个后端的一个路由服务(自定义的协议,不是http协议),该服务返回一个url后,将sock
> 通过setkeepalive保存到连接池。然后通过pass_proxy访问该url。
>
[...]
> call_ra就是访问后端的服务,其响应时间不是很快。hap模块的编写方式参考你写的mysql的模块。压力运行一段时候停止后发觉nginx的每个worker的rss都差不多在700多m了。导致OOM。。。
>
多谢你的报告!只是我想先问几个问题:
1. 你使用的是什么版本的 ngx_lua 模块?或者你使用的是某个版本的 openresty 软件包?请确保你使用的是最新版本 :)
2. 你使用的是最新的 LuaJIT 2.0.0 正式版吗?较老一些的 LuaJIT 版本的内存分配器存在已知的内存泄漏 bug.
3. 你是在 Linux x86_64 上面?
4. 你能通过下面这个接口检查一下 Lua 的 GC 实现分配了多少空间?
location = /gc {
content_by_lua '
collectgarbage()
ngx.say(string.format("Worker %d: GC size: %.3f KB",
ngx.var.pid, collectgarbage("count"))) ';
}
}
5. 你能否使用 Nginx Systemtap Toolkit 中的 ngx-active-reqs, ngx-shm,
ngx-cycle-pool, 和 ngx-leaked-pools 这几个工具分析一下你存在内存泄漏的 nginx worker
进程内部的内存分配(不包含 Lua GC 的部分)?
https://github.com/agentzh/nginx-systemtap-toolkit#ngx-active-reqs
6. 你是否方便提供一个最小化的可以复现问题的完整示例,以便我可以在我本地复现你看到的泄漏?这样我可以以最快的速度修正问题(如果有的话)。
>
> 在本机通过valgrind跑了下结果如附件。
>
你提供的 valgrind 报告中未见到确定的内存泄漏位置。
> 第一次使用lua写程序,是不是写的lua发生了内存泄漏?
>
常见的 Lua 程序的泄漏是由于把数据不断累积到自定义的 Lua module 级别的变量中去,而 Lua module 级别的数据在
worker 进程中是共享和持久的,见这里:
http://wiki.nginx.org/HttpLuaModule#Data_Sharing_within_an_Nginx_Worker
http://wiki.nginx.org/HttpLuaModule#Lua_Variable_Scope
我明天再写一个 Lua 工具动态分析你的 nginx worker 进程中 Lua GC 的内存分配细节(就像 Nginx
SystemTap Toolkit 里的那些分析 Nginx 内存分配细节的工具那样)。
同时抄送给 openresty 中文邮件列表:https://groups.google.com/group/openresty
这样其他用户也可以看到我们这里的讨论。同时也欢迎你加入此列表 :)
Thanks!
-agentzh
local class_mt = { -- to prevent use of casual module global variables __newindex = function (table, key, val) error('attempt to write to undeclared variable "' .. key .. '"') end } setmetatable(_M, class_mt)
This will guarantee that local variables in the Lua module functions are all declared with the local keyword, otherwise a runtime exception will be thrown. It prevents undesirable race conditions while accessing such variables. See Data Sharing within an Nginx Worker for the reasons behind this.
error('attempt to write to undeclared variable "' .. key .. '": '.. debug.traceback())end谢谢
2012/11/27 junwei shi:
> 1.对上面的描述有点不理解:添加这段代码的意义是为了防止module里定义的function里的变量不带local声明(如果都带上local,上面的代码是否非必要了?),
关键是你很难确保所有的局部变量都恰当地用 local 声明了,所以才需要这种运行时的检查。即使是我自己在编写 lua-resty-*
库的过程中也经常犯这种错误。而原先淘宝量子统计的 Lua 应用也在这里栽过跟头,所以我才把这个提示写进了文档。
> 导致在一个worker进程内,它以一个全局变量的身份存在的情况?但是为什么会导致内存泄漏?
这里的主要目的为了防止局部变量被多个并发请求意外共享,从而出现请求 A 看到请求 B
的数据。当然,这也有助于防止因为局部变量被意外提升至模块级别,而导致不能及时被回收而导致各种程度的内存泄漏。
> 是因为可能存在msg
> = msg + “xxxx"这样的情况,累积导致内存泄漏?
还有 table.insert(some_table, some_value) 和 some_table[some_key] =
some_value 这种 :)
> 2.http://wiki.nginx.org/HttpLuaModule#Data_Sharing_within_an_Nginx_Worker
> 里用了一个错误的指令:content_lua_by_lua?
嗯,这是一处笔误,多谢指出;已修正!谢谢!
> 3.看agentzh最新的lua-resty-*模块,module("resty.xxx", package.seeall) 改成
> module(...),而且下面的代码也被替换了,可以说说主要的原因么?
> getmetatable(resty.xxx).__newindex = function (table, key, val)
>
> error('attempt to write to undeclared variable "' .. key .. '": '
> .. debug.traceback())
> end
>
我在邮件列表的这封邮件中有详细的讨论(英文):
https://groups.google.com/group/openresty/msg/349859a2b2add127
Best regards,
-agentzh
1.local class_mt = { -- to prevent use of casual module global variables __newindex = function (table, key, val) error('attempt to write to undeclared variable "' .. key .. '"') end } setmetatable(_M, class_mt)This will guarantee that local variables in the Lua module functions are all declared with the
localkeyword, otherwise a runtime exception will be thrown. It prevents undesirable race conditions while accessing such variables. See Data Sharing within an Nginx Worker for the reasons behind this.1.对上面的描述有点不理解:添加这段代码的意义是为了防止module里定义的function里的变量不带local声明(如果都带上local,上面的代码是否非必要了?),导致在一个worker进程内,它以一个全局变量的身份存在的情况?但是为什么会导致内存泄漏?是因为可能存在msg = msg + “xxxx"这样的情况,累积导致内存泄漏?
2.http://wiki.nginx.org/HttpLuaModule#Data_Sharing_within_an_Nginx_Worker 里用了一个错误的指令:content_lua_by_lua?
3.看agentzh最新的lua-resty-*模块,module("resty.xxx", package.seeall) 改成 module(...),而且下面的代码也被替换了,可以说说主要的原因么?getmetatable(resty.xxx).__newindex = function (table, key, val)error('attempt to write to undeclared variable "' .. key .. '": '.. debug.traceback())end
--
邮件自: 列表“openresty”,专用于技术讨论!
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+...@googlegroups.com
详情: http://groups.google.com/group/openresty
官网: http://openresty.org/
仓库: https://github.com/agentzh/ngx_openresty
建议: 提问的智慧 http://wiki.woodpecker.org.cn/moin/AskForHelp
教程: http://agentzh.org/misc/nginx/agentzh-nginx-tutorials-zhcn.html
2012/11/28 believe3301:
> 4 按照你的方法检查了下GC size: 118.157 KB,是不是在lua层面没有泄漏啊?
是的,Lua 层面不像有泄露 :)
> 我使用了https://github.com/cloudwu/pbc 的lua binding做pb的序列化,不知道是不是这块的原因。。
>
你可以尝试临时去除这个库再进行测试 :)
> systemtap啥的正在准备。。
>
Cool! :)
Best regards,
-agentzh
-agentzh
2012/11/29 杨虎:
> 通过ab -t 60 -c 10000 -n 100000 "http://127.0.0.1:8090/test2"
>
> 然后在查看gc
> $ curl -i "http://127.0.0.1:8090/gc"
> HTTP/1.1 200 OK
> Server: nginx/1.2.4
> Date: Thu, 29 Nov 2012 08:41:40 GMT
> Content-Type: application/octet-stream
> Transfer-Encoding: chunked
> Connection: keep-alive
>
> Worker 16297: GC size: 2436.806 KB
>
>
> $ curl -i "http://127.0.0.1:8090/gc"
> HTTP/1.1 200 OK
> Server: nginx/1.2.4
> Date: Thu, 29 Nov 2012 08:41:46 GMT
> Content-Type: application/octet-stream
> Transfer-Encoding: chunked
> Connection: keep-alive
>
> Worker 16297: GC size: 154.040 KB
>
> 第一次查看2m多,马上在第二次查看150多k,这正常么?
>
1. 你没有说明这两次 curl 调用与 ab 调用之间在时间上的具体关系,同时你未给出 location /test2
的具体代码实现,所以我无法帮你判断是否正常。
2. 在使用 ab 进行大压力测试时,最好指定 -k 选项以启用 HTTP (1.0) keepalive,否则很容易把你运行 ab 的机器上的动态端口耗尽。
3. 建议你的接口内部为上游连接也启用连接池(即 cosocket 连接池),以避免把你运行 nginx 的机器上的动态端口耗尽。
4. 总是检查 ab 输出中的 "Failed requests" 和 "Write errors" 这两项的值,确保它们总是为
0,否则说明某些请求发生了错误。
5. 总是检查 nginx 的错误日志文件(默认为 logs/error.log)中是否有错误消息(同时确保你的 nginx.conf
中没有在配置 error_log 指令时指定 crit 或者 alert 这样过高的过滤级别而导致你看不到 error 级别的日志消息)。
下面我给出一个在我本地测试过的完整示例。
因为 lua-resty-mysql 是开源的,所以你也可以在你本地重复这个实验。这个实验的结果是下面这张 PNG 图片(同时也在邮件附件中):
http://agentzh.org/misc/nginx/lua-gc-resty-mysql.png
它清楚地显示出 lua GC 的分配空间随时间的变化(同时包括了 ab 启动前,ab 运行过程中,以及 ab 退出后这三段时间的变化)。
首先,我们的 nginx.conf 是这个样子的:
keepalive_timeout 60;
location = /t {
content_by_lua '
local mysql = require "resty.mysql"
local db = mysql:new()
local function error (...)
ngx.log(ngx.ERR, ...)
end
db:set_timeout(2000) -- 2 sec
local ok, err, errno, sqlstate = db:connect({
host = "127.0.0.1",
port = 3306,
database = "world",
user = "ngx_test",
password = "ngx_test"})
if not ok then
error("failed to connect: ", err, ": ", errno, " ", sqlstate)
return
end
local res, err = db:query("select * from City limit 1")
if not res then
error("failed to query: ", err)
return
end
local cjson = require "cjson"
ngx.say(cjson.encode(res))
local ok, err = db:set_keepalive(0, 1024)
if not ok then
error("failed to set keepalive: ", err)
return
end
';
}
location = /gc {
keepalive_timeout 0;
content_by_lua '
collectgarbage()
ngx.say(string.format("%.3f KB", collectgarbage("count")))
';
}
这里我使用的 MySQL 数据库 world 是 MySQL 官方提供的一个公开的样例数据库:
http://dev.mysql.com/doc/world-setup/en/index.html
然后我使用下面这个名为 ./bench 的 Bash 脚本进行压力实验(假设我们的 nginx 服务监听的是 8080 端口):
#!/bin/bash
port=8080
echo -n > gc.txt
function sample {
for (( ; ; )) do
sleep 0.05
curl -s 127.0.0.1:$port/gc >> gc.txt
done
}
sample &
pid=$!
sleep 1.5
echo ab starts >> gc.txt
ab -dS -c100 -n200000 -k localhost:$port/t > ab.txt
echo ab quits >> gc.txt
sleep 1.5
kill $pid && echo pid $pid killed
我们可以看到,它一方面使用 ab 对 /t 接口保持压力,另一方面则保持 20 Hz 的频率通过 curl 访问 /gc 接口。同时在启动
ab 前我们对空闲状态的 nginx 采样 1.5 秒;在 ab 退出之后,我们同时对空闲状态的 nginx 再次采样 1.5 秒。
运行这个 ./bench 脚本之后会生成 gc.txt 和 ab.txt 这两个文件;前者是对 /gc 采样的结果汇总,而后者则是 ab 命令的输出:
$ ./bench # 第一次运行是为了对 nginx 预热
$ ./bench
我们忽略掉第一次运行 ./bench 的结果,因为 nginx 需要预热过程。
接下来我们使用下面这个 Perl 脚本对 gc.txt 这个原始输出文件进行处理,生成可供分析和绘图之用的 CSV 逗号分隔文件:
http://agentzh.org/misc/nginx/lua-gc-resty-mysql.pl
具体命令是:
$ perl lua-gc-resty-mysql.pl gc.txt > a.csv
最后再通过下面这个 gnuplot 脚本生成 PNG 图片:
# a.gnu
set terminal pngcairo background "#ffffff" size 800, 600 \
enhanced font "Inconsolata,15" linewidth 1.5
set yrange [200:550]
set datafile separator ","
set title "Lua GC allocation changes for lua-resty-mysql"
set xlabel "Time (seconds)"
set ylabel "Lua GC count (KBytes)"
set output "lua-gc-resty-mysql.png"
plot "a.csv" using 1:2 with lines title "before ab starts" lw 1.5, \
"a.csv" using 1:3 with lines title "loaded by ab" lw 1.5, \
"a.csv" using 1:4 with lines title "after ab quits" lw 1.5
生成图片的命令是:
$ export GDFONTPATH=/usr/share/fonts/levien-inconsolata
$ gnuplot a.gnu
生成的图片文件名为 a.png,就在当前工作目录中。一个实例是:
http://agentzh.org/misc/nginx/lua-gc-resty-mysql.png
> 还有就是在nginx里通过#获取table的长度一直是0,这正常么?我不通过nginx,直接通过luajit获取table的长度是正常的。。
>
我在我本地尝试了下面这个简单的示例,# 运算符确实如我们期望那般工作的:
# nginx.conf
location = /t {
content_by_lua '
local tb = {"a", "b", nil, "d"}
ngx.say(#tb)
';
}
访问 /t 接口得到输出(假设 nginx 监听的是本地的 8080 端口):
$ curl localhost:8080/t
4
显然,4 是我们期望的结果。你也可以在你本地尝试这个例子。
如果你认为 ngx_lua 的行为不符合期望,请总是提供比较完整的代码示例,否则我也无法帮助你诊断问题 :)
Best regards,
-agentzh

。。
Best regards,
-agentzh
多谢 agentzh大哥了,写这么长。。。1 我知道为啥获取len不对了,我定义的table不是列表式的,而是类似的是t = { x =1 , y = 2 }, 这种不迭代的话应该如何获取长度?
2 对于upstream我使用了setkeepalive(0,100),不过也经常报下面两种错误1) 10240 worker_connections are not enough2) lua tcp socket connect timed out是否是upstream性能不够导致的还是我超时时间设置的3s太短了?3) 好像luajit 不支持unpack,对于t = { "Content-Type","text/css" }ngx.req.set_header(table.unpack(t))
我现在就写成ngx.req.set_header(t[1], t[2]),有更优雅的写法么?
4)对于自定义的协议,我想使用capture_multi使用多个subrequest同时访问多个upstream有啥好的解决方案么?我现在就是一次访问一个,访问时间为两者之和。
5)我想使用lua脚本访问一个路由服务,这个服务返回多个地址,然后类似nginx 标准的upstream模块对这多个地址访问,有容错处理。这种场景通过lua有好的解决方案么?
2012/11/29 tg.yang wrote
> 1 我知道为啥获取len不对了,我定义的table不是列表式的,而是类似的是t = { x =1 , y = 2 }, 这种不迭代的话应该如何获取长度?
>
这个是 Lua 语言方面的 FAQ,和 ngx_lua 模块本身无关。哈希表类型的 Lua table 并不能通过 # 运算符获取长度:
$ lua -e 'local tb = {x = 1, y = 2} print(#tb)'
0
$ luajit -e 'local tb = {x = 1, y = 2} print(#tb)'
0
建议先仔细阅读 Lua 5.1 语言的参考手册,以节约大家的时间,呵呵:
http://www.lua.org/manual/5.1/manual.html
> 2 对于upstream我使用了setkeepalive(0,100),不过也经常报下面两种错误
>
> 1) 10240 worker_connections are not enough
你需要在 nginx.conf 中恰当地配置 worker_connections 配置指令:
http://wiki.nginx.org/EventsModule#worker_connections
这个错误是说你的 worker_connections 配置的上限不够大。
值得一提的是,这里 worker_connections 的含义是指每个 nginx worker
进程中可以创建的连接数的上限。而连接数不仅包括下游连接(即 http 请求对应的连接),也包括上游连接(包括 ngx_lua
cosocket 和 nginx upstream 创建的连接)。
> 2) lua tcp socket connect timed out
> 是否是upstream性能不够导致的还是我超时时间设置的3s太短了?
>
常见的一种情况是你的远方服务过于繁忙而来不及 accept() 新的连接请求,导致远端的 accept 队列溢出,而导致 nginx
一侧发出的 TCP SYN 包被丢弃。另外一种情况就是 nginx 与你远方服务之间的网络链路上的延时问题。你需要通过 tcpdump 或者
wireshark 这样的抓包工具予以确认。
> 3) 好像luajit 不支持unpack,
> 对于
> t = { "Content-Type","text/css" }
> ngx.req.set_header(table.unpack(t))
> 我现在就写成
> ngx.req.set_header(t[1], t[2]),有更优雅的写法么?
>
table.unpack 是 Lua 5.2 语言的特性,在 Lua 5.1 语言中并不存在(正如 Kindy 同学所指出的)。而
ngx_openresty 只与 Lua 5.1 兼容(无论是标准 Lua 5.1 解释器还是 LuaJIT 2.0)。所以你需要这么写:
ngx.req.set_header(unpack(t))
即去掉 table. 前缀。细节可以参见 Lua 5.1 语言的官方手册:
http://www.lua.org/manual/5.1/manual.html#pdf-unpack
> 4)对于自定义的协议,我想使用capture_multi使用多个subrequest同时访问多个upstream有啥好的解决方案么?
> 我现在就是一次访问一个,访问时间为两者之和。
>
1. 如果你的自定义协议客户端是用 nginx upstream C 模块来实现的,则可以直接使用
ngx.location.capture_multi API 来发起多个并发的子请求,细节见官方文档:
http://wiki.nginx.org/HttpLuaModule#ngx.location.capture_multi
2. 如果你的客户端是像 lua-resty-mysql 那样基于 ngx_lua 的 cosocket API 来实现的话,则可以直接使用
ngx.thread API 来发起多个并发的 cosocket 连接,细节可以参见文档:
http://wiki.nginx.org/HttpLuaModule#ngx.thread.spawn
> 5)我想使用lua脚本访问一个路由服务,这个服务返回多个地址,然后类似nginx 标准的upstream模块对这多个地址访问,有容错处理。这种场景通过lua有好的解决方案么?
>
Lua 是图灵完全的语言,容错逻辑不过是一些 if else 而已。特别地,你会发现前面提及的 ngx.thread API 会对降低延时很有帮助。
另外,建议与当前邮件主题《关于使用nginx lua模块导致nginx内存暴涨》无关的问题和内容放入拥有正确标题的独立邮件中,这样方便大家追踪和回复。谢谢合作。
Best regards,
-agentzh
2012/12/2 tiger yang:
> 内存泄漏的问题解决了,出在cloudwu的pbc上。最新的commit经过压了几个小时经过测试没泄漏了。
>
> https://github.com/cloudwu/pbc/commit/18a65350f2e1c53b423e3a62b767b90a7308b28e
>
Cool! 多谢反馈结果 :) 很高兴并不是 ngx_lua 或者 nginx 的问题 :)
> 还有就是虽然nginx lua是turing 完全的,如果春哥能给加个实现第5点的feture那就没必要所有人都重复造轮子了。。
>
我认为这样的特性适合实现在一个独立的第三方的 Lua 库中,比如可以提供常用的取模、一致性哈希等哈希函数。它并不适合实现在 ngx_lua
核心中。欢迎编写并开源你自己的实现 :)
Best regards,
-agentzh