《OpenResty Lua 编程最佳实践与问题排查》

《OpenResty Lua 编程最佳实践与问题排查》

本文汇总 OpenResty 中 Lua 编程的写法规范与线上常见问题。

在 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
阅读 — · 全站 —
🎸 我的歌单 0 首