《OpenResty Lua 编程最佳实践与问题排查》
本文汇总 OpenResty 中 Lua 编程的写法规范与线上常见问题。
1 通过 ngx.var 获取 cookie 中的指定字段
在 Lua 中通过 ngx.var 获取 cookie 中指定 key 的值。假定 key 名字为 keyName,那么在 ngx.var 中索引为 cookie_keyName(以 cookie_ 为前缀拼接 key 名)。
ngx.var["cookie_keyName"]
例如请求头中包含 cookie: hello=skl,代码中就用 ngx.var["cookie_hello"] 取到 skl。
2 返回 JSON body
ngx.status = ngx.HTTP_OK
ngx.header.content_type = "application/json; charset=utf-8"
local response = {
status = ngx.HTTP_OK,
success = true
}
ngx.say(cjson.encode(response))
return ngx.exit(ngx.HTTP_OK)
3 ngx.req.get_body_data 和 ngx.req.get_post_args 的区别
ngx.req.get_post_args(max_args?):返回一个 Lua table,包含所有application/x-www-form-urlencoded类型的 POST 参数。需要先调用ngx.req.read_body,或开启lua_need_request_body。ngx.req.get_body_data():返回内存中的请求体 Lua 字符串(而非解析后的 table)。以下情况返回nil:请求体未被读取、请求体被读入磁盘临时文件、请求体长度为零。
要点:若请求体被读入磁盘,改用 ngx.req.get_body_file。强制请求体驻留内存可把 client_body_buffer_size 设成与 client_max_body_size 相同。
4 ngx.print 与 ngx.say
ngx.say 与 ngx.print 类似,只是在末尾多加了一个换行符。当 OpenResty 与前端 ajax 交互时,如果服务端使用 ngx.say(false),客户端收到的是一段字符串 "false\n"。为避免客户端处理多余的换行,这种场景建议使用:
ngx.print(false)
这样客户端只需做字符串判断:returnValue == "false"。
5 ngx.exit 参数的选用
ngx.exit 只在 rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua 阶段可调用。
(1)ngx.status、ngx.say、ngx.exit 必须按顺序调用,否则会有非预期行为。调用 ngx.exit 时最好使用 return ngx.exit(403) 明确终止当前请求。
(2)先 ngx.say 再 ngx.exit,响应状态码必定为 200:
location = /hello {
rewrite_by_lua_block {
ngx.say("rewrite_by_lua_block")
return ngx.exit(403)
}
}
实际返回 HTTP/1.1 200 OK,并输出 body,且会报 attempt to set status 403 via ngx.exit after sending out the response status 200。
(3)ngx.status = 403 配合 ngx.say 同时调用,请求会在此结束(该阶段继续执行完毕),状态码为 ngx.status 指定的 code,后续阶段(如 access_by_lua)不会执行:
location = /hello {
rewrite_by_lua_block {
ngx.status = 403
ngx.say("rewrite_by_lua_block")
}
# access_by_lua_block 不会执行
access_by_lua_block {
ngx.say("access_by_lua_block")
return ngx.exit(ngx.OK)
}
}
实际返回 HTTP/1.1 403 Forbidden,body 为 rewrite_by_lua_block。
(4)不调用 ngx.exit 时,rewrite_by_lua_block 阶段代码会继续执行(两个 ngx.say 都会输出);调用 ngx.exit 后请求立刻终止,后续代码不执行。
(5)ngx.status = 403 时:
ngx.exit(ngx.OK)与ngx.exit(ngx.HTTP_OK)效果一样:状态码 403,输出 body。ngx.exit(403):状态码 403,输出 body。ngx.exit(401):状态码仍是 403,但会报attempt to set status 401 via ngx.exit after sending out the response status 403。
6 使用 redis 的常见错误
(1)attempt to send data on a closed socket
set_keepalive 类似 close,会把连接立刻丢回连接池;之后基于该对象的 get/set 等命令会直接报 closed 错误。正确做法是在调用完 redis 命令之后再放回连接池。
(2)API disabled in the context of log_by_lua
ngx.socket.tcp 不允许在 log_by_lua 阶段调用,因此该阶段无法使用 redis(依赖 TCP 的 mysql/memcache 同理)。ngx.socket.tcp 支持的上下文:rewrite_by_lua*、access_by_lua*、content_by_lua*、ngx.timer.*、ssl_certificate_by_lua*、ssl_session_fetch_by_lua*。
(3)attempt to yield across C-call boundary
redis 实例不允许在模块级别定义和使用,否则跨请求复用同一个实例存在 race condition。
(4)lua tcp socket read timed out
可能原因:
- redis 设置的 backlog 过小;
- 客户端连接 redis 的 timeout 太短,可通过
set_timeout适当增大。
参考:https://github.com/openresty/lua-nginx-module/issues/323
7 unpack 对 table 大小有限制
传一个 size 为 10 万的 table 给 unpack 会报错:too many results to unpack。
lua entry thread aborted: runtime error: ...: too many results to unpack
大数组请改用循环遍历,不要依赖 unpack。
8 ipairs 与 pairs
- 不能对 hash table 使用
#,只能遍历求大小。 ipairs用于 array-like 的 table,pairs用于 hash table,两者不要混用。
local hash_table = {
age = 18,
name = "jack",
city = "beijing"
}
local array_table = {
"hey",
"hello",
"hi"
}
9 init_worker_by_lua 和 log_by_lua 中的 ngx.ctx 相互独立
init_worker_by_lua_block {
ngx.ctx.hello = "world"
}
log_by_lua_block {
ngx.log(ngx.ERR, "hello ", ngx.ctx.hello) -- 输出为空,此时 ngx.ctx.hello 为 nil
}
两个阶段的 ngx.ctx 互不影响。
10 PCRE library does not support JIT
通过 ldd /usr/local/nginx/sbin/nginx | grep pcre 查看 PCRE 版本,通常是版本太低导致,安装新版本 PCRE 即可。
11 dns resolver 报 lua udp socket read timed out
原因:resolver 请求超时设置太短。例如设置 timeout 1000ms、retrans 2 次,第一次尝试就可能超时:
local primary, err = resolver:new{
nameservers = ,
retrans = 2, -- 2 retransmissions on receive timeout
timeout = 1000, -- 1 sec
}
解决:将 timeout 调大,例如 5000ms。
参考:https://groups.google.com/g/openresty-en
12 slt.lua 简单模板的变量格式
谷歌出品的 slt.lua 模板引擎使用变量时必须严格遵循格式,否则解析失败且不报错(排查困难):
#{= variable_name }
结束标识符默认是 ` }`(结束标签前有一个空格)。
13 Redis Cluster 问题排查
(1)failed to fetch slots / cluster.slots_info == 0
原因:redis cluster 集群没创建完全,检查集群状态:
redis-cli --cluster check 127.0.0.1:6305
# [ERR] Not all 16384 slots are covered by nodes.
解决:
- 方法一:
redis-cli --cluster fix 127.0.0.1:6305(有时无效) - 方法二:
redis-cli --cluster reshard 127.0.0.1:6305 - 方法三:停掉所有节点,重新创建 cluster
(2)failed to parse host name “”: no host
原因:redis cluster 出故障了。
解决:停掉所有节点,删除 node 配置文件,重启所有节点后重新创建集群:
rm -f cluster-*.node.conf
14 OpenResty 单元测试(Test::Nginx)要点
(1)正则匹配错误日志
--- error_log eval
[qr/PREV:bucket:\d+:\d+\.\d+\.\d+\.\d+:\d=\d, CUR:bucket:\d+:.*, RATE=1\+\(0\*.+\)=1\.000000/]
(2)BEGIN 块做测试前初始化
通过 shell 脚本做变量替换或调用外部命令(如写 redis 数据),两种方式:反引号或 system 函数。
use lib qw( t/lib );
my $SkipReason;
BEGIN {
if (system("redis-cli -p 6305 -h 127.0.0.1 set hello jack") != 0) {
$SkipReason = "redis cluster is bad";
}
`sed 's/port = 7305/port = 6305/g' -i /usr/local/nginx/lua/conf.lua`;
}
use Test::Nginx $SkipReason ? (skip_all => $SkipReason) : ('no_plan');
(3)启动 ut 时 nginx 卡住
原因:dofile 传入了 nil 参数。当传入 nil 时,dofile() 会从标准输入读取内容,导致卡住。
local function get_keys()
local ok, keys = pcall(dofile, conf.KEYS_PATH)
...
end
解决:给 dofile 一个兜底路径,使异常被 pcall 捕获:
pcall(dofile, conf.KEYS_PATH or "no_such_file_or_directory")
(4)attempt to send data on a closed socket(单测中偶发)
大概率是 set_timeout 设置的超时过短,将 1 秒改为 5 秒即可。
local red, err = resty_redis:new()
-- red:set_timeout(1000) 单位:毫秒
red:set_timeout(5 * 1000)
ok, err = red:connect(host)
if not ok then
ngx.log(ngx.ERR, "failed to connect redis ", host, ". err: ", err)
return nil
else
red:select(db)
end