OpenResty的学习

参考https://time.geekbang.org/column/article/98660 极客时间学习。

1.安装碰到问题

1.1 无工具opm问题 ./bash opm**

使用了网上的cd /usr/local/openresty/bin
sudo ln -s pwd/opm /usr/local/bin/opm
命令还是无效
最后# 如果没有安装 wget 需要先 yum install wget
wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -ivh epel-release-latest-7.noarch.rpm
我的版本是
参考:https://www.cnblogs.com/zhuminghui/p/12102122.html

1.2.在学到碰到权限问题:

OpenResty Lua Permission Denied && nginx 配置user
在这里插入图片描述
在这里插入图片描述
看到这个一直以为路径错了后面想起看了error.log日志报权限问题虽然给文件都授权了chmod -R 777 * 最高权限了但还是报错了。最后在网上看到了一篇文章https://blog.csdn.net/fy_sun123/article/details/79209690
才知道默认是nobody的用户组

  1. 查看nginx的user 。 注意是worker的 默认是nobody

ps aux|grep nginx|grep -v grep
2. 查看要操作的路径用户归属
ls -ld
记得修改
3. 配置nginx conf !!!
user yourname group; # 2中查看的name 和 group后面要封号否则无效。
在这里插入图片描述
2.Lua查找路径:

在这里插入图片描述

3.只有 table 这一种数据结构

出自:温铭
不同于 Python 这种内置数据结构丰富的语言,Lua 中只有一种数据结构,那就是 table,它里面可以包括数组和哈希表

local color = {first = "red", "blue", third = "green", "yellow"}print(color["first"]) --> output: redprint(color[1]) --> output: blueprint(color["third"]) --> output: greenprint(color[2]) --> output: yellowprint(color[3]) --> output: nil```
如果不显式地用_键值对_的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 color[1] 就是 blue。另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:

```bash
local t1 = { 1, 2, 3 }print("Test1 " .. table.getn(t1))local t2 = { 1, a = 2, 3 }print("Test2 " .. table.getn(t2))local t3 = { 1, nil }print("Test3 " .. table.getn(t3))local t4 = { 1, nil, 2 }print("Test4 " .. table.getn(t4))

使用 resty 运行的结果如下:Test1 3Test2 2Test3 1Test4 1你可以看到,除了第一个返回长度为 3 的测试案例外,后面的测试都是我们预期之外的结果。事实上,想要在 Lua 中获取 table 长度,必须注意到,只有在 table 是 序列 的时候,才能返回正确的值。那什么是序列呢?首先序列是数组(array)的子集,也就是说,table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。
10 | JIT编译器的死穴:为什么要避免使用 NYI ?
温铭 2019-06-17

4.什么是 NYI?

那究竟什么是 NYI 呢?先回顾下我们之前提到过的一个知识点:
LuaJIT 的运行时环境,除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。
LuaJIT 中 JIT 编译器的实现还不完善,有一些原语它还无法编译,因为这些原语实现起来比较困难,再加上 LuaJIT 的作者目前处于半退休状态。这些原语包括常见的 pairs() 函数、unpack() 函数、基于 Lua CFunction 实现的 Lua C 模块等。这样一来,当 JIT 编译器在当前代码路径上遇到它不支持的操作时,便会退回到解释器模式。
而 JIT 编译器不支持的这些原语,其实就是我们今天要讲的 NYI,全称为 Not Yet Implemented。LuaJIT 的官网上有这些 NYI 的完整列表,建议你仔细浏览一遍。当然,目的不是让你背下这个列表的内容,而是让你要在写代码的时候有意识地提醒自己。
下面,我截取了 NYI 列表中 string 库的几个函数:

其中,string.byte 对应的能否被编译的状态是 yes,表明可以被 JIT,你可以放心大胆地在代码中使用。
string.char 对应的编译状态是 2.1,表明从 LuaJIT 2.1 开始支持。我们知道,OpenResty 中的 LuaJIT 是基于 LuaJIT 2.1 的,所以你也可以放心使用。
string.dump 对应的编译状态是 never,即不会被 JIT,会退回到解释器模式。目前来看,未来也没有计划支持这个原语。
string.find 对应的编译状态是 2.1 partial,意思是从 LuaJIT 2.1 开始部分支持,后面的备注中写的是 只支持搜索固定的字符串,不支持模式匹配。所以对于固定字符串的查找,你使用 string.find 是可以被 JIT 的。
我们自然应该避免使用 NYI,让更多的代码可以被 JIT 编译,这样性能才能得到保证。但在现实环境中,我们有时候不可避免要用到一些 NYI 函数的功能,这时又该怎么办呢?
NYI 的替代方案
其实,不用担心,大部分 NYI 函数我们都可以敬而远之,通过其他方式来实现它们的功能。接下来,我挑选了几个典型的 NYI 来讲解,带你了解不同类型的 NYI 替代方案。这样,其他的 NYI 你也可以自己触类旁通。
1.string.gsub() 函数
第一个我们来看 string.gsub() 函数。它是 Lua 内置的字符串操作函数,作用是做全局的字符串替换,比如下面这个例子:
$ resty -e ‘local new = string.gsub(“banana”, “a”, “A”); print(new)’
bAnAnA
这个函数是一个 NYI 原语,无法被 JIT 编译。
我们可以尝试在 OpenResty 自己的 API 中寻找替代函数,但对于大多数人来说,记住所有的 API 和用法是不现实的。所以在平时开发中,我都会打开 lua-nginx-module 的 GitHub 文档页面。
比如,针对刚刚的这个例子,我们可以用 gsub 作为关键字,在文档页面中搜索,这时ngx.re.gsub 就会映入眼帘。
细心的同学可能会问,这里为什么不用之前推荐的 restydoc 工具,来搜索 OpenResty API 呢?你可以尝试下用它来搜索 gsub:
$ restydoc -s gsub
看到了吧,这里并没有返回我们期望的 ngx.re.gsub,而是显示了 Lua 自带的函数。事实上,现阶段而言, restydoc 返回的是唯一的精准匹配的结果,所以它更适合在你明确知道 API 名字的前提下使用。至于模糊的搜索,还是要自己手动在文档中进行。
回到刚刚的搜索结果,我们看到,ngx.re.gsub 的函数定义如下:
newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)
这里,函数参数和返回值的命名都带有具体的含义。其实,在 OpenResty 中,我并不推荐你写很多注释,大多数时候,一个好的命名胜过好几行注释。
对于不熟悉 OpenResty 正则体系的工程师而言,看到最后的变参 options ,你可能会比较困惑。不过,这个变参的解释,并不在此函数中,而是在 ngx.re.match 函数的文档中。
通过查看参数 options 的文档,你会发现,只要我们把它设置为 jo,就开启了 PCRE 的 JIT。这样,使用 ngx.re.gsub 的代码,既可以被 LuaJIT 进行 JIT 编译,也可以被 PCRE JIT 进行 JIT 编译。
具体的文档内容,我就不再赘述了。不过这里我想强调一点——在翻看文档时,我们一定要有打破砂锅问到底的精神。OpenResty 的文档其实非常完善,仔细阅读文档,就可以解决你大部分的问题。
2.string.find() 函数
和 string.gsub 不同的是,string.find 在 plain 模式(即固定字符串的查找)下,是可以被 JIT 的;而带有正则这种的字符串查找,string.find 并不能被 JIT ,这时就要换用 OpenResty 自己的 API,也就是 ngx.re.find 来完成。
所以,当你在 OpenResty 中做字符串查找时,首先一定要明确区分,你要查找的是固定的字符串,还是正则表达式。如果是前者,就要用 string.find,并且记得把最后的 plain 设置为 true:
string.find(“foo bar”, “foo”, 1, true)
如果是后者,你应该用 OpenResty 自己的 API,并开启 PCRE 的 JIT 选项:
ngx.re.find(“foo bar”, “^foo”, “jo”)
其实,这里更适合做一层封装,并把优化选项默认打开,不要让最终的使用者知道这么多细节。这样,对外就是统一的字符串查找函数了。你可以感受到,有时候选择太多、太灵活并不是一件好事。
3.unpack() 函数
第三个我们来看 unpack() 函数。unpack() 也是要避免使用的函数,特别是不要在循环体中使用。你可以改用数组的下标去访问,比如下面代码的这个例子:
$ resty -e ’
local a = {100, 200, 300, 400}
for i = 1, 2 do
print(unpack(a))
end’
$ resty -e ‘local a = {100, 200, 300, 400}
for i = 1, 2 do
print(a[1], a[2], a[3], a[4])
end’
让我们再深究一下 unpack,这次我们可以用restydoc 来搜索一下:
$ restydoc -s unpack
从 unpack 的文档中,你可以看出,unpack (list [, i [, j]]) 和 return list[i], list[i+1], , list[j] 是等价的,你可以把 unpack 看成一个语法糖。这样,你完全可以用数组下标的方式来访问,以免打断 LuaJIT 的 JIT 编译。
4.pairs() 函数
最后我们来看遍历哈希表的 pairs() 函数,它也不能被 JIT 编译。
不过非常遗憾,这个并没有等价的替代方案,你只能尽量避免使用,或者改用数字下标访问的数组,特别是在热代码路径上不要遍历哈希表。这里我解释一下代码热路径,它的意思是,这段代码会被返回执行很多次,比如在一个很大的循环里面。
说完这四个例子,我们来总结一下,要想规避 NYI 原语的使用,你需要注意下面这两点:
请优先使用 OpenResty 提供的 API,而不是 Lua 的标准库函数。这里要牢记, Lua 是嵌入式语言,我们实际上是在 OpenResty 中编程,而不是 Lua。
如果万不得已要使用 NYI 原语,请一定确保它没有在代码热路径上。
如何检测 NYI?
讲了这么多 NYI 的规避方案,都是在教你该怎么做。不过,如果到这里戛然而止,那就不太符合 OpenResty 奉行的一个哲学:
能让机器自动完成的,就不要人工参与。
人不是机器,总会有疏漏,能够自动化地检测代码中使用到的 NYI,才是工程师价值的一个重要体现。
这里我推荐,LuaJIT 自带的 jit.dump 和 jit.v 模块。它们都可以打印出 JIT 编译器工作的过程。前者会输出非常详细的信息,可以用来调试 LuaJIT 本身,你可以参考它的源码来做更深入的了解;后者的输出比较简单,每行对应一个 trace,通常用来检测是否可以被 JIT。
具体应该怎么操作呢?
我们可以先在 init_by_lua 中,添加以下两行代码:
local v = require “jit.v”
v.on("/tmp/jit.log")
然后,运行你自己的压力测试工具,或者跑几百个单元测试集,让 LuaJIT 足够热,触发 JIT 编译。这些都完成后,再来检查 /tmp/jit.log 的结果。
当然,这个方法相对比较繁琐,如果你想要简单验证的话, 使用 resty 就足够了,这个 OpenResty 的 CLI 带有相关选项:
$resty -j v -e ‘for i=1, 1000 do
local newstr, n, err = ngx.re.gsub(“hello, world”, “([a-z])[a-z]+”, “[$0,$1]”, “i”)
end’
[TRACE 1 (command line -e):1 stitch C:107bc91fd]
[TRACE 2 (1/stitch) (command line -e):2 -> 1]
其中,resty 的 -j 就是和 LuaJIT 相关的选项;后面的值为 dump 和 v,就对应着开启 jit.dump 和 jit.v 模式。
在 jit.v 模块的输出中,每一行都是一个成功编译的 trace 对象。刚刚是一个能够被 JIT 的例子,而如果遇到 NYI 原语,输出里面就会指明 NYI,比如下面这个 pairs 的例子:
$resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end

