本文是 OpenResty 的初学者指南,提供一些资料的汇总。
初学者在刚开始学习 OpenResty 的时候,肯定要搭建一个环境,通常来说,我们推荐直接使用官方提供的二进制包,比如 CentOS 的话,直接用 yum 安装即可,不过二进制包有一个限制是它的各种编译选项都是固定的,没办法修改,比如现在新版的二进制包缺省开启了 GC64,用来支持大内存,但是目前的火焰图工具并不支持 GC64,报错:
semantic error: unable to find member ‘ptr32’ for struct MRef (alternatives: ptr64)
此时要用二进制包的话,可以考虑安装旧版二进制包:「yum install openresty-1.13.6.2」,或者使用源代码安装,编译的时候激活「without-luajit-gc64」选项,此外,社区有相应的 PR 可供选择,在官方正式修复此问题前,可以试试。
学习 OpenResty 之前必然要了解 Lua,在线的免费资料推荐阅读:
- Lua 5.1 Reference Manual
- Programming in Lua (first edition)
- Frequently Asked Questions
- Lua Unofficial FAQ (uFAQ)
具体到 OpenResty 的话,推荐阅读 OpenResty 作者 agentzh 撰写的 Nginx 教程,有中文版和英文版,一旦对 Nginx 有了基本的认知,那么可以读十遍 lua-nginx-module 的官方文档,同时 iresty 上还提供了一份中文文档,其中有很多细节。
比如在描述 ngx.print 的时候,文档中提到:「This is an asynchronous call and will return immediately without waiting for all the data to be written into the system send buffer. To run in synchronous mode, call ngx.flush(true) after calling ngx.print.」,看上去很简单,无非是说 ngx.print 是异步的,不过如果你忽视了这一点,那么很可能会掉坑里:
我见过有人在热代码里执行 ngx.print,结果导致卡顿,究其原因,正是因为 ngx.print 是异步的,调用后直接返回,正确的做法是在适当的时候执行 ngx.flush(true)。
此外,在描述 ngx.say 的时候,文档中提到:「Just as ngx.print but also emit a trailing newline.」,看上去很简单,无非是说 ngx.say 比 ngx.print 多了一个新行,不过如果你忽视了这一点,那么很可能会掉坑里:
我见过有人输出了 Content-Length 响应头后,接着用 ngx.say 输出相应体,结果报错。究其原因,正是因为 ngx.say 多了一个新行,导致 Content-Length 不匹配。
类似的细节还有很多,比如:
- ngx.unescape_uri 解码时如果遇到非法数据会直接删除
- ngx.req.get_uri_args、ngx.req.get_post_args,ngx.req.get_headers、ngx.resp.get_headers、ngx.decode_args,这些函数的结果都是有长度限制的,可以通过返回值 err 是否等于 truncated 来判断。
- 如果用 lua-resty-redis 查询一个不存在的 key,那么返回的是 ngx.null,而不是 nil,这是因为 nil 在 lua 里有特殊的意义。
如果有使用方面的问题,多留意各种官方库的测试用例,比如你想看看如果使用 redis 的 pubsub 功能的话,可以参考对应的测试用例,还有一些开源的电子书值得推荐,比如:
- Programming OpenResty
- OpenResty 最佳实践
理论知识学习的差不多了之后,有时间的话推荐把讨论组(中文,英文)里的帖子从头到尾捋一遍,常见问题里面都有介绍,举例说明 cjson 的几个问题:
比如除了 cjson 模块还有一个 cjson.safe,二者的区别在于前者在编码解码出错的时候会抛出异常,此时需要通过 pcall 来处理,后者在编码解码出错的时候则是返回错误,一般来说我们不太喜欢在代码里使用 pcall,所以相对而言更推荐使用 cjson.safe。
再比如 cjson 模块有一个encode_sparse_array 方法,直接上代码看看它的作用吧:
代码语言:javascript复制shell> resty -e '
local cjson = require "cjson";
ngx.say(cjson.encode({[11]="x"})
)'
Cannot serialise table: excessively sparse array
shell> resty -e '
local cjson = require "cjson";
cjson.encode_sparse_array(true)
ngx.say(cjson.encode({[11]="x"})
)'
{"11":"x"}
再比如 openresty 版本的 cjson 有一个新方法 encode_empty_table_as_object,可以改变编码时的行为,具体点来说,空表会被编码成空的 json 对象,而不是空的 json 数组。
此外,火焰图值得特别关注,其又分为 On-CPU 和 Off-CPU,如何选择?
如果瓶颈是 CPU 则使用 On-CPU 火焰图,如果瓶颈是 IO 或锁则使用 Off-CPU 火焰图。如果无法确定,那么可以通过压测工具来判断:通过压测工具看看能否让 CPU 使用率趋于饱和,如果能那么使用 On-CPU 火焰图,如果不管怎么压,CPU 使用率始终上不来,那么多半说明程序被 IO 或锁卡住了,此时适合使用 Off-CPU 火焰图。如果还是确认不了,那么不妨 On-CPU 火焰图和 Off-CPU 火焰图都搞搞,正常情况下它们的差异会比较大,如果两张火焰图长得差不多,那么通常认为 CPU 被其它进程抢占了。 如果使用 On-CPU,压测工具把 CPU 压得越满结果越准确;如果使用 Off-CPU,则不必如此,毕竟在 Off-CPU 的时间段内,进程的用户态调用栈和内核调用栈都不会发生变化。 关于压测工具,如果使用 ab 的话,一定要记得开启 -k 选项,否则可能会遇到端口不足的问题。当然 wrk 也不错。
当你用 OpenResty 写项目的时候,最好站在巨人的肩膀上,多使用一些成熟的开源组件,不过需要注意有些 Lua 库可能并不兼容 OpenResty 的非堵塞特性,在你选择的时候务必留心,比如 LuaRocks 上的包,尤其是那些使用了 LuaSocket 而不是 CoSocket 的库,需要说明的是,并不是说 LuaRocks 上的包质量低下,相反,LuaRocks 上的包质量不错,只不过它的定位是整个 Lua 社区,而不是单独 OpenResty 社区,一个相对安全的选择是只在 opm 或者 awesome-resty 上找。
Github 上 lua-resty-* 相关的项目最好也都留意一下,特别是如下几个公司的账户:
- upyun
- kong
- cloudflare
赞扬下 upyun,作为国内技术流公司,对社区贡献良多。
此外,再推荐几个组织或个人的账户(排名不分先后):
- iresty:代表作 lua-resty-etcd 等
- timebug:代表作 lua-resty-redis-ratelimit 等
- tokers:代表作 lua-resty-http2 等
- huangnauh:代表作 lua-resty-consul 等
- doujiang24:代表作 lua-resty-kafka 等
- spacewander:代表作 lua-resty-rsa 等
- smallfish:代表作 lua-resty-beanstalkd 等
- bungle:代表作 lua-resty-template 等
- ledgetech:代表作 lua-resty-http 等
- thibaultcha:代表作 lua-resty-mlcache 等
如果你多看几个开源库的源代码的话,那么就会发现其中很多库都是借助 ffi 来实现的,通过它,我们不仅可以调用 c 模块,甚至可以调用 go 模块,如果想要成为高级开发者的话,必须了解 ffi,luapower 上有很多不错的例子,此外有一些文章可供参考:
- LuaJIT FFI 介绍,及其在 OpenResty 中的应用(上)
- LuaJIT FFI 介绍,及其在 OpenResty 中的应用(下)
如上几篇文章的作者都是 spacewander,他写过不少 Openresty 方面的好东西:
- OpenResty单元测试实践
- 在 OpenResty 中使用正则
- 如何编写正确且高效的 OpenResty 应用
- 在 OpenResty 里实现进程间通讯
很多开源项目也会分享直接开发 OpenResty 的经验,比如 APISIX:
- APISIX 的高性能实践
- 再谈 APISIX 高性能实践
不多说了,撸起袖子干吧。