This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
<audio id="audio" title="01 | 初探OpenResty的三大特性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/74/55/747214d33b71126a7e00818541410a55.mp3"></audio>
你好,我是温铭。
开篇词里我们说过OpenResty的优势显而易见。不过在具体学习之前让我们先简单回顾下 OpenResty 的发展过程,这有助于你对后面内容有更好的理解。
## OpenResty的发展
OpenResty 并不像其他的开发语言一样从零开始搭建而是基于成熟的开源组件——NGINX 和 LuaJIT。OpenResty 诞生于 2007 年,不过,它的第一个版本并没有选择 Lua而是用了 Perl这跟作者章亦春的技术偏好有很大关系。
但 Perl 的性能远远不能达到要求于是在第二个版本中Perl 就被 Lua 给替换了。 不过,**在 OpenResty 官方的项目中Perl 依然占据着重要的角色OpenResty 工程化方面都是用 Perl 来构建比如测试框架、Linter、CLI 等**,后面我们也会逐步介绍。
后来,章亦春离开了淘宝,加入了美国的 CDN 公司 Cloudflare。因为 OpenResty 高性能和动态的优势很适合 CDN 的业务需求,很快, OpenResty 就成为 CDN 的技术标准。 通过丰富的 lua-resty 库OpenResty 开始逐渐摆脱 NGINX 的影子,形成自己的生态体系,在 API 网关、软WAF 等领域被广泛使用。
其实我经常说OpenResty 是一个被广泛使用的技术,但它并不能算得上是热门技术,这听上去有点矛盾,到底什么意思呢?
说它应用广,是因为 OpenResty 现在是全球排名第五的 Web 服务器。我们经常用到的 12306 的余票查询功能,或者是京东的商品详情页,这些高流量的背后,其实都是 OpenResty 在默默地提供服务。
说它并不热门,那是因为使用 OpenResty 来构建业务系统的比例并不高。使用者大都用OpenResty来处理入口流量并没有深入到业务里面去自然对于 OpenResty 的使用也是浅尝辄止,满足当前的需求就可以了。这当然也与 OpenResty 没有像 Java、Python 那样有成熟的 Web 框架和生态有关。
说了这么多接下来我重点来介绍下OpenResty 这个开源项目值得称道和学习的几个地方。
## OpenResty的三大特性
### 详尽的文档和测试用例
没错,文档和测试是判断开源项目是否靠谱的关键指标,甚至是排在代码质量和性能之前的。
OpenResty 的文档非常详细作者把每一个需要注意的点都写在了文档中。绝大部分时候我们只需要仔细查看文档就能解决遇到的问题而不用谷歌搜索或者是跟踪到源码中。为了方便起见OpenResty 还自带了一个命令行工具`restydoc`,专门用来帮助你通过 shell 查看文档,避免编码过程被打断。
不过,文档中只会有一两个通用的代码片段,并没有完整和复杂的示例,到哪里可以找到这样的例子呢?
对于 OpenResty 来说,自然是`/t`目录,它里面就是所有的测试案例。每一个测试案例都包含完整的 NGINX 配置和 Lua 代码以及测试的输入数据和预期的输出数据。不过OpenResty 使用的测试框架,与其他断言风格的测试框架完全不同,后面我会用专门章节来做介绍。
### 同步非阻塞
协程,是很多脚本语言为了提升性能,在近几年新增的特性。但它们实现得并不完美,有些是语法糖,有些还需要显式的关键字声明。
OpenResty 则没有历史包袱,在诞生之初就支持了协程,并基于此实现了**同步非阻塞**的编程模式。这一点是很重要的,毕竟,程序员也是人,代码应该更符合人的思维习惯。显式的回调和异步关键字会打断思路,也给调试带来了困难。
这里我解释一下,什么是同步非阻塞。先说同步,这个很简单,就是按照代码来顺序执行。比如下面这段伪码:
```
local res, err = query-mysql(sql)
local value, err = query-redis(key)
```
在同一请求连接中,如果要等 MySQL 的查询结果返回后,才能继续去查询 Redis那就是同步如果不用等 MySQL 的返回,就能继续往下走,去查询 Redis那就是异步。对于 OpenResty 来说,绝大部分都是同步操作,只有 `ngx.timer` 这种后台定时器相关的 API才是异步操作。
再来说说非阻塞,这是一个很容易和“异步”混淆的概念。这里我们说的“阻塞”,特指阻塞操作系统线程。我们继续看上面的例子,假设查询 MySQL 需要1s 的时间如果在这1s 内操作系统的资源CPU是空闲着并傻傻地等待返回那就是阻塞如果 CPU 趁机去处理其他连接的请求,那就是非阻塞。非阻塞也是 C10K、C100K 这些高并发能够实现的关键。
同步非阻塞这个概念很重要,建议你仔细琢磨一下。我认为,这一概念最好不要通过类比来理解,因为不恰当的类比,很可能把你搞得更糊涂。
在 OpenResty 中,上面的伪码就可以直接实现同步非阻塞,而不用任何显式的关键字。这里也再次体现了,让开发者用起来更简单,是 OpenResty 的理念之一。
### 动态
OpenResty 有一个非常大的优势,并且还没有被充分挖掘,就是它的**动态**。
传统的 Web 服务器,比如 NGINX如果发生任何的变动都需要你去修改磁盘上的配置文件然后重新加载才能生效这也是因为它们并没有提供 API来控制运行时的行为。所以在需要频繁变动的微服务领域NGINX 虽然有多次尝试,但毫无建树。而异军突起的 Envoy 正是凭着 xDS 这种动态控制的 API大有对 NGINX 造成降维攻击的威胁。
和 NGINX 、 Envoy 不同的是OpenResty 是由脚本语言 Lua 来控制逻辑的,而动态,便是 Lua 天生的优势。通过 OpenResty 中 lua-nginx-module 模块中提供的 Lua API我们可以动态地控制路由、上游、SSL 证书、请求、响应等。甚至更进一步,你可以在不重启 OpenResty 的前提下,修改业务的处理逻辑,并不局限于 OpenResty 提供的 Lua API。
这里有一个很合适的类比,可以帮你理解上面关于动态的说明。你可以把 Web 服务器当做是一个正在高速公路上飞驰的汽车NGINX 需要停车才能更换轮胎更换车漆颜色Envoy 可以一边跑一边换轮胎和颜色;而 OpenResty 除了具备前者能力外,还可以在不停车的情况下,直接把汽车从 SUV 变成跑车。
显然掌握这种“逆天”的能力后OpenResty 的能力圈和想象力就扩展到了其他领域,比如 Serverless 和边缘计算等。
## 你学习的重点在哪里?
讲了这么多OpenResty的重点特性你又该怎么学呢我认为学习需要抓重点围绕主线来展开而不是眉毛胡子一把抓这样你才能构建出脉络清晰的知识体系。
要知道,不管多么全面的课程,都不可能覆盖所有问题,更不能直接帮你解决线上的每个 bug 和异常。
回到OpenResty的学习在我看来想要学好 OpenResty你必须理解下面8个重点
<li>
同步非阻塞的编程模式;
</li>
<li>
不同阶段的作用;
</li>
<li>
LuaJIT 和 Lua 的不同之处;
</li>
<li>
OpenResty API 和周边库;
</li>
<li>
协程和 cosocket
</li>
<li>
单元测试框架和性能测试工具;
</li>
<li>
火焰图和周边工具链;
</li>
<li>
性能优化。
</li>
这些内容正是我们学习的重点,在专栏的各个模块中我都会分别讲到。在学习的过程中,我希望你能举一反三,并且根据自己的兴趣点和背景,有针对性地深入阅读某些章节。
如果你是 OpenResty 的初学者,那么你可以完全跟着专栏的进度,在自己的环境中安装 OpenResty运行并修改示例代码。要记住你的重点在于构建 OpenResty 的全貌,而非死磕某个知识点。当然,如果你有疑问的地方,随时可以在留言区提出,我会解答你的困惑。
如果你正在项目中使用 OpenResty那就太棒了相信你在阅读 LuaJIT 和性能优化章节时,一定会有更多的共鸣,更能应用到实际,在你的项目中看到优化前后的性能指标变化。
另外,如果你想要给 OpenResty 以及周边库贡献代码,那么最大的门槛,并不是对 OpenResty 原理的理解,或者是如何编写 NGINX C 模块的问题,而是测试案例和代码规范。我见过太多 OpenResty 的代码贡献者(也包括我自己),在一个 PR 上反复修改测试案例和代码风格,这其中有太多鲜为人知的潜规则。所以,专栏的代码规范和单元测试部分,就是为你准备的。
而如果你是测试工程师,即使你不使用 OpenRestyOpenResty 的测试框架和性能分析工具集也必能给你非常多的启发。毕竟OpenResty 在测试上面的投入和积累是相当深厚的。
## 写在最后
欢迎你留言和我分享你的 OpenResty 学习之路,在这期间,你又走过哪些弯路呢?也欢迎你把这篇文章转发给你的同事、朋友。
还是那句话,在学习的过程中,你有任何疑问,都可以在专栏中留言,我会第一时间给你答复。

View File