for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do

end
end
end’
它就不能被 JIT,所以结果里,指明了第 8 行中有 NYI 原语。
[TRACE 1 (command line -e):2 loop]
[TRACE — (command line -e):7 – NYI: bytecode 72 at (command line -e):8]
写在最后
这是我们第一次用比较多的篇幅来谈及 OpenResty 的性能问题。看完这些关于 NYI 的优化,不知道你有什么感想呢?可以留言说说你的看法。
最后,给你留一道思考题。在讲 string.find() 函数的替代方案时,我有提到过,那里其实更适合做一层封装,并默认打开优化选项。那么,这个任务就交给你来小试牛刀了。
欢迎在留言区写下你的答案,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。

5.|剖析Lua唯一的数据结构table和metatable特性

出自温铭 来自https://zjj1994.blog.csdn.net/article/details/117844883
今天我们一起学习下 LuaJIT 中唯一的数据结构:table。
和其他具有丰富数据结构的脚本语言不同,LuaJIT 中只有 table 这一个数据结构,并没有区分开数组、哈希、集合等概念,而是揉在了一起。让我们先温习下之前提到过的一个例子:
local color = {first = “red”, “blue”, third = “green”, “yellow”}
print(color[“first”]) --> output: red
print(color[1]) --> output: blue
print(color[“third”]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
这个例子中, color 这个 table 包含了数组和哈希,并且可以互不干扰地进行访问。比如,你可以用 ipairs 函数,只遍历数组部分的内容:
$ resty -e 'local color = {first = “red”, “blue”, third = “green”, “yellow”}
for k, v in ipairs(color) do
print(k)
end

table 的操作是如此重要,以至于 LuaJIT 对标准 Lua 5.1 的 table 库做了扩展,而 OpenResty 又对 LuaJIT 的 table 库做了更进一步的扩展。下面,我们就一起来分别看下这些库函数。
table 库函数
先来看标准 table 库函数。Lua 5.1 中自带的 table 库函数并不多,我们可以大概浏览一遍。
table.getn 获取元素个数
我们在 标准 Lua 和 LuaJIT 章节中曾经提到过,想正确地获取到 table 所有元素的个数,在 LuaJIT 中是一个老大难问题。
对于序列,你用table.getn 或者一元操作符 # ,就可以正确返回元素的个数。比如下面这个例子,就会返回我们预期中的 3。
$ resty -e 'local t = { 1, 2, 3 }
print(table.getn(t)) ’
而对于不是序列的 table,就无法返回正确的值。比如第二个例子,返回的就是 1。
$ resty -e 'local t = { 1, a = 2 }
print(#t) ’
不过,幸运的是,这种难以理解的函数,已经被 LuaJIT 的扩展替代,后面我们会提到。所以在 OpenResty 的环境下,除非你明确知道,你正在获取序列的长度,否则请不要使用函数 table.getn 和一元操作符 # 。
另外,table.getn 和一元操作符 # 并不是 O(1) 的时间复杂度,而是 O(n),这也是尽量避免使用它们的另外一个理由。
table.remove 删除指定元素
第二个我们来看table.remove 函数,它的作用是在 table 中根据下标来删除元素,也就是说只能删除 table 中数组部分的元素。我们还是来看color的例子:
$ resty -e ‘local color = {first = “red”, “blue”, third = “green”, “yellow”}
table.remove(color, 1)
for k, v in pairs(color) do
print(v)
end’
这段代码会把下标为 1 的 blue 删除掉。你可能会问,那该如何删除 table 中的哈希部分呢?也很简单,把 key 对应的 value 设置为 nil 即可。这样,color这个例子中,third 对应的green就被删除了。
$ resty -e ‘local color = {first = “red”, “blue”, third = “green”, “yellow”}
color.third = nil
for k, v in pairs(color) do
print(v)
end’
table.concat 元素拼接函数
第三个我们来看table.concat 元素拼接函数。它可以按照下标,把 table 中的元素拼接起来。既然这里又是根据下标来操作的,那么显然还是针对 table 的数组部分。同样还是color这个例子:
$ resty -e ‘local color = {first = “red”, “blue”, third = “green”, “yellow”}
print(table.concat(color, ", "))’
使用table.concat函数后,它输出的是 blue, yellow,哈希的部分被跳过了。
另外,这个函数还可以指定下标的起始位置来做拼接,比如下面这样的写法:
$ resty -e ‘local color = {first = “red”, “blue”, third = “green”, “yellow”, “orange”}
print(table.concat(color, ", ", 2, 3))’
这次输出是 yellow, orange,跳过了 blue。
你可能觉得这些操作还挺简单的,不过,我要说的是,函数不可貌相,海水不可。千万不要小看这个看上去没有太大用处的函数,在做性能优化时,它却会有意想不到的作用,也是我们后面性能优化章节中的主角之一。
table.insert 插入一个元素
最后我们来看table.insert 函数。它可以下标插入一个新的元素,自然,影响的还是 table 的数组部分。还是用color例子来说明:
$ resty -e 'local color = {first = “red”, “blue”, third = “green”, “yellow”}
table.insert(color, 1, “orange”)
print(color[1])

你可以看到, color 的第一个元素变为了 orange。当然,你也可以不指定下标,这样就会默认插入队尾。
这里我必须说明的是,table.insert 虽然是一个很常见的操作,但性能并不乐观。如果你不是根据指定下标来插入元素,那么每次都需要调用 LuaJIT 的 lj_tab_len 来获取数组的长度,以便插入队尾。正如我们在 table.getn 中提到的,获取 table 长度的时间复杂度为 O(n) 。
所以,对于table.insert 操作,我们应该尽量避免在热代码中使用,比如:
local t = {}
for i = 1, 10000 do
table.insert(t, i)
end
LuaJIT 的 table 扩展函数
接下来我们来看 LuaJIT 的 table 扩展函数。LuaJIT 在标准 Lua 的基础上,扩展了两个很有用的 table 函数,分别用来新建和清空一个 table,下面我具体来介绍一下。
table.new(narray, nhash) 新建 table
第一个是table.new(narray, nhash) 函数。这个函数,会预先分配好指定的数组和哈希的空间大小,而不是在插入元素时自增长,这也是它的两个参数 narray 和 nhash 的含义。自增长是一个代价比较高的操作,会涉及到空间分配、resize 和 rehash 等,我们应该尽量避免。
这里注意,table.new 的文档并没有出现在 LuaJIT 的官网,而是深藏在 GitHub 项目的扩展文档中,即使你用谷歌也难觅其踪迹,所以知道的工程师并不多。
下面是一个简单的例子,我来带你看下它该怎么用。首先要说明,这个函数是扩展出来的,所以在使用它之前,你需要先 require 一下:
local new_tab = require “table.new”
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end
你可以看到,这段代码新建了一个 table,里面包含 100 个数组元素和 0 个哈希元素。当然,你也可以根据实际需要,新建一个同时包含 100 个数组元素和 50 个 哈希元素的 table,这都是合法的:
local t = new_tab(100, 50)
另外,超出预设的空间大小,也可以正常使用,只不过性能会退化,也就失去了使用 table.new 的意义。
比如下面这个例子,我们预设大小为 100,而实际上却使用了 200:
local new_tab = require “table.new”
local t = new_tab(100, 0)
for i = 1, 200 do
t[i] = i
end
所以,你需要根据实际场景,来预设好 table.new 中数组和哈希空间的大小,这样才能在性能和内存占用上找到一个平衡点。
table.clear() 清空 table
第二个我们来看清空函数table.clear() 。它用来清空某个 table 里的所有数据,但并不会释放数组和哈希部分占用的内存。所以,它在循环利用 Lua table 时非常有用,可以避免反复创建和销毁 table 的开销。
$ resty -e ‘local clear_tab =require “table.clear”
local color = {first = “red”, “blue”, third = “green”, “yellow”}
clear_tab(color)
for k, v in pairs(color) do
print(k)
end’
不过,事实上,能使用这个函数的场景并不算多,大多数情况下,我们还是应该把这个任务交给 LuaJIT GC 去完成。
OpenResty 的 table 扩展函数
开头我提到过,OpenResty 自己维护的 LuaJIT 分支,也对 table 做了扩展,它新增了几个 API:table.isempty、table.isarray、 table.nkeys 和 table.clone。
需要注意的是,在使用这几个新增的 API 前,请记住检查你使用的 OpenResty 的版本,这些 API 大都只能在 OpenResty 1.15.8.1 之后的版本中使用。这是因为, OpenResty 在 1.15.8.1 版本之前,已经有一年左右没有发布新版本了,而这些 API 是在这个发布间隔中新增的。
文章中我已经附上了链接,这里我就只用 table.nkeys 来举例说明下,其他的三个 API 从命名上来说都非常容易理解,你自己翻阅 GitHub 上的文档就可以明白了。不得不说,OpenResty 的文档质量非常高,其中包含了代码示例、能否被 JIT、需要注意的事项等,比起 Lua 和 LuaJIT 的文档,着实高了好几个数量级。
好的,回到table.nkeys函数上,它的命名可能会让你迷惑,不过,它实际上是获取 table 长度的函数,返回的是 table 的元素个数,包括数组和哈希部分的元素。因此,我们可以用它来替代 table.getn,比如下面这样来用:
local nkeys = require “table.nkeys”
print(nkeys({})) – 0
print(nkeys({ “a”, nil, “b” })) – 2
print(nkeys({ dog = 3, cat = 4, bird = nil })) – 2
print(nkeys({ “a”, dog = 3, cat = 4 })) – 3
元表
讲完了 table 函数,我们再来看下由 table 引申出来的 元表(metatable)。元表是 Lua 中独有的概念,在实际项目中的使用非常广泛。不夸张地说,在几乎所有的 lua-resty-* 库中,你都能看到它的身影。
元表的表现行为类似于操作符重载,比如我们可以重载 __add,来计算两个 Lua 数组的并集;或者重载 __tostring,来定义转换为字符串的函数。
而 Lua 提供了两个处理元表的函数:
第一个是setmetatable(table, metatable), 用于为一个 table 设置元表;
第二个是getmetatable(table),用于获取 table 的元表。
介绍了这么半天,你可能更关心它的作用,我们接着就来看下元表具体有什么用处。下面是一段真实项目里的代码:
$ resty -e ’ local version = {
major = 1,
minor = 1,
patch = 1
}
version = setmetatable(version, {
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))

我们首先定义了一个 名为 version的 table ,你可以看到,这段代码的目的,是想把 version 中的版本号打印出来。但是,我们并不能直接打印 version,你可以试着操作一下,就会发现,直接打印的话,只会输出这个 table 的地址。
print(tostring(version))
所以,我们需要自定义这个 table 的字符串转换函数,也就是 __tostring,到这一步也就是元表的用武之地了。我们用 setmetatable ,重新设置 version 这个 table 的 __tostring 方法,就可以打印出版本号: 1.1.1。
其实,除了 __tostring 之外,在实际项目中,我们还经常重载元表中的以下两个元方法(metamethod)。
其中一个是__index。我们在 table 中查找一个元素时,首先会直接从 table 中查询,如果没有找到,就继续到元表的 __index 中查询。
比如下面这个例子,我们把 patch 从 version 这个 table 中去掉:
$ resty -e ’ local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = function(t, key)
if key == “patch” then
return 2
end
end,
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))

这样的话,t.patch 其实获取不到值,那么就会走到 __index 这个函数中,结果就会打印出 1.1.2。
事实上,__index 不仅可以是一个函数,也可以是一个 table。你试着运行下面这段代码,就会看到,它们实现的效果是一样的。
$ resty -e ’ local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = {patch = 2},
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))

另一个元方法则是__call。它类似于仿函数,可以让 table 被调用。
我们还是基于上面打印版本号的代码来做修改,看看如何调用一个 table:
$ resty -e ’
local version = {
major = 1,
minor = 1,
patch = 1
}
local function print_version(t)
print(string.format("%d.%d.%d", t.major, t.minor, t.patch))
end
version = setmetatable(version,
{__call = print_version})
version()

这段代码中,我们使用 setmetatable,给 version 这个 table 增加了元表,而里面的 __call 元方法指向了函数 print_version 。那么,如果我们尝试把 version 当作函数调用,这里就会执行函数 print_version。
而 getmetatable 是和 setmetatable 配对的操作,可以获取到已经设置的元表,比如下面这段代码:
$ resty -e ’ local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = {patch = 2},
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(getmetatable(version).__index.patch)

自然,除了今天讲到的这三个元方法外,还有一些不经常使用的元方法,你可以在遇到的时候再去查阅文档了解。
面向对象
最后我们来聊聊面向对象。你可能知道,Lua 并不是一个面向对象(Object Orientation)的语言,但我们可以使用 metatable 来实现 OO。
我们来看一个实际的例子。lua-resty-mysql 是 OpenResty 官方的 MySQL 客户端,里面就使用元表模拟了类和类方法,它的使用方式如下所示:
$ resty -e ‘local mysql = require “resty.mysql” – 先引用 lua-resty 库
local db, err = mysql:new() – 新建一个类的实例
db:set_timeout(1000) – 调用类的方法’
你可以直接用 resty 命令行来执行上述代码。这几行代码很好理解,唯一可能给你造成困扰的是:
在调用类方法的时候,为什么是冒号而不是点号呢?
其实,在这里冒号和点号都是可以的,db:set_timeout(1000) 和 db.set_timeout(db, 1000) 是完全等价的。冒号是 Lua 中的一个语法糖,可以省略掉函数的第一个参数 self。
众所周知,源码面前没有秘密,让我们来看看上述几行代码所对应的具体实现,以便你更好理解,如何用元表来模拟面向对象:
local _M = { _VERSION = ‘0.21’ } – 使用 table 模拟类
local mt = { __index = _M } – mt 即 metatable 的缩写,__index 指向类自身
– 类的构造函数
function _M.new(self)
local sock, err = tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock }, mt) – 使用 table 和 metatable 模拟类的实例
end