@@ -0,0 +1,229 @@
<audio id="audio" title="02 | 如何写出你的“hello world”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/0b/0df176a972a52a2555d75a11d2152e0b.mp3"></audio>
你好,我是温铭。今天起,就要开始我们的正式学习之旅。
每当我们开始学习一个新的开发语言或者平台,都会从最简单的`hello world`开始OpenResty 也不例外。让我们先跳过安装的步骤,直接看下,最简单的 OpenResty 程序是怎么编写和运行的:
```
$ resty -e &quot;ngx.say('hello world')&quot;
hello world
```
这应该是你见过的最简单的那种 hello world 代码写法,和 Python 类似:
```
$ python -c 'print(&quot;hello world&quot;)'
hello world
```
这背后其实是 OpenResty 哲学的一种体现,代码要足够简洁,也好让你打消“从入门到放弃“的念头。我们今天的内容,就专门围绕着这行代码来展开聊一聊。
上一节我们讲过OpenResty 是基于 NGINX 的。那你现在是不是有一个疑问:为什么这里看不到 NGINX 的影子?别着急,我们加一行代码,看看 `resty`背后真正运行的是什么:
```
resty -e &quot;ngx.say('hello world'); ngx.sleep(10)&quot; &amp;
```
我们加了一行 sleep 休眠的代码,让 resty 运行的程序打印出字符串后,并不退出。这样,我们就有机会一探究竟:
```
$ ps -ef | grep nginx
501 25468 25462 0 7:24下午 ttys000 0:00.01 /usr/local/Cellar/openresty/''1.13.6.2/nginx/sbin/nginx -p /tmp/resty_AfNwigQVOB/ -c conf/nginx.conf
```
终于看了熟悉的 NGINX 进程。看来,`resty` 本质上是启动了一个 NGINX 服务,那么`resty` 又是一个什么程序呢?我先卖个关子,咱后面再讲。
你的机器上可能还没有安装 OpenResty所以接下来我们先回到开头跳过的安装步骤把 OpenResty 安装完成后再继续。
## OpenResty 的安装
和其他的开源软件一样OpenResty 的安装有多种方法,比如使用操作系统的包管理器、源码编译或者 docker 镜像。我推荐你优先使用 yum、apt-get、brew 这类包管理系统,来安装 OpenResty。这里我们使用 Mac 系统来做示例:
```
brew tap openresty/brew
brew install openresty
```
使用其他操作系统也是类似的,先要在包管理器中添加 OpenResty 的仓库地址,然后用包管理工具来安装。具体步骤,你可以参考[官方文档](https://openresty.org/en/linux-packages.html)。
不过,这看似简单的安装背后,其实有两个问题:
<li>
为什么我不推荐使用源码来安装呢?
</li>
<li>
为什么不能直接从操作系统的官方仓库安装,而是需要先设置另外一个仓库地址?
</li>
对于这两个问题,你不妨先自己想一想。
这里我想补充一句。在这门课程里面,我会在表象背后提出很多的“为什么”,希望你可以一边学新东西一边思考,结果是否正确并不重要。独立思考在技术领域也是稀缺的,由于每个人技术领域和深度的不同,在任何课程中老师都会不可避免地带有个人观点以及知识的错漏。只有在学习过程中多问几个为什么,融会贯通,才能逐渐形成自己的技术体系。
很多工程师都有源码的情节,多年前的我也是一样。在使用一个开源项目的时候,我总是希望能够自己手工从源码开始 configure 和 make并修改一些编译参数感觉这样做才能最适合这台机器的环境才能把性能发挥到极致。
但现实并非如此,每次源码编译,我都会遇到各种诡异的环境问题,磕磕绊绊才能安装好。现在我想明白了,我们的最初目的其实是用开源项目来解决业务需求,不应该浪费时间和环境鏖战,更何况包管理器和容器技术,正是为了帮我们解决这些问题。
言归正传,给你说说我的看法。使用 OpenResty 源码安装,不仅仅步骤繁琐,需要自行解决 PCRE、OpenSSL 等外部依赖,而且还需要手工对 OpenSSL 打上对应版本的补丁。不然就会在处理 SSL session 时,带来功能上的缺失,比如像`ngx.sleep`这类会导致 yield 的 Lua API 就没法使用。这部分内容如果你还想深入了解,可以参考[[官方文档](https://github.com/openresty/lua-nginx-module#ssl_session_fetch_by_lua_block)]来获取更详细的信息。
从 OpenResty 自己维护的 OpenSSL [[打包脚本](https://github.com/openresty/openresty-packaging/blob/master/rpm/SPECS/openresty-openssl.spec)]中,就可以看到这些补丁。而在 OpenResty 升级 OpenSSL 版本时,都需要重新生成对应的补丁,并进行完整的回归测试。
```
Source0: https://www.openssl.org/source/openssl-%{version}.tar.gz
Patch0: https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-1.1.0d-sess_set_get_cb_yield.patch
Patch1: https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-1.1.0j-parallel_build_fix.patch
```
同时,我们可以看下 OpenResty 在 CentOS 中的[[打包脚本]](https://github.com/openresty/openresty-packaging/blob/master/rpm/SPECS/openresty.spec),看看是否还有其他隐藏的点:
```
BuildRequires: perl-File-Temp
BuildRequires: gcc, make, perl, systemtap-sdt-devel
BuildRequires: openresty-zlib-devel &gt;= 1.2.11-3
BuildRequires: openresty-openssl-devel &gt;= 1.1.0h-1
BuildRequires: openresty-pcre-devel &gt;= 8.42-1
Requires: openresty-zlib &gt;= 1.2.11-3
Requires: openresty-openssl &gt;= 1.1.0h-1
Requires: openresty-pcre &gt;= 8.42-1
```
从这里可以看出OpenResty 不仅维护了自己的 OpenSSL 版本,还维护了自己的 zlib 和 PCRE 版本。不过后面两个只是调整了编译参数,并没有维护自己的补丁。
所以,综合这些因素,我不推荐你自行源码编译 OpenResty除非你已经很清楚这些细节。
为什么不推荐源码安装,你现在应该已经很清楚了。其实我们在回答第一个问题时,也顺带回答了第二个问题:为什么不能直接从操作系统的官方仓库安装,而是需要先设置另外一个仓库地址?
这是因为,官方仓库不愿意接受第三方维护的 OpenSSL、PCRE 和 zlib 包这会导致其他使用者的困惑不知道选用哪一个合适。另一方面OpenResty 又需要指定版本的 OpenSSL、PCRE 库才能正常运行,而系统默认自带的版本都比较旧。
## OpenResty CLI
安装完 OpenResty 后,默认就已经把 OpenResty 的 CLI`resty` 安装好了。`resty`是个 1000 多行的 Perl 脚本之前我们提到过OpenResty 的周边工具都是 Perl 编写的,这个是由 OpenResty 作者的技术偏好决定的。
```
$ which resty
/usr/local/bin/resty
$ head -n 1 /usr/local/bin/resty
#!/usr/bin/env perl
```
`resty` 的功能很强大,想了解完整的列表,你可以查看`resty -h`或者[[官方文档](https://github.com/openresty/resty-cli)]。下面,我挑两个有意思的功能介绍一下。
```
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
dict:set(&quot;Tom&quot;, 56)
print(dict:get(&quot;Tom&quot;))'
56
```
先来看第一个例子。这个示例结合了 NGINX 配置和 Lua 代码,一起完成了一个共享内存字典的设置和查询。`dogs 1m` 是 NGINX 的一段配置,声明了一个共享内存空间,名字是 dogs大小是 1m在 Lua 代码中用字典的方式使用共享内存。另外还有`--http-include``--main-include`来设置 NGINX 配置文件。所以,上面的例子也可以写为:
```
resty --http-conf 'lua_shared_dict dogs 1m;' -e 'local dict = ngx.shared.dogs
dict:set(&quot;Tom&quot;, 56)
print(dict:get(&quot;Tom&quot;))'
```
OpenResty 世界中常用的调试工具,比如`gdb``valgrind``sysetmtap``Mozilla rr` ,也可以和 `resty` 一起配合使用,方便你平时的开发和测试。它们分别对应着 `resty` 不同的指令,内部的实现其实很简单,就是多套了一层命令行调用。我们以 valgrind 为例:
```
$ resty --valgrind -e &quot;ngx.say('hello world'); &quot;
ERROR: failed to run command &quot;valgrind /usr/local/Cellar/openresty/1.13.6.2/nginx/sbin/nginx -p /tmp/resty_hTFRsFBhVl/ -c conf/nginx.conf&quot;: No such file or directory
```
在后面调试、测试和性能分析的章节,会涉及到这些工具的使用。它们不仅适用于 OpenResty 世界,也是服务端的通用工具,让我们循序渐进地来学习吧。
## 更正式的 hello world
最开始我们使用`resty`写的第一个 OpenResty 程序,没有 master 进程,也不会监听端口。下面,让我们写一个更正式的 hello world。
写出这样的 OpenResty 程序并不简单,你至少需要三步才能完成:
<li>
创建工作目录;
</li>
<li>
修改 NGINX 的配置文件,把 Lua 代码嵌入其中;
</li>
<li>
启动 OpenResty 服务。
</li>
我们先来创建工作目录。
```
mkdir geektime
cd geektime
mkdir logs/ conf/
```
下面是一个最简化的 `nginx.conf`,在根目录下新增 OpenResty 的`content_by_lua`指令,里面嵌入了`ngx.say`的代码:
```
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
content_by_lua '
ngx.say(&quot;hello, world&quot;)
';
}
}
}
```
请先确认下,是否已经把`openresty`加入到`PATH`环境中;然后,启动 OpenResty 服务就可以了:
```
openresty -p `pwd` -c conf/nginx.conf
```
没有报错的话OpenResty 的服务就已经成功启动了。你可以打开浏览器,或者使用 curl 命令,来查看结果的返回:
```
$ curl -i 127.0.0.1:8080
HTTP/1.1 200 OK
Server: openresty/1.13.6.2
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive
hello, world
```
到这里,恭喜你,一个真正的 OpenResty 程序就完成了。
## 总结
让我们回顾下今天讲的内容。我们通过一行简单的 `hello, world` 代码延展到OpenResty 的安装和 CLI并在最后启动了 OpenResty 进程,运行了一个真正的后端程序。
其中, `resty` 是我们后面会频繁使用到的命令行工具,课程中的演示代码都是用它来运行的,而不是启动后台的 OpenResty 服务。
更为重要的是OpenResty 的背后隐藏了非常多的文化和技术细节,它就像漂浮在海面上的一座冰山。我希望能够通过这门课程,给你展示更全面、更立体的 OpenResty而不仅仅是它对外暴露出来的 API。
## 思考
最后,我给你留一个作业题。我们现在的做法,是把 Lua 代码写在 NGINX 配置文件中。不过,如果代码越来越多,那代码的可读性和可维护性就无法保证了。
你有什么方法来解决这个问题吗?欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友。

View File

@@ -0,0 +1,243 @@
<audio id="audio" title="03 | 揪出隐藏在背后的那些子项目" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a1/3c/a1f68d0e1bd11781769bf72081ca413c.mp3"></audio>
你好,我是温铭。
我们先来揭晓上一节最后留下的思考题,如何把 Lua 代码从 nginx.conf 里面抽取出来,保持代码的可读性和可维护性呢?
操作其实很简单。
我们先在 geektime 的工作目录下,创建一个名为 lua 的目录,专门用来存放代码:
```
$ mkdir lua
$ cat lua/hello.lua
ngx.say(&quot;hello, world&quot;)
```
然后修改 nginx.conf 的配置,把 content_by_lua_block 改为 content_by_lua_file
```
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
content_by_lua_file lua/hello.lua;
}
}
}
```
最后,重启 OpenResty 的服务就可以了:
```
$ sudo kill -HUP `cat logs/nginx.pid`
```
你可以使用 curl ,验证是否返回了预期的结果。至于后面 Lua 代码的变更,你就可以直接修改 hello.lua 这个文件,而不是 nginx.conf 了。
其实,在上面这个小例子里面,也有几个有趣的地方:
<li>
content_by_lua_file lua/hello.lua; 里面写的是相对路径,那么 OpenResty 是如何找到这个 Lua 文件的?
</li>
<li>
Lua 代码内容的变更,需要重启 OpenResty 服务才会生效,这样显然不方便调试,那么有没有什么即时生效的方法呢?
</li>
<li>
如何把 lua 代码所在的文件夹,加入到 OpenResty 的查找路径中呢?
</li>
这几个问题,我鼓励你先自己思考一下,它们都可以在官方文档里面找到[答案](https://github.com/openresty/lua-nginx-module#content_by_lua_file)。这也是为什么,我一直强调文档的重要性。
接下来我们一起来解答。先看第一个问题。如果原本给出的是相对路径,那么 OpenResty 在启动时,会把 OpenResty 启动的命令行参数中的 -p PATH 作为前缀将相对路径拼接为绝对路径。这样自然就可以顺利找到Lua 文件。
再来看第二个问题。Lua 代码在第一个请求时会被加载,并默认缓存起来。所以在你每次修改 Lua 源文件后,都必须重新加载 OpenResty 才会生效。其实,在 nginx.conf 中关闭 lua_code_cache 就能避免重新加载,这一点你可以自己试试看。不过,特别需要注意的是,这种方法**只能临时**用于开发和调试,如果是线上部署,一定要记得打开缓存,否则会非常影响性能。
最后一个问题OpenResty 提供了 lua_package_path 指令,可以设置 Lua 模块的查找路径。针对上面的例子,我们可以把 lua_package_path 设置为 `$prefix/lua/?.lua;;`,其中,
<li>
`$prefix`就是启动参数中的 -p PATH
</li>
<li>
`/lua/?.lua`表示 lua 目录下所有以 .lua 作为后缀的文件;
</li>
<li>
最后的两个分号,则代表内置的代码搜索路径。
</li>
## OpenResty 安装后的目录结构
了解完第一个 hello world 程序后,我们继续追根究底,来看下 OpenResty 自身安装完成后,它的目录结构是怎样的,以及里面包含哪些文件。
我们先通过 -V 选项,查看 OpenResty 安装到了哪一个目录。下面的这个结果,我省略了很多模块的编译参数,这些我们稍后再来补上:
```
$ openresty -V
nginx version: openresty/1.13.6.2
built by clang 10.0.0 (clang-1000.10.44.4)
built with OpenSSL 1.1.0h 27 Mar 2018
TLS SNI support enabled
configure arguments: --prefix=/usr/local/Cellar/openresty/1.13.6.2/nginx ...
```
我本地是通过 brew 安装的,所以目录是`/usr/local/Cellar/openresty/1.13.6.2/nginx` ,和你的本地环境很可能不同。这其中主要包含了 bin、luajit、lualib、nginx、pod 这几个子目录。理解这几个文件夹的含义很重要,可以帮我们更好地学习 OpenResty。接下来我们逐个来看一下。
首先是最重要的 bin 目录:
```
$ ll /usr/local/Cellar/openresty/1.13.6.2/bin
total 320
-r-xr-xr-x 1 ming admin 19K 3 27 12:54 md2pod.pl
-r-xr-xr-x 1 ming admin 15K 3 27 12:54 nginx-xml2pod
lrwxr-xr-x 1 ming admin 19B 3 27 12:54 openresty -&gt; ../nginx/sbin/nginx
-r-xr-xr-x 1 ming admin 62K 3 27 12:54 opm
-r-xr-xr-x 1 ming admin 29K 3 27 12:54 resty
-r-xr-xr-x 1 ming admin 15K 3 27 12:54 restydoc
-r-xr-xr-x 1 ming admin 8.3K 3 27 12:54 restydoc-index
```
这里面既有我们上一节中提到的 OpenResty CLI resty也有最核心的可执行文件 openresty它其实是 nginx 的一个软链接。至于目录里面其他的一些工具,没有任何悬念,它们和 resty 一样,都是 Perl 脚本。
在这其中opm 是包管理工具,可以通过它来管理各类第三方包,后面会有一节内容专门来讲;而 restydoc则是我们第一节提到过的“老朋友”了它是 OpenResty 提供的文档查看工具,你可以通过它来查看 OpenResty 和 NGINX 的使用文档:
```
$ restydoc -s ngx.say
$ restydoc -s proxy_pass
```
这段代码中的两个例子,分别查询了 OpenResty 的 API 和 NGINX 的指令。restydoc 这个工具,对服务端工程师的专注开发有很大帮助。
浏览完了 bin 目录,我们接着看下 pod 目录。先强调一点这里的“pod”和 k8s 里“pod”的概念完全没有关系。pod 是 Perl 里面的一种标记语言,用于给 Perl 的模块编写文档。pod 目录中存放的就是 OpenResty、 NGINX、lua-resty-*、LuaJIT 的文档, 这些就和刚才提到的 restydoc 联系在一起了。
接下来是熟悉的 nginx 和 luajit 这两个目录。这两个很好理解,主要存放 NGINX 和 LuaJIT 的可执行文件和依赖,是 OpenResty 的基石。很多人说 OpenResty 基于 Lua这个说法其实并不准确从上面我们可以看出 OpenResty 其实是基于 LuaJIT的。
事实上,早期的 OpenResty 同时带有 Lua 和 LuaJIT你可以通过编译选项来决定使用 Lua 还是 LuaJIT。不过到了现在Lua逐渐被淘汰就只支持更高性能的 LuaJIT了。
最后,我们看下 lualib 目录。它里面存放的是 OpenResty 中使用到的 Lua 库,主要分为 ngx 和 resty 两个子目录。
<li>
前者存放的是 [lua-resty-core](https://github.com/openresty/lua-resty-core/tree/master/lib/ngx) 这个官方项目中的 Lua 代码,里面都是基于 FFI 重新实现的 OpenResty API后面我会用专门的文章来解释为什么要重新实现这里你有个大概印象即可不必深究。
</li>
<li>
而 resty 目录中存放的则是各种 lua-resty-* 项目包含的 Lua 代码,接下来我们会接触到。
</li>
按照我讲课的惯例,到这一步我会给出这些目录源头的出处。这也是开源项目的乐趣之一,如果你喜欢打破砂锅问到底,那你总发现更多好玩的东西。
下面是 OpenResty 在 CentOS 中的[打包脚本](https://github.com/openresty/openresty-packaging/blob/master/rpm/SPECS/openresty.spec#L218),里面包含了上面提到的所有目录,你可以自己了解一下。
```
%files
%defattr(-,root,root,-)
/etc/init.d/%{name}
/usr/bin/%{name}
%{orprefix}/bin/openresty
%{orprefix}/site/lualib/
%{orprefix}/luajit/*
%{orprefix}/lualib/*
%{orprefix}/nginx/html/*
%{orprefix}/nginx/logs/
%{orprefix}/nginx/sbin/*
%{orprefix}/nginx/tapset/*
%config(noreplace) %{orprefix}/nginx/conf/*
%{orprefix}/COPYRIGHT
```
## OpenResty 项目概览
提到 OpenResty你应该会想到 lua-nginx-module。没错**这个 NGINX 的 C 模块确实是 OpenResty 的核心,但它并不等价于 OpenResty**。很多工程师都会把 OpenResty 叫做 ngx lua有不少技术大会的分享和出版的书籍中也是用的这个叫法这其实是不严谨的也是 OpenResty 社区不提倡的。
下面我来讲讲为什么,以及 OpenResty 中除了 lua-nginx-module ,还有哪些其他的关联项目。
打开 OpenResty 在 GitHub 的 [项目主页](https://github.com/openresty/),你可以看到 OpenResty 包含了 68 个公开的项目,大概分为以下 7 类, 下面我来分别简单介绍下,让你有个初步的印象,这样你后面学习起来也轻松一些。
### **NGINX C 模块**
OpenResty 的项目命名都是有规范的,以 `*-nginx-module`命名的就是 NGINX 的 C 模块。
OpenResty 中一共包含了 20 多个 C 模块我们在本节最开始使用的openresty -V 中,也可以看到这些 C 模块:
```
$ openresty -V
nginx version: openresty/1.13.6.2
built by clang 10.0.0 (clang-1000.10.44.4)
built with OpenSSL 1.1.0h 27 Mar 2018
TLS SNI support enabled
configure arguments: --prefix=/usr/local/Cellar/openresty/1.13.6.2/nginx --with-cc-opt='-O2 -I/usr/local/include -I/usr/local/opt/pcre/include -I/usr/local/opt/openresty-openssl/include' --add-module=../ngx_devel_kit-0.3.0 --add-module=../echo-nginx-module-0.61 --add-module=../xss-nginx-module-0.06 --add-module=../ngx_coolkit-0.2rc3 --add-module=../set-misc-nginx-module-0.32 --add-module=../form-input-nginx-module-0.12 --add-module=../encrypted-session-nginx-module-0.08 --add-module=../srcache-nginx-module-0.31 --add-module=../ngx_lua-0.10.13 --add-module=../ngx_lua_upstream-0.07 --add-module=../headers-more-nginx-module-0.33 --add-module=../array-var-nginx-module-0.05 --add-module=../memc-nginx-module-0.19 --add-module=../redis2-nginx-module-0.15 --add-module=../redis-nginx-module-0.3.7 --add-module=../ngx_stream_lua-0.0.5 --with-ld-opt='-Wl,-rpath,/usr/local/Cellar/openresty/1.13.6.2/luajit/lib -L/usr/local/lib -L/usr/local/opt/pcre/lib -L/usr/local/opt/openresty-openssl/lib' --pid-path=/usr/local/var/run/openresty.pid --lock-path=/usr/local/var/run/openresty.lock --conf-path=/usr/local/etc/openresty/nginx.conf --http-log-path=/usr/local/var/log/nginx/access.log --error-log-path=/usr/local/var/log/nginx/error.log --with-pcre-jit --with-ipv6 --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_v2_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_realip_module --with-http_addition_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-http_geoip_module --with-http_gzip_static_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-threads --with-dtrace-probes --with-stream --with-stream_ssl_module --with-http_ssl_module
```
这里`--add-module=`后面跟着的,就是 OpenResty 的 C 模块。其中,最核心的就是 lua-nginx-module 和 stream-lua-nginx-module前者用来处理七层流量后者用来处理四层流量。
**这些 C 模块中,有些是需要特别注意的,虽然默认编译进入了 OpenResty但并不推荐使用**。 比如 redis2-nginx-module、redis-nginx-module 和 memc-nginx-module它们是用来和 redis以及memcached 交互使用的。这些 C 库是 OpenResty 早期推荐使用的,但在 cosocket 功能加入之后,它们都已经被 lua-resty-redis 和 lua-resty-memcached 替代,处于疏于维护的状态。
OpenResty 后面也不会开发更多的 NGINX C 库,而是专注在基于 cosocket 的 Lua 库上,后者才是未来。
### lua-resty-周边库
OpenResty 官方仓库中包含 18 个 lua-resty-* 库,涵盖 Redis、MySQL、memcached、websocket、dns、流量控制、字符串处理、进程内缓存等常用库。除了官方自带的之外还有更多的第三方库。它们非常重要所以下一章节我们会花更多的篇幅来专门介绍这些周边库。
### 自己维护的 LuaJIT 分支
OpenResty 除了维护自己的 OpenSSL patch 外,还维护了自己的 [LuaJIT 分支](https://github.com/openresty/luajit2)。在 2015 年LuaJIT 的作者 Mike Pall 宣布退休,寻找新的 LuaJIT 维护者,但 Mike 并没有找到合适的维护者,他现在主要是做 bugfix 的维护工作,新功能的开发也已经暂停,所以 OpenResty 维护着自己的 LuaJIT 分支。
**相对于 LuaLuaJIT 增加了不少独有的函数,这些函数非常重要**但知道的工程师并不多算是_半隐藏技能_后面我也会专门介绍。
### 测试框架
OpenResty 的测试框架是[test-nginx](https://github.com/openresty/test-nginx),同样也是用 Perl 语言来开发的,从名字上就能看出来,它是专门用来测试 NGINX 相关的项目。OpenResty 官方的所有 C 模块和 lua-resty 库的测试案例,都是由 test-nginx 驱动的。
这个框架和常见的基于断言的框架不同,是一套更强大和独立的系统,我们后面会花几节课来专门学习。
事实上,有些 OpenResty 的代码贡献者也没有搞清楚这个测试框架,有时候提交的 PR 中包含了不少复杂的 C 和 Lua 代码,但对编写对应的测试案例一事,还是经常发怵。所以,如果你已经查看过一些 OpenResty 项目中`/t`目录里面的测试案例,却仍然一头雾水,先别急着怀疑自己,大部分人都是一样的。
除了 test-nginx 之外,[mockeagain](https://github.com/openresty/mockeagain) 这个项目可以模拟慢速的网络,让程序每次只读写一个字节。对于 web 服务器来说,这是一个很有用的工具。
### 调试工具链
OpenResty 项目在如何科学和动态地调试代码上花费了大量的精力可以说是达到了极致。OpenResty 的作者章亦春专门写了[一篇文章](https://openresty.org/posts/dynamic-tracing/),来介绍动态追踪技术。我强烈推荐给你,看完也有助于理解对应的工具链。
[openresty-systemtap-toolkit](https://github.com/openresty/openresty-systemtap-toolkit) 和 [stapxx](https://github.com/openresty/stapxx) 这两个 OpenResty 的项目,都基于 systemtap 这个动态调试和追踪工具。使用 systemtap 最大的优势,便是实现活体分析,同时对目标程序完全无侵入。
打个比方systemtap就像是我们去医院照了个 CT无痛无感知。更棒的是systemtap 可以生成直观的火焰图来做性能分析,后面我也会专门介绍,这里先放一个火焰图,让你直观上有个感性的认识:
<img src="https://static001.geekbang.org/resource/image/dc/7f/dcc1340a7622ba1643e8d8b9347a417f.png" alt="">
### 打包相关
OpenResty 在不同发行操作系统(比如 CentOS、Ubuntu、MacOS 等)版本中的打包脚本,出于更细可控力度的目的,都是手工编写的。我们在介绍安装后目录结构的时候,就已经涉及到了这些打包相关的项目:[openresty-packaging](https://github.com/openresty/openresty-packaging) 和 [home-brew](https://github.com/openresty/homebrew-brew)。如果你对此有兴趣,可以自行学习,这里我就不再赘述了。
### 工程化工具
除了上面这些比较大块儿的项目之外OpenResty 还有一些负责工程化的工具,大都也是“深藏闺中”。
比如 [openresty-devel-utils](https://github.com/openresty/openresty-devel-utils) 就是开发 OpenResty 和 NGINX 的工具集。它们也都使用 Perl 开发,其中大部分的工具都是没有文档的。但对于 OpenResty 的开发者来说,这些工具又是非常有用的。
这里我先挑几个简单介绍一下。
[lj-releng](https://github.com/openresty/openresty-devel-utils/blob/master/lj-releng) 是一个简单有效的 LuaJIT 代码检测工具,类似 luacheck可以找出全局变量等潜在的问题。
[reindex](https://github.com/openresty/openresty-devel-utils/blob/master/reindex) 从名字来看是重建索引的意思,它其实是格式化 test-nginx 测试案例的工具可以重新排列测试案例的编号以及去除多余的空白符。reindex 可以说是 OpenResty 开发者每天都会用到的工具之一。
[opsboy](https://github.com/openresty/opsboy) 也是一个深藏不露的项目主要用于自动化部署。OpenResty 每次发布版本前,都会在 AWS EC2 集群上做完整的回归测试,详细的文档你可以参考[官方文档](https://openresty.org/en/ec2-test-cluster.html),而这个回归测试正是由 opsboy 来部署和驱动的。
opsboy 是一个用 Perl 实现的 DSL领域特定语言。实际上 OpenResty 的作者非常喜欢创造各种不同的 DSL 来解决问题。
## 写在最后
今天我们主要学习了OpenResty 安装后的目录结构,以及背后的一些子项目。希望你学完今天的内容后,能够了解更多 OpenResty 的项目。OpenResty 已经远远超出了 NGINX 负载均衡和反向代理的范畴,实现了自己的生态,下一次我们会详细聊聊这方面。
对于今天的内容,你有哪些疑惑和问题吗?欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友,一起学习高效开发。

View File

@@ -0,0 +1,163 @@
<audio id="audio" title="04 | 如何管理第三方包从包管理工具luarocks和opm说起" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/96/b3c3cad0a84d56ab548a91a66b35ad96.mp3"></audio>
你好,我是温铭。
在上一节中,我们大概了解了下 OpenResty 官方的一些项目。不过,如果我们把 OpenResty 用于生产环境显然OpenResty 安装包自带的这些库是远远不够的,比如没有 lua-resty 库来发起HTTP请求也没有办法和 Kafka 交互。
那么应该怎么办呢?本节我们就来一起了解下,应该从什么渠道来找到这些第三方库。
这里我再次强调下OpenResty 并不是 NGINX 的 fork也不是在 NGINX 的基础上加了一些常用库重新打包,而**只是把 NGINX 当作底层的网络库来使用**。
当你使用 NGINX 的时候是不会想着如何发起自定义的HTTP请求以及如何与 Kafka 交互的。而在 OpenResty 的世界中,由于 cosocket 的存在,开发者可以轻松地写出 lua-resty-http 和 lua-resty-kafka ,来处理这类需求,就像你用 Python、PHP 这类的开发语言一样。
另外,还有一个建议告诉你:你不应该使用任何 Lua 世界的库来解决上述问题,而是应该使用 cosocket 的 lua-resty-* 库。**Lua 世界的库很可能会带来阻塞**,让原本高性能的服务,直接下降几个数量级。这是 OpenResty 初学者的常见错误,而且并不容易觉察到。
那我们怎么找到这些非阻塞的 lua-resty-* 库呢?接下来,我来为你介绍下面几种途径。
## **OPM**
[OPM](https://opm.openresty.org/)OpenResty Package Manager是 OpenResty 自带的包管理器,在你安装好 OpenResty 之后,就可以直接使用。我们可以试着去找找发送 http 请求的库 `$ opm search http`
第一次查询可能会比较慢需要几秒钟的时间。opm.openresty.org 会从 PostgreSQL 数据库中做一次查询并把结果缓存一段时间。search 具体的返回结果比较长,我们这里只看下第一条返回值:
```
openresty/lua-resty-upload Streaming reader and parser for HTTP file uploading based on ngx_lua cosocket
```
呃,看到这个结果,你可能会疑惑:这个 lua-resty-upload 包和发送 http 有什么关系呢?
原来OPM做搜索的时候是用后面的关键字同时搜索了包的名字和包的简介。这也是为什么上面的搜索会持续几秒因为它在 PostgreSQL 里面做了字符串的全文搜索。
不过,不管怎么说,这个返回并不友好。让我们修改下关键字,重新搜索下:
```
$ opm search lua-resty-http
ledgetech/lua-resty-http Lua HTTP client cosocket driver for OpenResty/ngx_lua
pintsized/lua-resty-http Lua HTTP client cosocket driver for OpenResty/ngx_lua
agentzh/lua-resty-http Lua HTTP client cosocket driver for OpenResty/ngx_lua
```
其实,在 OpenResty 世界中,如果你使用 cosocket 实现了一个包,那么就要使用 lua-resty- 这个前缀,算是一个不成文的规定。
回过头来看刚刚的搜索结果OPM 使用了贡献者的 GitHub 仓库地址作为包名,即 GitHub ID / repo name。上面返回了三个 lua-resty-http 第三方库,我们应该选择哪一个呢?
眼尖的你,可能已经发现了 agentzh 这个 ID没错这就是 OpenResty 作者春哥本人。在选择这个包之前,我们看下它的 star 数和最后更新时间:只有十几个 star最后一次更新是在 2016 年。很明显这是个被放弃的坑。更深入地看下pintsized/lua-resty-http 和 ledgetech/lua-resty-http 其实指向了同一个仓库。所以,不管你选哪个都是一样的。
同时 [OPM 的网站](https://opm.openresty.org/) 也相对简单,没有提供包的下载次数,也没有这个包的依赖关系。你需要花费更多的时间,来甄别出到底使用哪些 lua-resty 库才是正确的选择,而这些本应该是维护者的事情。
## **LUAROCKS**
[LuaRocks](https://luarocks.org) 是 OpenResty 世界的另一个包管理器,诞生在 OPM 之前。不同于 OPM 里只包含 OpenResty 相关的包LuaRocks 里面还包含 Lua 世界的库。举个例子LuaRocks 里面的 LuaSQL-MySQL就是 Lua 世界中连接 MySQL 的包,并不能用在 OpenResty 中。
还是以HTTP库为例我们尝试用 LuaRocks 来试一试查找:
```
$ luarocks search http
```
你可以看到,也是返回了一大堆包。
我们不妨再换个关键字:
```
$ luarocks search lua-resty-http
```
这次只返回了一个包。我们可以到 LuaRocks 的网站上,去看看[这个包的详细信息](https://luarocks.org/modules/pintsized/lua-resty-http),下面是网站页面的截图:
<img src="https://static001.geekbang.org/resource/image/ba/95/ba5cbaae9a7a9ab1fbd05099dc7e9695.jpg" alt="">
这里面包含了作者、License、GitHub 地址、下载次数、功能简介、历史版本、依赖等。和 OPM 不同的是LuaRocks 并没有直接使用 GitHub 的用户信息,而是需要开发者单独在 LuaRocks 上进行注册。
其实,开源的 API 网关项目 Kong就是使用 LuaRocks 来进行包的管理,并且还把 LuaRocks 的作者收归麾下。我们接着就来简单看下Kong 的包管理配置是怎么写的。
目前 Kong 的最新版本是 1.1.1 你可以在 [https://github.com/Kong/kong](https://github.com/Kong/kong) 的项目下找到最新的 .rockspec 后缀的文件。
```
package = &quot;kong&quot;
version = &quot;1.1.1-0&quot;
supported_platforms = {&quot;linux&quot;, &quot;macosx&quot;}
source = {
url = &quot;git://github.com/Kong/kong&quot;,
tag = &quot;1.1.1&quot;
}
description = {
summary = &quot;Kong is a scalable and customizable API Management Layer built on top of Nginx.&quot;,
homepage = &quot;https://konghq.com&quot;,
license = &quot;Apache 2.0&quot;
}
dependencies = {
&quot;inspect == 3.1.1&quot;,
&quot;luasec == 0.7&quot;,
&quot;luasocket == 3.0-rc1&quot;,
&quot;penlight == 1.5.4&quot;,
&quot;lua-resty-http == 0.13&quot;,
&quot;lua-resty-jit-uuid == 0.0.7&quot;,
&quot;multipart == 0.5.5&quot;,
&quot;version == 1.0.1&quot;,
&quot;kong-lapis == 1.6.0.1&quot;,
&quot;lua-cassandra == 1.3.4&quot;,
&quot;pgmoon == 1.9.0&quot;,
&quot;luatz == 0.3&quot;,
&quot;http == 0.3&quot;,
&quot;lua_system_constants == 0.1.3&quot;,
&quot;lyaml == 6.2.3&quot;,
&quot;lua-resty-iputils == 0.3.0&quot;,
&quot;luaossl == 20181207&quot;,
&quot;luasyslog == 1.0.0&quot;,
&quot;lua_pack == 1.0.5&quot;,
&quot;lua-resty-dns-client == 3.0.2&quot;,
&quot;lua-resty-worker-events == 0.3.3&quot;,
&quot;lua-resty-mediador == 0.1.2&quot;,
&quot;lua-resty-healthcheck == 0.6.0&quot;,
&quot;lua-resty-cookie == 0.1.0&quot;,
&quot;lua-resty-mlcache == 2.3.0&quot;,
......
```
通过文件你可以看到,依赖项里面掺杂了 lua-resty 库和纯 Lua 世界的库,使用 OPM 只能部分安装这些依赖项。写好配置后,使用 luarocks 的 upload 命令把这个配置上传,用户就可以用 LuaRocks 来下载并安装 Kong 了。
另外,在 OpenResty 中,除了 Lua 代码外,我们还经常会调用 C 代码这时候就需要编译才能使用。LuaRocks 是支持这么做的,你可以在 rockspec 文件中,指定 C 源码的路径和名称这样LuaRocks 就会帮你本地编译。而 OPM 暂时还不支持这种特性。
不过需要注意的是OPM 和 LuaRocks 都不支持私有包。
## **AWESOME-RESTY**
讲了这么多包管理的内容,其实呢,即使有了 OPM 和 LuaRocks对于 OpenResty 的 lua-resty 包,我们还是管中窥豹的状态。到底有没有地方可以让我们一览全貌呢?
当然是有的,[awesome-resty](https://github.com/bungle/awesome-resty) 这个项目,就维护了几乎所有 OpenResty 可用的包,并且都分门别类地整理好了。当你不确定是否存在适合的第三方包时,来这里“按图索骥”,可以说是最好的办法。
还是以HTTP库为例 在awesome-resty 中,它自然是属于 [networking](https://github.com/bungle/awesome-resty#networking) 分类:
```
lua-resty-http by @pintsized — Lua HTTP client cosocket driver for OpenResty / ngx_lua
lua-resty-http by @liseen — Lua http client driver for the ngx_lua based on the cosocket API
lua-resty-http by @DorianGray — Lua HTTP client driver for ngx_lua based on the cosocket API
lua-resty-http-simple — Simple Lua HTTP client driver for ngx_lua
lua-resty-httpipe — Lua HTTP client cosocket driver for OpenResty / ngx_lua
lua-resty-httpclient — Nonblocking Lua HTTP Client library for aLiLua &amp; ngx_lua
lua-httpcli-resty — Lua HTTP client module for OpenResty
lua-resty-requests — Yet Another HTTP Library for OpenResty
```
我们看到,这里有 8 个 lua-resty-http 的第三方库。对比一下前面的结果,我们使用 OPM 只找到 2 个而LuaRocks 里面更是只有 1 个。不过,如果你是选择困难症,请直接使用第一个,它和 LuaRocks 中的是同一个。
而对于愿意尝试的工程师,我更推荐你用最后一个库: [lua-resty-requests](https://github.com/tokers/lua-resty-requests),它是人类更友好的 HTTP访问库接口风格与 Python 中大名鼎鼎的 [Requests](http://docs.python-requests.org/en/master/) 一致。如果你跟我一样是一个 Python 爱好者,一定会喜欢上 lua-resty-requests。这个库的作者是 OpenResty 社区中活跃的 tokers因此你可以放心使用。
必须要承认OpenResty 现有的第三方库并不完善,所以,如果你在 awesome-resty 中没有找到你需要的库,那就需要你自己来实现,比如 OpenResty 一直没有访问 Oracle 或者 SQLServer 的 lua-rsety 库。
## 写在最后
一个开源项目想要健康地发展壮大,不仅需要有硬核的技术、完善的文档和完整的测试,还需要带动更多的开发者和公司一起加入进来,形成一个生态。正如 Apache 基金会的名言:社区胜于代码。
还是那句话,想把 OpenResty 代码写好一点儿也不简单。OpenResty 还没有系统的学习资料,也没有官方的代码指南,很多的优化点的确已经写在了开源项目中,但大多数开发者却是知其然而不知其所以然。这也是我这个专栏的目的所在,希望你学习完之后,可以写出更高效的 OpenResty 代码,也可以更容易地参与到 OpenResty 相关的开源项目中来。
你是如何看待 OpenResty 生态的呢?欢迎留言我们一起聊聊,也欢迎你把这篇文章转发给你的同事、朋友,一起在交流中进步。

View File

@@ -0,0 +1,35 @@
<video poster="https://static001.geekbang.org/resource/image/b8/a2/b8e479499551550984792f338043a8a2.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/32bb5df8-16d13f123cf-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/71f11392644a4bd19a04901804f3faa2/b54cb30cb2b648d1a3e6392ecc351626-e6d62449b88ad07255d04f874d679181-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/71f11392644a4bd19a04901804f3faa2/b54cb30cb2b648d1a3e6392ecc351626-32e46e96bebe336fa79a9419934ca38a-hd.m3u8" type="application/x-mpegURL"></video>
你好,我是温铭。
今天的内容,我特意安排成了视频的形式来讲解。不过,在你看视频之前,我想先问你这么几个问题:
- 在真实的项目中,你会配置 nginx.conf以便和 Lua 代码联动吗?
- 你清楚 OpenResty 的代码结构该如何组织吗?
这两个问题,也是今天视频课要解决的核心内容,希望你可以先自己思考一下,并带着问题来学习今天的视频内容。
同时,我会给出相应的文字介绍,方便你在听完视频内容后,及时总结与复习。下面是今天这节课的文字介绍部分。
## 今日核心
[opm](https://github.com/openresty/opm/) 是 OpenResty 中为数不多的网站类项目,而里面的代码,基本上是由 OpenResty 的作者亲自操刀完成的。
很多 OpenResty 的使用者并不清楚,如何在真实的项目中去配置 nginx.conf 以及如何组织 Lua 的代码结构。确实,在这方面可以参考的开源项目并不多,给学习使用带了不小的阻力。
不过借助今天的这个项目你就可以克服这一点了。你将会熟悉一个OpenResty 项目的结构和开发流程,还能看到 OpenResty 的作者是如何编写业务类 Lua 代码的。
opm 还涉及到数据库的操作它后台数据的储存使用的是PostgreSQL ,你可以顺便了解下 OpenResty 和数据库是如何交互的。
除此之外,这个项目还涉及到一些简单的性能优化,也是为了后面专门设立的性能优化内容做个铺垫。
最后,浏览完 opm 这个项目后,你可以自行看下另外一个类似的项目,那就是 OpenResty 的官方网站:[https://github.com/openresty/openresty.org](https://github.com/openresty/openresty.org)。
## 课件参考
今天的课件已经上传到了我的GitHub上你可以自己下载学习。
链接如下:[https://github.com/iresty/geektime-slides](https://github.com/iresty/geektime-slides)
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。

View File

@@ -0,0 +1,203 @@
<audio id="audio" title="06 | OpenResty 中用到的 NGINX 知识" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/6c/83a0cd3e6feb685678e9ba02a0c9d46c.mp3"></audio>
你好,我是温铭。
通过前面几篇文章的介绍,相信你对 OpenResty 的轮廓已经有了一个大概的认知。下面几节课里,我会带你熟悉下 OpenResty 的两个基石NGINX 和 LuaJIT。万丈高楼平地起掌握些这些基础的知识才能更好地去学习 OpenResty。
今天我先来讲 NGINX。这里我只会介绍下OpenResty 中可能会用到的一些 NGINX 基础知识,这些仅仅是 NGINX 很小的一个子集。如果你需要系统和深入学习 NGINX可以参考陶辉老师的《NGINX 核心知识 100 讲》,这也是极客时间上评价非常高的一门课程。
说到配置,其实,在 OpenResty 的开发中,我们需要注意下面几点:
- 要尽可能少地配置 nginx.conf
- 避免使用if、set 、rewrite 等多个指令的配合;
- 能通过 Lua 代码解决的,就别用 NGINX 的配置、变量和模块来解决。
这样可以最大限度地提高可读性、可维护性和可扩展性。
下面这段 NGINX 配置,就是一个典型的反例,可以说是把配置项当成了代码来使用:
```
location ~ ^/mobile/(web/app.htm) {
set $type $1;
set $orig_args $args;
if ( $http_user_Agent ~ &quot;(iPhone|iPad|Android)&quot; ) {
rewrite ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
}
proxy_pass http://foo.com/$type?$orig_args;
}
```
这是我们在使用 OpenResty 进行开发时需要避免的。
## **NGINX 配置**
我们首先来看下 NGINX 的配置文件。NGINX 通过配置文件来控制自身行为,它的配置可以看作是一个简单的 DSL。NGINX 在进程启动的时候读取配置,并加载到内存中。**如果修改了配置文件,需要你重启或者重载 NGINX再次读取后才能生效**。只有 NGINX 的商业版本,才会在运行时, 以 API 的形式提供部分动态的能力。
我们先来看下面这段配置,里面的内容非常简单,我相信大部分工程师都能看懂:
```
worker_processes auto;
pid logs/nginx.pid;
error_log logs/error.log notice;
worker_rlimit_nofile 65535;
events {
worker_connections 16384;
}
http {
server {
listen 80;
listen 443 ssl;
location / {
proxy_pass https://foo.com;
}
}
}
stream {
server {
listen 53 udp;
}
}
```
不过,即使是简单的配置,背后也涉及到了一些很重要的基础概念。
第一每个指令都有自己适用的上下文Context也就是NGINX 配置文件中指令的作用域。
最上层的是 main里面是和具体业务无关的一些指令比如上面出现的 worker_processes、pid 和 error_log都属于 main 这个上下文。另外,上下文是有层级关系的,比如 location 的上下文是 serverserver 的上下文是 httphttp 的上下文是 main。
指令不能运行在错误的上下文中NGINX 在启动时会检测 nginx.conf 是否合法。比如我们把
`listen 80;` 从 server 上下文换到 main 上下文,然后启动 NGINX 服务,会看到类似这样的报错:
```
&quot;listen&quot; directive is not allowed here ......
```
第二NGINX 不仅可以处理 HTTP 请求 和 HTTPS 流量,还可以处理 UDP 和 TCP 流量。
其中,七层的放在 HTTP 中,四层的放在 stream中。在 OpenResty 里面, lua-nginx-module 和 stream-lua-nginx-module 分别和这俩对应。
这里有一点需要注意,**NGINX 支持的功能OpenResty 并不一定支持,需要看 OpenResty 的版本号**。OpenResty 的版本号是和 NGINX 保持一致的,所以很容易识别。比如 NGINX 在 2018 年 3 月份发布的 1.13.10 版本中,增加了对 gRPC 的支持,但 OpenResty 在 2019 年 4 月份时的最新版本是 1.13.6.2,由此可以推断 OpenResty 还不支持 gRPC。
上面 nginx.conf 涉及到的配置指令,都在 NGINX 的核心模块 [ngx_core_module](http://nginx.org/en/docs/ngx_core_module.html)、[ngx_http_core_module_](http://nginx.org/en/docs/http/ngx_http_core_module.html) 和 [ngx_stream_core_module_](http://nginx.org/en/docs/stream/ngx_stream_core_module.html) 中,你可以点击这几个链接去查看具体的文档说明。
## **MASTER-WORKER 模式**
了解完配置文件,我们再来看下 NGINX 的多进程模式。这里我放了一张图来表示你可以看到NGINX 启动后,会有一个 Master 进程和多个 Worker 进程(也可以只有一个 Worker 进程,看你如何配置)。
<img src="https://static001.geekbang.org/resource/image/a7/92/a7304c2c8af0e1e6c54819c97611b992.jpg" alt="">
先来说 Master 进程,一如其名,扮演“管理者”的角色,并不负责处理终端的请求。它是用来管理 Worker 进程的,包括接受管理员发送的信号量、监控 Worker 的运行状态。当 Worker 进程异常退出时Master 进程会重新启动一个新的 Worker 进程。
Worker 进程则是“一线员工”,用来处理终端用户的请求。它是从 Master 进程 fork 出来的,彼此之间相互独立,互不影响。多进程的模式比 Apache 多线程的模式要先进很多,没有线程间加锁,也方便调试。即使某个进程崩溃退出了,也不会影响其他 Worker 进程正常工作。
而 OpenResty 在 NGINX Master-Worker 模式的前提下又增加了独有的特权进程privileged agent。这个进程并不监听任何端口和 NGINX 的 Master 进程拥有同样的权限,所以可以做一些需要高权限才能完成的任务,比如对本地磁盘文件的一些写操作等。
如果特权进程与 NGINX 二进制热升级的机制互相配合OpenResty 就可以实现自我二进制热升级的整个流程,而不依赖任何外部的程序。
减少对外部程序的依赖,尽量在 OpenResty 进程内解决问题不仅方便部署、降低运维成本也可以降低程序出错的概率。可以说OpenResty 中的特权进程、ngx.pipe 等功能,都是出于这个目的。
## **执行阶段**
执行阶段也是 NGINX 重要的特性,与 OpenResty 的具体实现密切相关。NGINX 有 11 个执行阶段,我们可以从 ngx_http_core_module.h 的源码中看到:
```
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_PRECONTENT_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
```
如果你想详细了解这 11 个阶段的作用,可以学习陶辉老师的视频课程,或者 NGINX 文档,这里我就不再赘述。
不过巧合的是OpenResty 也有 11 个 `*_by_lua`指令,它们和 NGINX 阶段的关系如下图所示(图片来自 lua-nginx-module 文档):
<img src="https://static001.geekbang.org/resource/image/2a/73/2a05cb2a679bd1c81b44508666e70273.png" alt="">
其中, `init_by_lua` 只会在 Master 进程被创建时执行,`init_worker_by_lua` 只会在每个 Worker 进程被创建时执行。其他的 `*_by_lua` 指令则是由终端请求触发,会被反复执行。
所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COWcopy on write特性来节省一些内存。
对于业务代码来说,其实大部分的操作都可以在 content_by_lua 里面完成,但我更推荐的做法,是根据不同的功能来进行拆分,比如下面这样:
- set_by_lua设置变量
- rewrite_by_lua转发、重定向等
- access_by_lua准入、权限等
- content_by_lua生成返回内容
- header_filter_by_lua应答头过滤处理
- body_filter_by_lua应答体过滤处理
- log_by_lua日志记录。
我举一个例子来说明这样拆分的好处。我们假设,你对外提供了很多明文 API现在需要增加自定义的加密和解密逻辑。那么请问你需要修改所有 API 的代码吗?
```
# 明文协议版本
location /mixed {
content_by_lua '...'; # 处理请求
}
```
当然不用。事实上,利用阶段的特性,我们只需要简单地在 access 阶段解密,在 body filter 阶段加密就可以了,原来 content 阶段的代码是不用做任何修改的:
```
# 加密协议版本
location /mixed {
access_by_lua '...'; # 请求体解密
content_by_lua '...'; # 处理请求,不需要关心通信协议
body_filter_by_lua '...'; # 应答体加密
}
```
## **二进制热升级**
最后,我来简单说一下 NGINX 的二进制热升级。我们知道,在你修改完 NGINX 的配置文件后,还需要重启才能生效。但在 NGINX 升级自身版本的时候,却可以做到热升级。这看上去有点儿本末倒置,不过,考虑到 NGINX 是从传统静态的负载均衡、反向代理、文件缓存起家的,这倒也可以理解。
热升级通过向旧的 Master 进程发送 USR2 和 WINCH 信号量来完成。对于这两步,前者的作用,是启动新的 Master 进程;后者的作用,是逐步关闭 Worker 进程。
执行完这两步后,新的 Master 和新的 Worker 就已经启动了。不过此时,旧的 Master 并没有退出。不退出的原因也很简单,如果你需要回退,依旧可以给旧的 Master 发送 HUP 信号量。当然,如果你已经确定不需要回退,就可以给旧 Master 发送 KILL 信号量来退出。
至此,大功告成,二进制的热升级就完成了。
关于二进制升级,我主要就讲这些。如果你想了解这方面更详细的资料,可以查阅[官方文档](http://nginx.org/en/docs/control.html#upgrade)继续学习。
## **课外延伸**
OpenResty 的作者多年前写过一个 [NGINX 教程](https://openresty.org/download/agentzh-nginx-tutorials-zhcn.html),如果你对此感兴趣,可以自己学习下。这里面的内容比较多,即使看不懂也没有关系,并不会影响你学习 OpenResty。
## 写在最后
总的来说,在 OpenResty 中用到的都是 Nginx 的基础知识,主要涉及到配置、主从进程、执行阶段等。而**其他能用 Lua 代码解决的尽量用代码来解决而非使用Nginx 的模块和配置**,这是在学习 OpenResty 中的一个思路转变。
最后我给你留了一道开放的思考题。Nginx 官方支持 NJS也就是可以用 JS 写控制部分 Nginx 的逻辑,和 OpenResty 的思路很类似。对此,你是怎么看待的呢?
欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友。

View File

@@ -0,0 +1,341 @@
<audio id="audio" title="07 | 带你快速上手 Lua" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/ab/61cbc24102eb4f04301c96a0f2f845ab.mp3"></audio>
你好,我是温铭。
在大概了解 NGINX 的基础知识后,接下来,我们就要来进一步学习 Lua了。它是 OpenResty 中使用的编程语言,掌握它的基本语法还是很有必要的。
Lua 是一个小巧精妙的脚本语言诞生于巴西的大学实验室这个名字在葡萄牙语里的含义是“美丽的月亮”。从作者所在的国家来看NGINX 诞生于俄罗斯Lua 诞生于巴西OpenResty 诞生于中国,这三门同样精巧的开源技术都出自金砖国家,而不是欧美,也是挺有趣的一件事。
回到Lua语言上。事实上Lua 在设计之初,就把自己定位为一个简单、轻量、可嵌入的胶水语言,没有走大而全的路线。虽然你平常工作中可能没有直接编写 Lua 代码,但 Lua 的使用其实非常广泛。很多的网游,比如魔兽世界,都会采用 Lua 来编写插件;而键值数据库 Redis 则是内置了 Lua 来控制逻辑。
另一方面,虽然 Lua 自身的库比较简单,但它可以方便地调用 C 库,大量成熟的 C 代码都可以为其所用。比如在 OpenResty 中,很多时候都需要你调用 NGINX 和 OpenSSL 的 C 函数,而这都得益于 Lua 和 LuaJIT 这种方便调用 C 库的能力。
下面,我带你来快速熟悉下 Lua 的数据类型和语法,以便你后面更顺畅地学习 OpenResty。
## 环境和 hello world
我们不用专门去安装标准 Lua 5.1 之类的环境,因为 OpenResty 已经不再支持标准 Lua而只支持 LuaJIT。这里我介绍的 Lua 语法,也是和 LuaJIT 兼容的部分,而不是基于最新的 Lua 5.3,这一点需要你特别注意。
在 OpenResty 的安装目录下,你可以找到 LuaJIT 的目录和可执行文件。我这里是 Mac 环境,使用 brew 安装 OpenResty所以你本地的路径很可能和下面的不同
```
$ ll /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit
lrwxr-xr-x 1 ming admin 18B 4 2 14:54 /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit -&gt; luajit-2.1.0-beta3
```
你也可以在系统的可执行文件目录中找到它:
```
$ which luajit
/usr/local/bin/luajit
```
并查看 LuaJIT 的版本号:
```
$ luajit -v
LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
```
查清楚这些信息后,你可以新建一个 `1.lua` 文件,并用 luajit 来运行其中的 hello world 代码:
```
$ cat 1.lua
print(&quot;hello world&quot;)
$ luajit 1.lua
hello world
```
当然,你还可以使用 `resty` 来直接运行,要知道,它最终也是用 LuaJIT 来执行的:
```
$ resty -e 'print(&quot;hello world&quot;)'
hello world
```
上述两种运行 hello world 的方式都是可行的。不顾对我来说,我更喜欢 `resty` 这种方式,因为后面很多 OpenResty 的代码,也都是通过 `resty` 来运行的。
## 数据类型
Lua 中的数据类型不多,你可以通过 `type` 函数来返回一个值的类型,比如下面这样的操作:
```
$ resty -e 'print(type(&quot;hello world&quot;))
print(type(print))
print(type(true))
print(type(360.0))
print(type({}))
print(type(nil))
'
```
会打印出如下内容:
```
string
function
boolean
number
table
nil
```
这几种就是 Lua 中的基本数据类型了。下面我们来简单介绍一下它们。
### 字符串
在 Lua 中,字符串是不可变的值,如果你要修改某个字符串,就等于创建了一个新的字符串。这种做法显然有利有弊:好处是即使同一个字符串出现了很多次,在内存中也只有一份;但劣势也很明显,如果你想修改、拼接字符串,会额外地创建很多不必要的字符串。
我们举一个例子,来说明这个弊端。下面这段代码,是把 1 到 10 这些数字当作字符串拼接起来。对了,在 Lua 中,我们使用两个点号来表示字符串的相加:
```
$ resty -e 'local s = &quot;&quot;
for i = 1, 10 do
s = s .. tostring(i)
end
print(s)'
```
这里我们循环了 10 次,但只有最后一次是我们想要的,而中间新建的 9 个字符串都是无用的。它们不仅占用了额外的空间,也消耗了不必要的 CPU 运算。
当然,在后面的性能优化章节,我们会有对应的方法来解决它。
另外,在 Lua 中,你有三种方式可以表达一个字符串:单引号、双引号,以及长括号(`[[]]`)。前面两种都比较好理解,别的语言一般也这么用,那么长括号有什么用处呢?
我们看一个具体的示例:
```
$ resty -e 'print([[string has \n and \r]])'
string has \n and \r
```
你可以看到,长括号中的字符串不会做任何的转义处理。
你也许会问另外一个问题:如果上面那段字符串中包括了长括号本身,又该怎么处理呢?答案很简单,就是在长括号中间增加一个或者多个 `=` 符号:
```
$ resty -e 'print([=[ string has a [[]]. ]=])'
string has a [[]].
```
### 布尔值
这个很简单true 和 false。但在 Lua 中,只有 nil 和 false 为假,其他都为真,包括 0 和空字符串也为真。我们可以用下面的代码印证一下:
```
$ resty -e 'local a = 0
if a then
print(&quot;true&quot;)
end
a = &quot;&quot;
if a then
print(&quot;true&quot;)
end'
```
这种判断方式和很多常见的开发语言并不一致,所以,为了避免在这种问题上出错,你可以显式地写明比较的对象,比如下面这样:
```
$ resty -e 'local a = 0
if a == false then
print(&quot;true&quot;)
end
'
```
### 数字
Lua 的 number 类型是用双精度浮点数来实现的。值得一提的是LuaJIT 支持 `dual-number`(双数)模式,也就是说, LuaJIT 会根据上下文来用整型来存储整数,而用双精度浮点数来存放浮点数。
此外LuaJIT 还支持`长长整型`的大整数,比如下面的例子:
```
$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL
```
### 函数
函数在 Lua 中是一等公民,你可以把函数存放在一个变量中,也可以当作另外一个函数的入参和出参。
比如,下面两个函数的声明是完全等价的:
```
function foo()
end
```
```
foo = function ()
end
```
### table
table 是 Lua 中唯一的数据结构,自然非常重要,所以后面我会用专门的章节来介绍它。我们可以先来看一个简单的示例代码:
```
$ resty -e 'local color = {first = &quot;red&quot;}
print(color[&quot;first&quot;])'
red
```
### 空值
在 Lua 中,空值就是 nil。如果你定义了一个变量但没有赋值它的默认值就是 nil
```
$ resty -e 'local a
print(type(a))'
nil
```
当你真正进入 OpenResty 体系中后,会发现很多种空值,比如 `ngx.null` 等等,我们后面再细聊。
Lua的数据类型我主要就介绍这么多先给你打个基础。一些需要重点掌握的内容后面的文章中我们都会继续学习。在练习、使用中学习永远是吸收新知识最便捷的方式。
## 常用标准库
很多时候,我们学习一门语言,其实就是在学习它的标准库。
Lua 比较小巧,内置的标准库并不多。而且,在 OpenResty 的环境中Lua 标准库的优先级是很低的。对于同一个功能,我更推荐你优先使用 OpenResty 的 API 来解决,然后是 LuaJIT 的库函数,最后才是标准 Lua 的函数。
`OpenResty的API &gt; LuaJIT的库函数 &gt; 标准Lua的函数`,这个优先级后面会被反复提及,它不仅关系到是否好用这一点,更会对性能产生非常大的影响。
不过,尽管如此,在实际的项目开发中,我们还是不可避免会用到一些 Lua 库。这里,我挑选了几个比较常用的标准库做下介绍,如果你想要了解更多内容,可以查阅 Lua 的官方文档。
### string 库
字符串操作是我们最常用到的,也是坑最多的地方。有一个简单的原则,那就是如果涉及到正则表达式的,请一定要使用 OpenResty 提供的 `ngx.re.*` 来解决,不要用 Lua 的 `string.*` 处理。这是因为Lua 的正则独树一帜,不符合 PCRE 的规范,我相信绝大部分工程师是玩不转的。
其中 `string.byte(s [, i [, j ]])`,是比较常用到的一个 string 库函数,它返回字符 s[i]、s[i + 1]、s[i + 2]、······、s[j] 所对应的 ASCII 码。i 的默认值为 1即第一个字节j 的默认值为 i。
下面我们来看一段示例代码:
```
$ resty -e 'print(string.byte(&quot;abc&quot;, 1, 3))
print(string.byte(&quot;abc&quot;, 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte(&quot;abc&quot;)) -- 缺少第二个和第三个参数,此时这两个参数都默认为 1
'
```
它的输出为:
```
979899
99
97
```
### table 库
在 OpenResty 的上下文中对于Lua 自带的 table 库,除了 `table.concat``table.sort` 等少数几个函数,大部分我都不推荐使用。至于它们的细节,我们留在 LuaJIT 章节中专门来讲。
这里我简单提一下`table.concat``table.concat`一般用在字符串拼接的场景下,比如下面这个例子。它可以避免生成很多无用的字符串。
```
$ resty -e 'local a = {&quot;A&quot;, &quot;b&quot;, &quot;C&quot;}
print(table.concat(a))'
```
### math 库
Lua math 库由一组标准的数学函数构成。数学库的引入,既丰富了 Lua 编程语言的功能,同时也方便了程序的编写。
在 OpenResty 的实际项目中,我们很少用 Lua 去做数学方面的运算,不过其中和随机数相关的 `math.random()``math.randomseed()` 两个函数,倒是比较常用,比如下面的这段代码,它可以在指定的范围内,随机地生成两个数字。
```
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
```
## 虚变量
了解了这些常见的标准库,接下来,我们再来学习一个新的概念——虚变量。
设想这么一个场景,当一个函数返回多个值的时候,有些返回值我们并不需要,这时候,应该怎么接收这些值呢?
不知道你是怎么看待这件事的,起码对我来说,要想法设法给这些用不到的变量,去赋予有意义的名字,着实是一件很折磨人的事情。
还好, Lua 中可以完美地解决这一点。Lua 提供了一个虚变量dummy variable的概念 按照惯例以一个下划线来命名,用来表示丢弃不需要的数值,仅仅起到占位的作用。
下面我们以 `string.find` 这个标准库函数为例,来看虚变量的用法。这个标准库函数会返回两个值,分别代表开始和结束的下标。
如果我们只需要获取开始的下标,那么很简单,只声明一个变量来接收 `string.find` 的返回值即可:
```
$ resty -e 'local start = string.find(&quot;hello&quot;, &quot;he&quot;)
print(start)'
1
```
但如果你只想获取结束的下标,那就必须使用虚变量了:
```
$ resty -e 'local _, end_pos = string.find(&quot;hello&quot;, &quot;he&quot;)
print(end_pos)'
2
```
除了在返回值里使用,虚变量还经常用于循环中,比如下面这个例子:
```
$ resty -e 'for _, v in ipairs({4,5,6}) do
print(v)
end'
4
5
6
```
而当有多个返回值需要忽略时,你可以重复使用同一个虚变量。这里我就不举例子了,你可以试着自己写一个这样的示例代码吗?欢迎你把代码贴在留言区里和我分享、交流。
## 写在最后
今天,我们一起快速地学习了标准 Lua 的数据结构和语法,相信你对这门简单精巧的语言已经有了初步的了解。下节课,我会带你了解 Lua 和 LuaJIT 的关系LuaJIT 更是 OpenResty 中的重头戏,值得我们深入挖掘。
最后,我想再为你留下一道思考题。
还记得这节课讲math库时学过的这段代码吗它可以在指定范围内随机生成两个数字。
```
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
```
不过,你可能注意到了,这段代码是用当前时间戳作为种子的,那么这种方法是否有问题呢?又该如何生成好的种子呢?要知道,很多时候我们生成的随机数其实并不随机,并且有很大的安全隐患。
欢迎在留言区来说说你的看法,也欢迎你把这篇文章转发给你的同事、朋友。我们一起交流、一起进步。

View File

@@ -0,0 +1,190 @@
<audio id="audio" title="08 | LuaJIT分支和标准Lua有什么不同" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/25/93/257ae12a315f7eee7ac83f171c932e93.mp3"></audio>
你好,我是温铭。
这节课,我们来学习下 OpenResty 的另一块基石LuaJIT。今天主要的篇幅我会留给 Lua 和 LuaJIT 中重要和鲜为人知的一些知识点。而更多 Lua 语言的基础知识,你可以通过搜索引擎或者 Lua 的书籍自己来学习,这里我推荐 Lua 作者编写的《Lua 程序设计》这本书。
**当然,在 OpenResty 中,写出正确的 LuaJIT 代码的门槛并不高,但要写出高效的 LuaJIT 代码绝非易事**,这里的关键内容,我会在后面 OpenResty 性能优化部分详细介绍。
我们先来看下 LuaJIT 在 OpenResty 整体架构中的位置:
<img src="https://static001.geekbang.org/resource/image/cd/ef/cdef970a60810548b9c297e6959671ef.png" alt="">
前面我们提到过OpenResty 的 worker 进程都是 fork master 进程而得到的, 其实, master 进程中的 LuaJIT 虚拟机也会一起 fork 过来。在同一个 worker 内的所有协程,都会共享这个 LuaJIT 虚拟机Lua 代码的执行也是在这个虚拟机中完成的。
这可以算是 OpenResty 的基本原理,后面课程我们再详细聊聊。今天我们先来理顺 Lua 和 LuaJIT 的关系。
## 标准 Lua 和 LuaJIT 的关系
先把重要的事情放在前面说:
**标准 Lua 和 LuaJIT 是两回事儿LuaJIT 只是兼容了 Lua 5.1 的语法。**
标准 Lua 现在的最新版本是 5.3LuaJIT 的最新版本则是 2.1.0-beta3。在 OpenResty 几年前的老版本中,编译的时候,你可以选择使用标准 Lua VM ,或者 LuaJIT VM 来作为执行环境,不过,现在已经去掉了对标准 Lua 的支持,只支持 LuaJIT。
LuaJIT 的语法兼容 Lua 5.1,并对 Lua 5.2 和 5.3 做了选择性支持。所以我们应该先学习 Lua 5.1 的语法,并在此基础上学习 LuaJIT 的特性。上节课我已经带你入门了 Lua的基础语法今天只提及Lua的一些特别之处。
值得注意的是OpenResty 并没有直接使用 LuaJIT 官方提供的 2.1.0-beta3 版本,而是在此基础上,扩展了自己的 fork: [openresty-luajit2]
>
OpenResty 维护了自己的 LuaJIT 分支,并扩展了很多独有的 API。
这些独有的 API都是在实际开发 OpenResty 的过程中,出于性能方面的考虑而增加的。**所以,我们后面提到的 LuaJIT特指 OpenResty 自己维护的 LuaJIT 分支。**
## 为什么选择 LuaJIT
说了这么多 LuaJIT和Lua 的关系你可能会纳闷儿为什么不直接使用Lua而是要用自己维护的LuaJIT呢其实最主要的原因还是LuaJIT的性能优势。
其实标准 Lua 出于性能考虑,也内置了虚拟机,所以 Lua 代码并不是直接被解释执行的,而是先由 Lua 编译器编译为字节码Byte Code然后再由 Lua 虚拟机执行。
而 LuaJIT 的运行时环境,除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。开始的时候LuaJIT和标准 Lua 一样Lua 代码被编译为字节码,字节码被 LuaJIT 的解释器解释执行。
但不同的是LuaJIT的解释器会在执行字节码的同时记录一些运行时的统计信息比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。当这些次数超过某个随机的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够热,这时便会触发 JIT 编译器开始工作。
JIT 编译器会从热函数的入口或者热循环的某个位置开始,尝试编译对应的 Lua 代码路径。编译的过程,是把 LuaJIT 字节码先转换成LuaJIT 自己定义的中间码IR然后再生成针对目标体系结构的机器码。
所以,**所谓 LuaJIT 的性能优化,本质上就是让尽可能多的 Lua 代码可以被 JIT 编译器生成机器码,而不是回退到 Lua 解释器的解释执行模式**。明白了这个道理你才能理解后面学到的OpenResty 性能优化的本质。
## Lua 特别之处
正如我们上节课介绍的一样Lua 语言相对简单。对于有其他开发语言背景的工程师来说,注意 到Lua 中一些独特的地方后你就能很容易的看懂代码逻辑。接下来我们一起来看Lua语言比较特别的几个地方。
### 1. Lua 的下标从 1 开始
Lua 是我知道的唯一一个下标从 1 开始的编程语言。这一点,虽然对于非程序员背景的人来说更好理解,但却容易导致程序的 bug。
下面是一个例子:
```
$ resty -e 't={100}; ngx.say(t[0])'
```
你自然期望打印出 `100`,或者报错说下标 0 不存在。但结果出乎意料,什么都没有打印出来,也没有报错。既然如此,让我们加上 `type` 命令,来看下输出到底是什么:
```
$ resty -e 't={100};ngx.say(type(t[0]))'
nil
```
原来是空值。事实上,在 OpenResty 中,对于空值的判断和处理也是一个容易让人迷惑的点,后面我们讲到 OpenResty 的时候再细聊。
### 2. 使用 `..` 来拼接字符串
这一点,上节课我也提到过。和大部分语言使用 `+` 不同Lua 中使用两个点号来拼接字符串:
```
$ resty -e &quot;ngx.say('hello' .. ', world')&quot;
hello, world
```
在实际的项目开发中我们一般都会使用多种开发语言而Lua 这种不走寻常路的设计,总是会让开发者的思维,在字符串拼接的时候卡顿一下,也是让人哭笑不得。
### 3. 只有 `table` 这一种数据结构
不同于 Python 这种内置数据结构丰富的语言Lua 中只有一种数据结构,那就是 table它里面可以包括数组和哈希表
```
local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
print(color[&quot;first&quot;]) --&gt; output: red
print(color[1]) --&gt; output: blue
print(color[&quot;third&quot;]) --&gt; output: green
print(color[2]) --&gt; output: yellow
print(color[3]) --&gt; output: nil
```
如果不显式地用`_键值对_`的方式赋值table 就会默认用数字作为下标,从 1 开始。所以 `color[1]` 就是 blue。
另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:
```
local t1 = { 1, 2, 3 }
print(&quot;Test1 &quot; .. table.getn(t1))
local t2 = { 1, a = 2, 3 }
print(&quot;Test2 &quot; .. table.getn(t2))
local t3 = { 1, nil }
print(&quot;Test3 &quot; .. table.getn(t3))
local t4 = { 1, nil, 2 }
print(&quot;Test4 &quot; .. table.getn(t4))
```
使用 `resty` 运行的结果如下:
```
Test1 3
Test2 2
Test3 1
Test4 1
```
你可以看到,除了第一个返回长度为 3 的测试案例外后面的测试都是我们预期之外的结果。事实上想要在Lua 中获取 table 长度,必须注意到,只有在 table 是 `_序列_` 的时候,才能返回正确的值。
那什么是序列呢首先序列是数组array的子集也就是说table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。
其次序列中不包含空洞hole即 nil。综合这两点来看上面的 table 中, t1 是一个序列,而 t3 和 t4 是 array却不是序列sequence
到这里,你可能还有一个疑问,为什么 t4 的长度会是 1 呢?其实这是因为,在遇到 nil 时,获取长度的逻辑就不继续往下运行,而是直接返回了。
不知道你完全看懂了吗?这部分确实相当复杂。那么有没有什么办法可以获取到我们想要的 table 长度呢自然是有的OpenResty 在这方面做了扩展,在后面专门的 table 章节我会讲到,这里先留一个悬念。
### 4. 默认是全局变量
我想先强调一点,除非你相当确定,否则在 Lua 中声明变量时,前面都要加上 `local`
```
local s = 'hello'
```
这是因为在 Lua 中,变量默认是全局的,会被放到名为 `_G` 的 table 中。不加 local 的变量会在全局表中查找,这是昂贵的操作。如果再加上一些变量名的拼写错误,就会造成难以定位的 bug。
所以,在 OpenResty 编程中,我强烈建议你总是使用 `local` 来声明变量,即使在 require module 的时候也是一样:
```
-- Recommended
local xxx = require('xxx')
-- Avoid
require('xxx')
```
## LuaJIT
明白了Lua这四点特别之处我们继续来说LuaJIT。除了兼容 Lua 5.1 的语法并支持 JIT 外LuaJIT 还紧密结合了 FFIForeign Function Interface可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。
下面是一个最简单的例子:
```
local ffi = require(&quot;ffi&quot;)
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf(&quot;Hello %s!&quot;, &quot;world&quot;)
```
短短这几行代码,就可以直接在 Lua 中调用 C 的 `printf` 函数,打印出 `Hello world!`。你可以使用 `resty` 命令来运行它,看下是否成功。
类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数来完成更多的功能。实际上FFI 方式比传统的 Lua/C API 方式的性能更优,这也是 `lua-resty-core` 项目存在的意义。下一节我们就来专门讲讲 FFI 和 `lua-resty-core`
此外出于性能方面的考虑LuaJIT 还扩展了 table 的相关函数:`table.new``table.clear`。**这是两个在性能优化方面非常重要的函数**,在 OpenResty 的 lua-resty 库中会被频繁使用。不过,由于相关文档藏得非常深,而且没有示例代码,所以熟悉它们的开发者并不多。我们留到性能优化章节专门来讲它们。
## 写在最后
让我们来回顾下今天的内容。
OpenResty 出于性能的考虑,选择了 LuaJIT 而不是标准 Lua并且维护了自己的 LuaJIT 分支。而 LuaJIT 基于 Lua 5.1 的语法,并选择性地兼容了部分 Lua5.2 和 Lua5.3 的语法形成了自己的体系。至于你需要掌握的Lua 语法,在下标、字符串拼接、数据结构和变量上,都有自己鲜明的特点,在写代码的时候你应该特别留意。
你在学习 Lua 和 LuaJIT 的时候,是否遇到一些陷阱和坑呢?欢迎留言一起来聊一聊,我在后面也专门写了一篇文章,来分享我遇到过的那些坑。也欢迎你把这篇文章分享给你的同事、朋友,一起学习,一起进步。

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="09 | 为什么 lua-resty-core 性能更高一些?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/03/9df43767fc424ac63ecff81a69748703.mp3"></audio>
你好,我是温铭。
前面两节课我们说了Lua 是一种嵌入式开发语言,核心保持了短小精悍,你可以在 Redis、NGINX 中嵌入 Lua来帮助你更灵活地完成业务逻辑。同时Lua 也可以调用已有的 C 函数和数据结构,避免重复造轮子。
在 Lua 中,你可以用 Lua C API 来调用 C 函数,而在 LuaJIT 中还可以使用 FFI。对 OpenResty 而言:
- 在核心的 `lua-nginx-module` 中,调用 C 函数的 API都是使用 Lua C API 来完成的;
- 而在 `lua-resty-core` 中,则是把 `lua-nginx-module` 已有的部分 API使用 FFI 的模式重新实现了一遍。
看到这里你估计纳闷了:为什么要用 FFI 重新实现一遍?
别着急,让我们以 [ngx.base64_decode](https://github.com/openresty/lua-nginx-module#ngxdecode_base64) 这个很简单的 API 为例,一起看下 Lua C API 和 FFI 的实现有何不同之处,这样你也可以对它们的性能有个直观的认识。
## Lua CFunction
我们先来看下, `lua-nginx-module` 中用 Lua C API 是如何实现的。我们在项目的代码中搜索 `decode_base64`,可以找到它的代码实现在 `ngx_http_lua_string.c` 中:
```
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64);
lua_setfield(L, -2, &quot;decode_base64&quot;);
```
上面的代码看着就头大,不过还好,我们不用深究那两个 `lua_` 开头的函数,以及它们参数的具体作用,只需要知道一点——这里注册了一个 CFunction`ngx_http_lua_ngx_decode_base64` 而它与 `ngx.base64_decode` 这个对外暴露的 API 是对应关系。
我们继续“按图索骥”,在这个 C 文件中搜索 `ngx_http_lua_ngx_decode_base64`,它定义在文件的开始位置:
```
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
```
对于那些能够被 Lua 调用的 C 函数来说,它的接口必须遵循 Lua 要求的形式,也就是 `typedef int (*lua_CFunction)(lua_State* L)`。它包含的参数是 `lua_State` 类型的指针 L ;它的返回值类型是一个整型,表示返回值的数量,而非返回值自身。
它的实现如下(这里我已经去掉了错误处理的代码):
```
static int
ngx_http_lua_ngx_decode_base64(lua_State *L)
{
ngx_str_t p, src;
src.data = (u_char *) luaL_checklstring(L, 1, &amp;src.len);
p.len = ngx_base64_decoded_length(src.len);
p.data = lua_newuserdata(L, p.len);
if (ngx_decode_base64(&amp;p, &amp;src) == NGX_OK) {
lua_pushlstring(L, (char *) p.data, p.len);
} else {
lua_pushnil(L);
}
return 1;
}
```
这段代码中,最主要的是 `ngx_base64_decoded_length``ngx_decode_base64` 它们都是 NGINX 自身提供的 C 函数。
我们知道,用 C 编写的函数,无法把返回值传给 Lua 代码,而是需要通过栈,来传递 Lua 和 C 之间的调用参数和返回值。这也是为什么,会有很多我们一眼无法看懂的代码。同时,这些代码也不能被 JIT 跟踪到,所以对于 LuaJIT 而言,这些操作是处于黑盒中的,没法进行优化。
## LuaJIT FFI
而 FFI 则不同。FFI 的交互部分是用 Lua 实现的,这部分代码可以被 JIT 跟踪到,并进行优化;当然,代码也会更加简洁易懂。
我们还是以 `base64_decode`为例,它的 FFI 实现分散在两个仓库中: `lua-resty-core``lua-nginx-module`。我们先来看下前者里面[实现的代码](https://github.com/openresty/lua-resty-core/blob/master/lib/resty/core/base64.lua#L72)
```
ngx.decode_base64 = function (s)
local slen = #s
local dlen = base64_decoded_length(slen)
local dst = get_string_buf(dlen)
local pdlen = get_size_ptr()
local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen)
if ok == 0 then
return nil
end
return ffi_string(dst, pdlen[0])
end
```
你会发现,相比 CFunctionFFI 实现的代码清爽了很多,它具体的实现是 `lua-nginx-module` 仓库中的`ngx_http_lua_ffi_decode_base64`,如果你对这里感兴趣,可以自己去查看这个函数的实现,特别简单,这里我就不贴代码了。
不过,细心的你,是否从上面的代码片段中,发现函数命名的一些规律了呢?
没错OpenResty 中的函数都是有命名规范的,你可以通过命名推测出它的用处。比如:
- `ngx_http_lua_ffi_` ,是用 FFI 来处理 NGINX HTTP 请求的 Lua 函数;
- `ngx_http_lua_ngx_` ,是用 Cfunction 来处理 NGINX HTTP 请求的 Lua 函数;
- 其他 `ngx_``lua_` 开头的函数,则分别属于 NGINX 和 Lua 的内置函数。
更进一步OpenResty 中的 C 代码,也有着严格的代码规范,这里我推荐阅读[官方的 C 代码风格指南](https://openresty.org/cn/c-coding-style-guide.html)。对于有意学习 OpenResty 的 C 代码并提交 PR 的开发者来说,这是必备的一篇文档。否则,即使你的 PR 写得再好,也会因为代码风格问题被反复评论并要求修改。
关于 FFI 更多的 API 和细节,推荐你阅读 LuaJIT [官方的教程](http://luajit.org/ext_ffi_tutorial.html) 和 [文档](http://luajit.org/ext_ffi_api.html)。技术专栏并不能代替官方文档,我也只能在有限的时间内帮你指出学习的路径,少走一些弯路,硬骨头还是需要你自己去啃的。
## LuaJIT FFI GC
使用 FFI 的时候,我们可能会迷惑:在 FFI 中申请的内存,到底由谁来管理呢?是应该我们在 C 里面手动释放,还是 LuaJIT 自动回收呢?
这里有个简单的原则LuaJIT 只负责由自己分配的资源;而 `ffi.C` 是 C 库的命名空间,所以,使用 `ffi.C` 分配的空间不由 LuaJIT 负责,需要你自己手动释放。
举个例子,比如你使用 `ffi.C.malloc` 申请了一块内存,那你就需要用配对的 `ffi.C.free` 来释放。LuaJIT 的官方文档中有一个对应的示例:
```
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
...
p = nil -- Last reference to p is gone.
-- GC will eventually run finalizer: ffi.C.free(p)
```
这段代码中,`ffi.C.malloc(n)` 申请了一段内存,同时 `ffi.gc` 就给它注册了一个析构的回调函数 `ffi.C.free`。这样一来,`p` 这个 `cdata` 在被 LuaJIT GC 的时候,就会自动调用 `ffi.C.free`,来释放 C 级别的内存。而 `cdata` 是由 LuaJIT 负责 GC的 ,所以上述代码中的 `p` 会被 LuaJIT 自动释放。
这里要注意,如果你要在 OpenResty 中申请大块的内存,我更推荐你用 `ffi.C.malloc` 而不是 `ffi.new`。原因也很明显:
1. `ffi.new` 返回的是一个 `cdata`,这部分内存由 LuaJIT 管理;
1. LuaJIT GC 的管理内存是有上限的OpenResty 中的 LuaJIT 并未开启 GC64 选项,所以**单个 worker 内存的上限只有2G**。一旦超过 LuaJIT 的内存管理上限,就会导致报错。
**在使用 FFI 的时候,我们还需要特别注意内存泄漏的问题**。不过,凡人皆会犯错,只要是人写的代码,百密一疏,总会出现 bug。那么有没有什么工具可以检测内存泄漏呢
这时候OpenResty 强大的周边测试和调试工具链就派上用场了。
我们先来说说测试。在 OpenResty 体系中,我们使用 Valgrind 来检测内存泄漏问题。
前面课程我们提到过的测试框架 `test::nginx`,有专门的内存泄漏检测模式去运行单元测试案例集,你只需要设置环境变量 `TEST_NGINX_USE_VALGRIND=1` 即可。OpenResty 的官方项目在发版本之前,都会在这个模式下完整回归,后面的测试章节中我们再详细介绍。
而 OpenResty 的 CLI `resty` 也有 `--valgrind` 选项,方便你单独运行某段 Lua 代码,即使你没有写测试案例也是没问题的。
再来看调试工具。
OpenResty 提供[基于 systemtap 的扩展](https://github.com/openresty/stapxx),来对 OpenResty 程序进行活体的动态分析。你可以在这个项目的工具集中,搜索 `gc` 这个关键字,会看到 `lj-gc``lj-gc-objs` 这两个工具。
而对于 core dump 这种离线分析OpenResty 提供了 [GDB 的工具集](https://github.com/openresty/openresty-gdb-utils),同样你可以在里面搜索 `gc`,找到 `lgc``lgcstat``lgcpath` 三个工具。
这些调试工具的具体用法我们会在后面的调试章节中详细介绍你先有个印象即可。这样你遇到内存问题就不会“病急乱投医“毕竟OpenResty 有专门的工具集,帮你定位和解决这些问题。
## lua-resty-core
从上面的比较中我们可以看到FFI 的方式不仅代码更简洁,而且可以被 LuaJIT 优化显然是更优的选择。其实现实也是如此实际上CFunction 的实现方式已经被 OpenResty 废弃,相关的实现也从代码库中移除了。现在新的 API都通过 FFI 的方式,在 `lua-resty-core` 仓库中实现。
在 OpenResty 2019 年 5 月份发布的 1.15.8.1 版本前,`lua-resty-core` 默认是不开启的,而这不仅会带来性能损失,更严重的是会造成潜在的 bug。所以我强烈推荐还在使用历史版本的用户都手动开启 `lua-resty-core`。你只需要在 `init_by_lua` 阶段,增加一行代码就可以了:
```
require &quot;resty.core&quot;
```
当然,姗姗来迟的 1.15.8.1 版本中,已经增加了 `lua_load_resty_core` 指令,默认开启了 `lua-resty-core`。我个人感觉OpenResty 对于 `lua-resty-core` 的开启还是过于谨慎了,开源项目应该尽早把类似的功能设置为默认开启。
`lua-resty-core` 中不仅重新实现了部分 lua-nginx-module 项目中的 API比如 `ngx.re.match``ngx.md5` 等,还实现了不少新的 API比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore 等等,我们在后面的 OpenResty API 章节中会介绍到。
## 写在最后
讲了这么多内容最后我还是想说FFI 虽然好,却也并不是性能银弹。它之所以高效,主要原因就是可以被 JIT 追踪并优化。如果你写的 Lua 代码不能被 JIT而是需要在解释模式下执行那么 FFI 的效率反而会更低。
那么到底有哪些操作可以被 JIT哪些不能呢怎样才可以避免写出不能被 JIT 的代码呢?下一节我来揭晓这个问题。
最后给你留一个需要动手的作业题你可以找一两个lua-nginx-module 和 lua-resty-core 中都存在的 API然后性能测试比较一下两者的差异吗你可以看下 FFI 的性能提升到底有多大。
欢迎留言和我分享你的思考、收获,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。

View File

@@ -0,0 +1,205 @@
<audio id="audio" title="10 | JIT编译器的死穴为什么要避免使用 NYI " controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/65/8881c2d03baebc07deba8f2295dd5f65.mp3"></audio>
你好,我是温铭。
上一节,我们一起了解了 LuaJIT 中的 FFI。如果你的项目中只用到了 OpenResty 提供的 API没有自己调用 C 函数的需求,那么 FFI 对你而言并没有那么重要,你只需要确保开启了 `lua-resty-core` 即可。
但我们今天要讲的 LuaJIT 中 NYI却是每一个使用 OpenResty 的工程师都逃避不了的关键问题,它对于性能的影响举足轻重。
**你可以很快使用 OpenResty 写出逻辑正确的代码,但不明白 NYI你就不能写出高效的代码无法发挥 OpenResty 真正的威力**。这两者的性能差距,至少是一个数量级的。
## 什么是 NYI
那究竟什么是 NYI 呢?先回顾下我们之前提到过的一个知识点:
**LuaJIT 的运行时环境,除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。**
LuaJIT 中 JIT 编译器的实现还不完善,有一些原语它还无法编译,因为这些原语实现起来比较困难,再加上 LuaJIT 的作者目前处于半退休状态。这些原语包括常见的 pairs() 函数、unpack() 函数、基于 Lua CFunction 实现的 Lua C 模块等。这样一来,当 JIT 编译器在当前代码路径上遇到它不支持的操作时,便会退回到解释器模式。
而JIT 编译器不支持的这些原语,其实就是我们今天要讲的 NYI全称为Not Yet Implemented。LuaJIT 的官网上有[这些 NYI 的完整列表](http://wiki.luajit.org/NYI),建议你仔细浏览一遍。当然,目的不是让你背下这个列表的内容,而是让你要在写代码的时候有意识地提醒自己。
下面,我截取了 NYI 列表中 string 库的几个函数:
<img src="https://static001.geekbang.org/resource/image/1b/91/1b15183f8282ce235379281a961bd991.png" alt="">
其中,`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(&quot;banana&quot;, &quot;a&quot;, &quot;A&quot;); print(new)'
bAnAnA
```
这个函数是一个 NYI 原语,无法被 JIT 编译。
我们可以尝试在 OpenResty 自己的 API 中寻找替代函数,但对于大多数人来说,记住所有的 API 和用法是不现实的。所以在平时开发中,我都会打开 lua-nginx-module 的 [GitHub 文档页面](https://github.com/openresty/lua-nginx-module)。
比如,针对刚刚的这个例子,我们可以用 `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(&quot;foo bar&quot;, &quot;foo&quot;, 1, true)
```
如果是后者,你应该用 OpenResty 自己的 API并开启 PCRE 的 JIT 选项:
```
ngx.re.find(&quot;foo bar&quot;, &quot;^foo&quot;, &quot;jo&quot;)
```
其实,**这里更适合做一层封装,并把优化选项默认打开,不要让最终的使用者知道这么多细节**。这样,对外就是统一的字符串查找函数了。你可以感受到,有时候选择太多、太灵活并不是一件好事。
### 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 本身,你可以参考[它的源码](https://github.com/openresty/luajit2/blob/v2.1-agentzh/src/jit/dump.lua)来做更深入的了解;后者的输出比较简单,每行对应一个 trace通常用来检测是否可以被 JIT。
具体应该怎么操作呢?
我们可以先在 `init_by_lua` 中,添加以下两行代码:
```
local v = require &quot;jit.v&quot;
v.on(&quot;/tmp/jit.log&quot;)
```
然后,运行你自己的压力测试工具,或者跑几百个单元测试集,让 LuaJIT 足够热,触发 JIT 编译。这些都完成后,再来检查 `/tmp/jit.log` 的结果。
当然,这个方法相对比较繁琐,如果你想要简单验证的话, 使用 `resty` 就足够了,这个 OpenResty 的 CLI 带有相关选项:
```
$resty -j v -e 'for i=1, 1000 do
local newstr, n, err = ngx.re.gsub(&quot;hello, world&quot;, &quot;([a-z])[a-z]+&quot;, &quot;[$0,$1]&quot;, &quot;i&quot;)
end'
[TRACE 1 (command line -e):1 stitch C:107bc91fd]
[TRACE 2 (1/stitch) (command line -e):2 -&gt; 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() 函数的替代方案时,我有提到过,那里其实**更适合做一层封装,并默认打开优化选项**。那么,这个任务就交给你来小试牛刀了。
欢迎在留言区写下你的答案,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。

View File

@@ -0,0 +1,397 @@
<audio id="audio" title="11 | 剖析Lua唯一的数据结构table和metatable特性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/37/b7/37b91fa264c2af0acabd16235e1472b7.mp3"></audio>
你好我是温铭。今天我们一起学习下LuaJIT 中唯一的数据结构:`table`
和其他具有丰富数据结构的脚本语言不同LuaJIT 中只有 `table` 这一个数据结构,并没有区分开数组、哈希、集合等概念,而是揉在了一起。让我们先温习下之前提到过的一个例子:
```
local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
print(color[&quot;first&quot;]) --&gt; output: red
print(color[1]) --&gt; output: blue
print(color[&quot;third&quot;]) --&gt; output: green
print(color[2]) --&gt; output: yellow
print(color[3]) --&gt; output: nil
```
这个例子中, `color` 这个 table 包含了数组和哈希,并且可以互不干扰地进行访问。比如,你可以用 `ipairs` 函数,只遍历数组部分的内容:
```
$ resty -e 'local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
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 = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
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 = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
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 = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
print(table.concat(color, &quot;, &quot;))'
```
使用`table.concat`函数后,它输出的是 `blue, yellow`,哈希的部分被跳过了。
另外,这个函数还可以指定下标的起始位置来做拼接,比如下面这样的写法:
```
$ resty -e 'local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;, &quot;orange&quot;}
print(table.concat(color, &quot;, &quot;, 2, 3))'
```
这次输出是 `yellow, orange`,跳过了 `blue`
你可能觉得这些操作还挺简单的,不过,我要说的是,函数不可貌相,海水不可。千万不要小看这个看上去没有太大用处的函数,在做性能优化时,它却会有意想不到的作用,也是我们后面性能优化章节中的主角之一。
### `table.insert` 插入一个元素
最后我们来看`table.insert` 函数。它可以下标插入一个新的元素,自然,影响的还是 table 的数组部分。还是用`color`例子来说明:
```
$ resty -e 'local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
table.insert(color, 1, &quot;orange&quot;)
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 项目的[扩展文档](https://github.com/openresty/luajit2/blob/v2.1-agentzh/doc/extensions.html)中,即使你用谷歌也难觅其踪迹,所以知道的工程师并不多。
下面是一个简单的例子,我来带你看下它该怎么用。首先要说明,这个函数是扩展出来的,所以在使用它之前,你需要先 `require` 一下:
```
local new_tab = require &quot;table.new&quot;
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 &quot;table.new&quot;
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 &quot;table.clear&quot;
local color = {first = &quot;red&quot;, &quot;blue&quot;, third = &quot;green&quot;, &quot;yellow&quot;}
clear_tab(color)
for k, v in pairs(color) do
print(k)
end'
```
不过,事实上,能使用这个函数的场景并不算多,大多数情况下,我们还是应该把这个任务交给 LuaJIT GC 去完成。
## OpenResty 的 table 扩展函数
开头我提到过OpenResty 自己维护的 LuaJIT 分支,也对 table 做了扩展,它[新增了几个 API](https://github.com/openresty/luajit2/#new-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 &quot;table.nkeys&quot;
print(nkeys({})) -- 0
print(nkeys({ &quot;a&quot;, nil, &quot;b&quot; })) -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil })) -- 2
print(nkeys({ &quot;a&quot;, 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(&quot;%d.%d.%d&quot;, 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 == &quot;patch&quot; then
return 2
end
end,
__tostring = function(t)
return string.format(&quot;%d.%d.%d&quot;, 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(&quot;%d.%d.%d&quot;, 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(&quot;%d.%d.%d&quot;, 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(&quot;%d.%d.%d&quot;, t.major, t.minor, t.patch)
end
})
print(getmetatable(version).__index.patch)
'
```
自然,除了今天讲到的这三个元方法外,还有一些不经常使用的元方法,你可以在遇到的时候再去查阅[文档](http://lua-users.org/wiki/MetamethodsTutorial)了解。
## 面向对象
最后我们来聊聊面向对象。你可能知道Lua 并不是一个面向对象Object Orientation的语言但我们可以使用 metatable 来实现 OO。
我们来看一个实际的例子。[lua-resty-mysql](https://github.com/openresty/lua-resty-mysql/blob/master/lib/resty/mysql.lua) 是 OpenResty 官方的 MySQL 客户端,里面就使用元表**模拟**了类和类方法,它的使用方式如下所示:
```
$ resty -e 'local mysql = require &quot;resty.mysql&quot; -- 先引用 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, &quot;not initialized&quot;
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 来做一层封装呢?欢迎在留言区一起讨论这个问题,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。

View File

@@ -0,0 +1,334 @@
<audio id="audio" title="12 | 高手秘诀识别Lua的独有概念和坑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f7/14/f738771f4d119db9326fc0607719e414.mp3"></audio>
你好,我是温铭。
上一节中,我们一起了解了 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(&quot;func&quot;) 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(&quot;func&quot;) end
setmetatable(tb, {__mode = &quot;v&quot;})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 0
'
```
可以看到,没有被使用的对象都被 GC 了。这其中,最重要的就是下面这一行代码:
```
setmetatable(tb, {__mode = &quot;v&quot;})
```
是不是似曾相识?这不就是元表的操作吗!没错,当一个 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(&quot;func&quot;) end
setmetatable(tb, {__mode = &quot;k&quot;})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 2
'
```
请注意,这里我们只演示了 `value` 为弱引用的弱表,也就是数组类型的弱表。自然,你同样可以把对象作为 `key`,来构建哈希表类型的弱表,比如下面这样写:
```
$ resty -e 'local tb = {}
tb[{color = red}] = &quot;red&quot;
local fc = function() print(&quot;func&quot;) end
tb[fc] = &quot;func&quot;
fc = nil
setmetatable(tb, {__mode = &quot;k&quot;})
for k,v in pairs(tb) do
print(v)
end
collectgarbage()
print(&quot;----------&quot;)
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(&quot;func&quot;) end
```
其实就是把一个匿名函数,作为 table 的值给存储了起来。
在 Lua 中,下面这段代码中动两个函数的定义是完全等价的。不过注意,后者是把函数赋值给一个变量,这也是我们经常会用到的一种方式:
```
local function foo() print(&quot;foo&quot;) end
local foo = fuction() print(&quot;foo&quot;) 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(&quot;char[?]&quot;, 128)
```
所以,如果你要访问上面这段代码中 `buf` 这个 cdata请记得下标从 0 开始,而不是 1。在使用 FFI 和 C 交互的时候,一定要特别注意这个地方。
### 正则模式匹配
第二个坑正则模式匹配问题。OpenResty 中并行着两套字符串匹配方法Lua 自带的 `sting` 库,以及 OpenResty 提供的 `ngx.re.*` API。
其中, Lua 正则模式匹配是自己独有的格式,和 PCRE 的写法不同。下面是一个简单的示例:
```
resty -e 'print(string.match(&quot;foo 123 bar&quot;, &quot;%d%d%d&quot;))' — 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 &quot;cjson&quot;
local t = {}
print(cjson.encode(t))
'
```
比如上面这段代码,它的输出是 `{}`,由此可见, OpenResty 的 cjson 库,默认把空 table 当做字典来编码。当然,我们可以通过 `encode_empty_table_as_object` 这个函数,来修改这个全局的默认值:
```
resty -e 'local cjson = require &quot;cjson&quot;
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 &quot;cjson&quot;
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 &quot;cjson&quot;
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 这个阈值是从何而来的呢?这算是我们今天的思考题,欢迎你留言说下你的看法,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。

View File

@@ -0,0 +1,33 @@
<video poster="https://static001.geekbang.org/resource/image/6a/f7/6ada085b44eddf37506b25ad188541f7.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/30d99c0d-16d14089303-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/2ce11b32e3e740ff9580185d8c972303/a01ad13390fe4afe8856df5fb5d284a2-f2f547049c69fa0d4502ab36d42ea2fa-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/2ce11b32e3e740ff9580185d8c972303/a01ad13390fe4afe8856df5fb5d284a2-2528b0077e78173fd8892de4d7b8c96d-hd.m3u8" type="application/x-mpegURL"></video>
你好,我是温铭。
今天的内容,我同样会以视频的形式来讲解。不过,在你进行视频学习之前,我想先问你这么几个问题:
- lua-resty-lrucache 内部最重要的数据结构是什么?
- lua-resty-lrucache 有两种 FFI 的实现,我们今天讲的这一种更适合什么场景?
这几个问题,也是今天视频课要解决的核心内容,希望你可以先自己思考一下,并带着问题来学习今天的视频内容。
同时,我会给出相应的文字介绍,方便你在听完视频内容后,及时总结与复习。下面是今天这节课的文字介绍部分。
## 今日核心
[lua-resty-lrucache](https://github.com/openresty/lua-resty-lrucache) 是一个使用 LuaJIT FFI 实现的 LRU 缓存库,可以在 worker 内缓存各种类型的数据。功能与之类似的是 shared dict但 shared dict 只能存储字符串类型的数据。在大多数实际情况下这两种缓存是配合在一起使用的——lrucache 作为一级缓存shared dict 作为二级缓存。
lrucache 的实现,并没有涉及到 OpenResty 的 Lua API。所以即使你以前没有用过OpenResty也可以通过这个项目来学习如何使用 LuaJIT 的 FFI。
lrucache 仓库中包含了两种实现方案,一种是使用 Lua table 来实现缓存,另外一种则是使用 hash 表来实现。前者更适合命中率高的情况,后者适合命中率低的情况。两个方案没有哪个更好,要看你的线上环境更适合哪一个。
通过今天这个项目,你可以弄清楚要如何使用 FFI并了解一个完整的 lua-resty 库应该包括哪些必要的内容。当然,我顺道也会介绍下 travis 的使用。
最后,还是想强调一点,在你面对一个陌生的开源项目时,文档和测试案例永远是最好的上手方式。而你后期如果要阅读源码,也不要先去抠细节,而是应该先去看主要的数据结构,围绕重点逐层深入。
## 课件参考
今天的课件已经上传到了我的GitHub上你可以自己下载学习。
链接如下:[https://github.com/iresty/geektime-slides](https://github.com/iresty/geektime-slides)
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="14 | 答疑Lua 规则和 NGINX 配置文件产生冲突怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9c/e5/9c1350a183922cf8c6cf71127c15e1e5.mp3"></audio>
你好,我是温铭。
专栏更新到现在OpenResty第一版块入门篇我们就已经学完了。恭喜你没有掉队仍然在积极学习和实践操作并且热情地留下了你的思考。
很多留言提出的问题很有价值大部分我都已经在app里回复过一些手机上不方便回复的或者比较典型、有趣的问题我专门摘了出来作为今天的答疑内容集中回复。另一方面也是为了保证所有人都能不漏掉任何一个重点。
下面我们来看今天的这5个问题。
## 第一问OpenResty的名字和语言
Q看到现在我还没看懂 OpenResty 这个名字的来历。另外OpenResty 借助 Lua 语言,插上翅膀,那么为什么不借助其他脚本语言呢?比如 Shell 等。
A事实上OpenResty 最早是雅虎中国的一个公司项目,起步于 2007 年 10 月。当时兴起了 OpenAPI 的热潮,于是春哥想做一个类似的东西,可以支持各种 Web Service 的需求。Open 这个名字取自 OpenAPI Resty 则是取自 rest API。最初 OpenResty 的目的,并非是做 web 服务器和开发平台,而是做类似网站这样的应用。
OpenResty 在十几年前开源的时候,支持同步非阻塞的语言凤毛麟角。即使是到了现在,后端语言可以达到 OpenResty 这种性能级别的也不多。当前,更多的开发者把 OpenResty 用在 API 网关和软 WAF 领域,这也算是开发者的自然选择了。
至于语言方面OpenResty 并不是唯一一个把其他开发语言嵌入NGINX 的项目。比如NGINX 官方就把 JS 嵌入了进来;同时也有一些开源项目,把 PHP 嵌入 NGINX。
通常来说选择借助哪一门语言会综合考虑协程、JIT和语言普及度等多种因素。对于OpenResty在 2007 年时Lua 确实是最佳的选择。实际上OpenResty 在最早的版本中选择了 perl 而不是 Lua也可以说是走了一段弯路。
## 第二问,配置文件的规则优先级
Q当 OpenResty 中的 Lua 规则和 NGINX 配置文件产生冲突时比如NGINX配置了rewrite规则又同时引用了rewrite_by_lua_file那么这两条规则的优先级是什么
A其实这个具体要看 NGINX 配置的 rewrite 规则是怎么写的了,是 break 还是 last。这一点在 OpenResty 的官方文档中有注明,并且配了一个示例代码:
```
location /foo {
rewrite ^ /bar;
rewrite_by_lua 'ngx.exit(503)';
}
location /bar {
...
}
```
在示例代码的这个配置中ngx.exit(503) 是不会被执行的。
但是如果你改成下面这样的写法ngx.exit(503) 就可以被执行。
```
rewrite ^ /bar break
```
不过,为了避免这种歧义,我还是建议都使用 OpenResty 来处理 rewrite而不是 NGINX 的配置。说实话NGINX 的很多配置是比较晦涩的,需要你反复查阅文档才能读懂。
## 第三问,我的代码为什么报错?
Q在LuaJIT 扩展的table 函数中,为什么下面这两行代码用 LuaJIT 去执行,都会报错“找不到 moudule”呢我用的LuaJIT 为 2.0.5版本。
```
local new_tab = require('table.new')
# 或者
require('table.clear')
# 执行后会报错
luajit: table_luajit.lua:1: module 'table.new' not found:
```
A这个问题要注意这两行代码需要 LuaJIT 2.1 的版本才能运行, 文档在这里:[https://github.com/LuaJIT/LuaJIT/blob/v2.1/doc/extensions.html#L218](https://github.com/LuaJIT/LuaJIT/blob/v2.1/doc/extensions.html#L218),可以了解一下。
其实,这也是你在使用 OpenResty 时需要特别留意的。OpenResty 需要特定版本的 LuaJIT 才能正常运行,前面我们也讲过,因为 OpenResty 基于 LuaJIT 2.1 的分支,并且对 LuaJIT 做了不少自己的扩展。
所以在运行本专栏的代码时请记得使用OpenResty 官方的安装方式,如果你在 NGINX 的基础上添加 lua-nginx-module 来编译,还是会踩不少坑的。
## 第四问,关于空值的困惑
Q我遇到一些让人困惑的地方是`ngx.null``nil``null``""`。在网上搜索的时候,看到有人说`null``ngx.null`的一个定义。Redis 返回的时候,经常会判断返回结果是否为空,那么,判断的时候是和哪个值进行比较呢?关于这些值,有没有其他一些使用上的坑呢?一直以来我都没有一个明确的认识,想和老师确认一下。
A在回答你的问题之前我建议你在 lua-resty-redis 里,使用下面的代码去查找一个 key
```
local res, err = red:get(&quot;dog&quot;)
```
如果返回值 res 是 nil就说明函调用失败了如果 res 是 ngx.null 就说明redis 中不存在 dog 这个key。这是因为 Lua 的 nil 无法作为 table 的 value所以 OpenResty 引入了 `ngx.null`,作为 table 中的空值。
我们可以用下面的代码,打印出 `ngx.null` 和它的类型:
```
# 打印ngx.null
$ resty -e 'print(ngx.null)'
null
# 打印类型
$ resty -e 'print(type(ngx.null))'
userdata
```
你可以看到, `ngx.null` 并非`nil`,而是 `userdata` 类型。
更进一步,在 OpenResty 中有很多种空值,比如 `cjson.null``cdata:NULL` 等等,后面我都会专门讲到。
总的来说,在 OpenResty 中只有 `nil``false` 是假值。所以,在你写类似 `if not res then`这种代码的时候,一定要慎之又慎,最好改成明确的 `if res ~= nil and res ~= false then`,用类似这样的写法,并要有对应的测试案例覆盖。
## 第五问API 网关到底是什么?
Q文中一直说的 API 网关是指什么和NGINX、Tomcat、Apache这种Web服务器又有什么区别呢
AAPI 网关其实是用来统一管理服务的网关。举个例子,像是支付、用户登录等,都是 API 形式对外提供的服务,它们都需要一个网关来做统一的安全和身份认证。
API 网关可以替代传统的 NGINX、Apache 来处理南北向流量,也可以在微服务环境下处理东西向的流量,是更加贴近业务的一种中间件,而非底层的 Web 服务器。
所以,在专栏的最后几篇文章中,我会带着你一起来看下,如何实现一个 API 网关,这是 OpenResty 当前最热门的使用场景之一。
学习是一个需要反复和刻意练习的过程,就像你高中、大学读书的时候一样,能提出问题、敢于提出问题,是吸收知识的重要步骤。希望你能够体会“把书读厚再读薄”的这个学习过程。
最后,欢迎你继续在留言区写下你的疑问,我会持续不断地解答。希望可以通过交流和答疑,帮你把所学转化为所得。也欢迎你把这篇文章转发给你的同事朋友,一起交流、一起进步。