– 类的成员函数
function _M.set_timeout(self, timeout) – 使用 self 参数,获取要操作的类的实例
local sock = self.sock
if not sock then
return nil, “not initialized”
end
return sock:settimeout(timeout)
end
你可以看到,_M 这个 table 模拟了一个类,初始化时,它只有 _VERSION 这一个成员变量,并在随后定义了 _M.set_timeout 等成员函数。在 _M.new(self) 这个构造函数中,我们返回了一个 table,这个 table 的元表就是 mt,而 mt 的 __index 元方法指向了 _M,这样,返回的这个 table 就模拟了类 _M 的实例。
写在最后
好的,到这里,今天的主要内容就结束了。事实上,table 和 metatable 会大量地用在 OpenResty 的 lua-resty-* 库以及基于 OpenResty 的开源项目中,我希望通过这节课的学习,可以让你更容易地读懂这些源代码。
自然,除了 table 外,Lua 中还有其他一些常用的函数,我们下节课再一起来学习。
最后,我想给你留一个思考题。为什么 lua-resty-mysql 库要模拟 OO 来做一层封装呢?欢迎在留言区一起讨论这个问题,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
将学到的知识总结成笔记,方便日后快速查找及复习

6 | 高手秘诀:识别Lua的独有概念和坑

出自:温铭
上一节中,我们一起了解了 LuaJIT 中 table 相关的库函数。除了这些常用的函数外,今天我再为你介绍一些 Lua 独有的或不太常用的概念,以及 OpenResty 中常见的 Lua 的坑。
弱表
首先是 弱表(weak table),它是 Lua 中很独特的一个概念,和垃圾回收相关。和其他高级语言一样,Lua 是自动垃圾回收的,你不用关心具体的实现,也不用显式 GC。没有被引用到的空间,会被垃圾收集器自动完成回收。
但简单的引用计数还不太够用,有时候我们需要一种更灵活的机制。举个例子,我们把一个 Lua 的对象 Foo(table 或者函数)插入到 table tb 中,这就会产生对这个对象 Foo 的引用。即使没有其他地方引用 Foo,tb 对它的引用也还一直存在,那么 GC 就没有办法回收 Foo 所占用的内存。这时候,我们就只有两种选择:
一是手工释放 Foo;
二是让它常驻内存。
比如下面这段代码:
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print(“func”) end
print(#tb) – 2
collectgarbage()
print(#tb) – 2
table.remove(tb, 1)
print(#tb) – 1
不过,你肯定不希望,内存一直被用不到的对象占用着吧,特别是 LuaJIT 中还有 2G 内存的上限。而手工释放的时机并不好把握,也会增加代码的复杂度。
那么这时候,就轮到弱表来大显身手了。看它的名字,弱表,首先它是一个表,然后这个表里面的所有元素都是弱引用。概念总是抽象的,让我们先来看一段稍加修改后的代码:
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print(“func”) end
setmetatable(tb, {__mode = “v”})
print(#tb) – 2
collectgarbage()
print(#tb) – 0

可以看到,没有被使用的对象都被 GC 了。这其中,最重要的就是下面这一行代码:
setmetatable(tb, {__mode = “v”})
是不是似曾相识?这不就是元表的操作吗!没错,当一个 table 的元表中存在 __mode 字段时,这个 table 就是弱表(weak table)了。
如果 __mode 的值是 k,那就意味着这个 table 的 键 是弱引用。
如果 __mode 的值是 v,那就意味着这个 table 的 值 是弱引用。
当然,你也可以设置为 kv,表明这个表的键和值都是弱引用。
这三者中的任意一种弱表,只要它的 键 或者 值 被回收了,那么对应的整个键值 对象都会被回收。
在上面的代码示例中,__mode 的值 v,而tb 是一个数组,数组的 value 则是 table 和函数对象,所以可以被自动回收。不过,如果你把__mode 的值改为 k,就不会 GC 了,比如看下面这段代码:
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print(“func”) end
setmetatable(tb, {__mode = “k”})
print(#tb) – 2
collectgarbage()
print(#tb) – 2

请注意,这里我们只演示了 value 为弱引用的弱表,也就是数组类型的弱表。自然,你同样可以把对象作为 key,来构建哈希表类型的弱表,比如下面这样写:
$ resty -e 'local tb = {}
tb[{color = red}] = “red”
local fc = function() print(“func”) end
tb[fc] = “func”
fc = nil
setmetatable(tb, {__mode = “k”})
for k,v in pairs(tb) do
print(v)
end
collectgarbage()
print("----------")
for k,v in pairs(tb) do
print(v)
end

在手动调用 collectgarbage() 进行强制 GC 后,tb 整个 table 里面的元素,就已经全部被回收了。当然,在实际的代码中,我们大可不必手动调用 collectgarbage(),它会在后台自动运行,无须我们担心。
不过,既然提到了 collectgarbage() 这个函数,我就再多说几句。这个函数其实可以传入多个不同的选项,且默认是 collect,即完整的 GC。另一个比较有用的是 count,它可以返回 Lua 占用的内存空间大小。这个统计数据很有用,可以让你看出是否存在内存泄漏,也可以提醒我们不要接近 2G 的上限值。
弱表相关的代码,在实际应用中会写得比较复杂,不太容易理解,相对应的,也会隐藏更多的 bug。具体有哪些呢?不必着急,后面内容,我会专门介绍一个开源项目中,使用弱表带来的内存泄漏问题。
闭包和 upvalue
再来看闭包和 upvalue。前面我强调过,在 Lua 中,所有的值都是一等公民,包含函数也是。这就意味着函数可以保存在变量中,当作参数传递,以及作为另一个函数的返回值。比如在上面弱表中出现的这段示例代码:
tb[2] = function() print(“func”) end
其实就是把一个匿名函数,作为 table 的值给存储了起来。
在 Lua 中,下面这段代码中动两个函数的定义是完全等价的。不过注意,后者是把函数赋值给一个变量,这也是我们经常会用到的一种方式:
local function foo() print(“foo”) end
local foo = fuction() print(“foo”) end
另外,Lua 支持把一个函数写在另外一个函数里面,即嵌套函数,比如下面的示例代码:
$ resty -e ’
local function foo()
local i = 1
local function bar()
i = i + 1
print(i)
end
return bar
end
local fn = foo()
print(fn()) – 2

你可以看到, bar 这个函数可以读取函数 foo 里面的局部变量 i,并修改它的值,即使这个变量并不在 bar 里面定义。这个特性叫做词法作用域(lexical scoping)。
事实上,Lua 的这些特性正是闭包的基础。所谓闭包 ,简单地理解,它其实是一个函数,不过它访问了另外一个函数词法作用域中的变量。
如果按照闭包的定义来看,Lua 的所有函数实际上都是闭包,即使你没有嵌套。这是因为 Lua 编译器会把 Lua 脚本外面,再包装一层主函数。比如下面这几行简单的代码段:
local foo, bar
local function fn()
foo = 1
bar = 2
end
在编译后,就会变为下面的样子:
function main(…)
local foo, bar
local function fn()
foo = 1
bar = 2
end
end
而函数 fn 捕获了主函数的两个局部变量,因此也是闭包。
当然,我们知道,很多语言中都有闭包的概念,它并非 Lua 独有,你也可以对比着来加深理解。只有理解了闭包,你才能明白我们接下来要讲的 upvalue。
upvalue 就是 Lua 中独有的概念了。从字面意思来看,可以翻译成 上面的值。实际上,upvalue 就是闭包中捕获的自己词法作用域外的那个变量。还是继续看上面那段代码:
local foo, bar
local function fn()
foo = 1
bar = 2
end
你可以看到,函数 fn 捕获了两个不在自己词法作用域的局部变量 foo 和 bar,而这两个变量,实际上就是函数 fn 的 upvalue。
常见的坑
介绍了 Lua 中的几个概念后,我再来说说,在 OpenResty 开发中遇到的那些和 Lua 相关的坑。
在前面内容中,我们提到了一些 Lua 和其他开发语言不同的点,比如下标从 1 开始、默认全局变量等等。在 OpenResty 实际的代码开发中,我们还会遇到更多和 Lua、 LuaJIT 相关的问题点, 下面我会讲其中一些比较常见的。
这里要先提醒一下,即使你知道了所有的 坑,但不可避免的,估计还是要自己踩过之后才能印象深刻。当然,不同的是,你能够更块地从坑里面爬出来,并找到症结所在。
下标从 0 开始还是从 1 开始
第一个坑,Lua 的下标是从 1 开始的,这点我们之前反复提及过。但我不得不说,这并非事实的全部。
因为在 LuaJIT 中,使用 ffi.new 创建的数组,下标又是从 0 开始的:
local buf = ffi_new(“char[?]”, 128)
所以,如果你要访问上面这段代码中 buf 这个 cdata,请记得下标从 0 开始,而不是 1。在使用 FFI 和 C 交互的时候,一定要特别注意这个地方。
正则模式匹配
第二个坑,正则模式匹配问题。OpenResty 中并行着两套字符串匹配方法:Lua 自带的 sting 库,以及 OpenResty 提供的 ngx.re.* API。
其中, Lua 正则模式匹配是自己独有的格式,和 PCRE 的写法不同。下面是一个简单的示例:
resty -e ‘print(string.match(“foo 123 bar”, “%d%d%d”))’ — 123
这段代码从字符串中提取了数字部分,你会发现,它和我们的熟悉的正则表达式完全不同。Lua 自带的正则匹配库,不仅代码维护成本高,而且性能低——不能被 JIT,而且被编译过一次的模式也不会被缓存。
所以,在你使用 Lua 内置的 string 库去做 find、match 等操作时,如果有类似正则这样的需求,不用犹豫,请直接使用 OpenResty 提供的 ngx.re 来替代。只有在查找固定字符串的时候,我们才考虑使用 plain 模式来调用 string 库。
这里我有一个建议:在 OpenResty 中,我们总是优先使用 OpenResty 的 API,然后是 LuaJIT 的 API,使用 Lua 库则需要慎之又慎。
json 编码时无法区分 array 和 dict
第三个坑,json 编码时无法区分 array 和 dict。由于 Lua 中只有 table 这一个数据结构,所以在 json 对空 table 编码的时候,自然就无法确定编码为数组还是字典:
resty -e 'local cjson = require “cjson”
local t = {}
print(cjson.encode(t))

比如上面这段代码,它的输出是 {},由此可见, OpenResty 的 cjson 库,默认把空 table 当做字典来编码。当然,我们可以通过 encode_empty_table_as_object 这个函数,来修改这个全局的默认值:
resty -e 'local cjson = require “cjson”
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))

这次,空 table 就被编码为了数组:[]。
不过,全局这种设置的影响面比较大,那能不能指定某个 table 的编码规则呢?答案自然是可以的,我们有两种方法可以做到。
第一种方法,把 cjson.empty_array 这个 userdata 赋值给指定 table。这样,在 json 编码的时候,它就会被当做空数组来处理:
$ resty -e 'local cjson = require “cjson”
local t = cjson.empty_array
print(cjson.encode(t))

不过,有时候我们并不确定,这个指定的 table 是否一直为空。我们希望当它为空的时候编码为数组,那么就要用到 cjson.empty_array_mt 这个函数,也就是我们的第二个方法。
它会标记好指定的 table,当 table 为空时编码为数组。从cjson.empty_array_mt 这个命名你也可以看出,它是通过 metatable 的方式进行设置的,比如下面这段代码操作:
$ resty -e 'local cjson = require “cjson”
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))

你可以在本地执行一下这段代码,看看输出和你预期的是否一致。
变量的个数限制
再来看第四个坑,变量的个数限制问题。 Lua 中,一个函数的局部变量的个数,和 upvalue 的个数都是有上限的,你可以从 Lua 的源码中得到印证:
/*
@@ LUAI_MAXVARS is the maximum number of local variables per function
@* (must be smaller than 250).
/
#define LUAI_MAXVARS 200
/

@@ LUAI_MAXUPVALUES is the maximum number of upvalues per function
@* (must be smaller than 250).
*/
#define LUAI_MAXUPVALUES 60
这两个阈值,分别被硬编码为 200 和 60。虽说你可以手动修改源码来调整这两个值,不过最大也只能设置为 250。
一般情况下,我们不会超过这个阈值,但写 OpenResty 代码的时候,你还是要留意这个事情,不要过多地使用局部变量和 upvalue,而是要尽可能地使用 do … end 做一层封装,来减少局部变量和 upvalue 的个数。
比如我们来看下面这段伪码:
local re_find = ngx.re.find
function foo() … end
function bar() … end
function fn() … end
如果只有函数 foo 使用到了 re_find, 那么我们可以这样改造下:
do
local re_find = ngx.re.find
function foo() … end
end
function bar() … end
function fn() … end
这样一来,在 main 函数的层面上,就少了 re_find 这个局部变量。这在单个的大的 Lua 文件中,算是一个优化技巧。
写在最后
从“多问几个为什么”的角度出发,Lua 中 250 这个阈值是从何而来的呢?这算是我们今天的思考题,欢迎你留言说下你的看法,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
将学到的知识总结成笔记,方便日后快速查找及复习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值