mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
118
极客时间专栏/OpenResty从入门到实战/入门篇/01 | 初探OpenResty的三大特性.md
Normal file
118
极客时间专栏/OpenResty从入门到实战/入门篇/01 | 初探OpenResty的三大特性.md
Normal 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 上反复修改测试案例和代码风格,这其中有太多鲜为人知的潜规则。所以,专栏的代码规范和单元测试部分,就是为你准备的。
|
||||
|
||||
而如果你是测试工程师,即使你不使用 OpenResty,OpenResty 的测试框架和性能分析工具集,也必能给你非常多的启发。毕竟,OpenResty 在测试上面的投入和积累是相当深厚的。
|
||||
|
||||
## 写在最后
|
||||
|
||||
欢迎你留言和我分享你的 OpenResty 学习之路,在这期间,你又走过哪些弯路呢?也欢迎你把这篇文章转发给你的同事、朋友。
|
||||
|
||||
还是那句话,在学习的过程中,你有任何疑问,都可以在专栏中留言,我会第一时间给你答复。
|
||||
|
||||
|
||||
229
极客时间专栏/OpenResty从入门到实战/入门篇/02 | 如何写出你的“hello world”?.md
Normal file
229
极客时间专栏/OpenResty从入门到实战/入门篇/02 | 如何写出你的“hello world”?.md
Normal 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 "ngx.say('hello world')"
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
这应该是你见过的最简单的那种 hello world 代码写法,和 Python 类似:
|
||||
|
||||
```
|
||||
$ python -c 'print("hello world")'
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
这背后其实是 OpenResty 哲学的一种体现,代码要足够简洁,也好让你打消“从入门到放弃“的念头。我们今天的内容,就专门围绕着这行代码来展开聊一聊。
|
||||
|
||||
上一节我们讲过,OpenResty 是基于 NGINX 的。那你现在是不是有一个疑问:为什么这里看不到 NGINX 的影子?别着急,我们加一行代码,看看 `resty`背后真正运行的是什么:
|
||||
|
||||
```
|
||||
resty -e "ngx.say('hello world'); ngx.sleep(10)" &
|
||||
|
||||
```
|
||||
|
||||
我们加了一行 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 >= 1.2.11-3
|
||||
BuildRequires: openresty-openssl-devel >= 1.1.0h-1
|
||||
BuildRequires: openresty-pcre-devel >= 8.42-1
|
||||
Requires: openresty-zlib >= 1.2.11-3
|
||||
Requires: openresty-openssl >= 1.1.0h-1
|
||||
Requires: openresty-pcre >= 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("Tom", 56)
|
||||
print(dict:get("Tom"))'
|
||||
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("Tom", 56)
|
||||
print(dict:get("Tom"))'
|
||||
|
||||
```
|
||||
|
||||
OpenResty 世界中常用的调试工具,比如`gdb`、`valgrind`、`sysetmtap`和`Mozilla rr` ,也可以和 `resty` 一起配合使用,方便你平时的开发和测试。它们分别对应着 `resty` 不同的指令,内部的实现其实很简单,就是多套了一层命令行调用。我们以 valgrind 为例:
|
||||
|
||||
```
|
||||
$ resty --valgrind -e "ngx.say('hello world'); "
|
||||
ERROR: failed to run command "valgrind /usr/local/Cellar/openresty/1.13.6.2/nginx/sbin/nginx -p /tmp/resty_hTFRsFBhVl/ -c conf/nginx.conf": 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("hello, world")
|
||||
';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
请先确认下,是否已经把`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 配置文件中。不过,如果代码越来越多,那代码的可读性和可维护性就无法保证了。
|
||||
|
||||
你有什么方法来解决这个问题吗?欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友。
|
||||
|
||||
|
||||
243
极客时间专栏/OpenResty从入门到实战/入门篇/03 | 揪出隐藏在背后的那些子项目.md
Normal file
243
极客时间专栏/OpenResty从入门到实战/入门篇/03 | 揪出隐藏在背后的那些子项目.md
Normal 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("hello, world")
|
||||
|
||||
```
|
||||
|
||||
然后修改 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 -> ../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 分支。
|
||||
|
||||
**相对于 Lua,LuaJIT 增加了不少独有的函数,这些函数非常重要**,但知道的工程师并不多,算是_半隐藏技能_,后面我也会专门介绍。
|
||||
|
||||
### 测试框架
|
||||
|
||||
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 负载均衡和反向代理的范畴,实现了自己的生态,下一次我们会详细聊聊这方面。
|
||||
|
||||
对于今天的内容,你有哪些疑惑和问题吗?欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友,一起学习高效开发。
|
||||
163
极客时间专栏/OpenResty从入门到实战/入门篇/04 | 如何管理第三方包?从包管理工具luarocks和opm说起.md
Normal file
163
极客时间专栏/OpenResty从入门到实战/入门篇/04 | 如何管理第三方包?从包管理工具luarocks和opm说起.md
Normal 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 = "kong"
|
||||
version = "1.1.1-0"
|
||||
supported_platforms = {"linux", "macosx"}
|
||||
source = {
|
||||
url = "git://github.com/Kong/kong",
|
||||
tag = "1.1.1"
|
||||
}
|
||||
description = {
|
||||
summary = "Kong is a scalable and customizable API Management Layer built on top of Nginx.",
|
||||
homepage = "https://konghq.com",
|
||||
license = "Apache 2.0"
|
||||
}
|
||||
dependencies = {
|
||||
"inspect == 3.1.1",
|
||||
"luasec == 0.7",
|
||||
"luasocket == 3.0-rc1",
|
||||
"penlight == 1.5.4",
|
||||
"lua-resty-http == 0.13",
|
||||
"lua-resty-jit-uuid == 0.0.7",
|
||||
"multipart == 0.5.5",
|
||||
"version == 1.0.1",
|
||||
"kong-lapis == 1.6.0.1",
|
||||
"lua-cassandra == 1.3.4",
|
||||
"pgmoon == 1.9.0",
|
||||
"luatz == 0.3",
|
||||
"http == 0.3",
|
||||
"lua_system_constants == 0.1.3",
|
||||
"lyaml == 6.2.3",
|
||||
"lua-resty-iputils == 0.3.0",
|
||||
"luaossl == 20181207",
|
||||
"luasyslog == 1.0.0",
|
||||
"lua_pack == 1.0.5",
|
||||
"lua-resty-dns-client == 3.0.2",
|
||||
"lua-resty-worker-events == 0.3.3",
|
||||
"lua-resty-mediador == 0.1.2",
|
||||
"lua-resty-healthcheck == 0.6.0",
|
||||
"lua-resty-cookie == 0.1.0",
|
||||
"lua-resty-mlcache == 2.3.0",
|
||||
......
|
||||
|
||||
```
|
||||
|
||||
通过文件你可以看到,依赖项里面掺杂了 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 & 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 生态的呢?欢迎留言我们一起聊聊,也欢迎你把这篇文章转发给你的同事、朋友,一起在交流中进步。
|
||||
|
||||
|
||||
35
极客时间专栏/OpenResty从入门到实战/入门篇/05 | [视频]opm项目导读.md
Normal file
35
极客时间专栏/OpenResty从入门到实战/入门篇/05 | [视频]opm项目导读.md
Normal 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)
|
||||
|
||||
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。
|
||||
203
极客时间专栏/OpenResty从入门到实战/入门篇/06 | OpenResty 中用到的 NGINX 知识.md
Normal file
203
极客时间专栏/OpenResty从入门到实战/入门篇/06 | OpenResty 中用到的 NGINX 知识.md
Normal 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 ~ "(iPhone|iPad|Android)" ) {
|
||||
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 的上下文是 server,server 的上下文是 http,http 的上下文是 main。
|
||||
|
||||
指令不能运行在错误的上下文中,NGINX 在启动时会检测 nginx.conf 是否合法。比如我们把
|
||||
|
||||
`listen 80;` 从 server 上下文换到 main 上下文,然后启动 NGINX 服务,会看到类似这样的报错:
|
||||
|
||||
```
|
||||
"listen" 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 模块和公共的只读数据,这样可以利用操作系统的 COW(copy 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 的思路很类似。对此,你是怎么看待的呢?
|
||||
|
||||
欢迎留言和我分享,也欢迎你把这篇文章转发给你的同事、朋友。
|
||||
|
||||
|
||||
341
极客时间专栏/OpenResty从入门到实战/入门篇/07 | 带你快速上手 Lua.md
Normal file
341
极客时间专栏/OpenResty从入门到实战/入门篇/07 | 带你快速上手 Lua.md
Normal 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 -> 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("hello world")
|
||||
|
||||
$ luajit 1.lua
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
当然,你还可以使用 `resty` 来直接运行,要知道,它最终也是用 LuaJIT 来执行的:
|
||||
|
||||
```
|
||||
$ resty -e 'print("hello world")'
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
上述两种运行 hello world 的方式都是可行的。不顾对我来说,我更喜欢 `resty` 这种方式,因为后面很多 OpenResty 的代码,也都是通过 `resty` 来运行的。
|
||||
|
||||
## 数据类型
|
||||
|
||||
Lua 中的数据类型不多,你可以通过 `type` 函数来返回一个值的类型,比如下面这样的操作:
|
||||
|
||||
```
|
||||
$ resty -e 'print(type("hello world"))
|
||||
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 = ""
|
||||
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("true")
|
||||
end
|
||||
a = ""
|
||||
if a then
|
||||
print("true")
|
||||
end'
|
||||
|
||||
```
|
||||
|
||||
这种判断方式和很多常见的开发语言并不一致,所以,为了避免在这种问题上出错,你可以显式地写明比较的对象,比如下面这样:
|
||||
|
||||
```
|
||||
$ resty -e 'local a = 0
|
||||
if a == false then
|
||||
print("true")
|
||||
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 = "red"}
|
||||
print(color["first"])'
|
||||
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 > LuaJIT的库函数 > 标准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("abc", 1, 3))
|
||||
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
|
||||
print(string.byte("abc")) -- 缺少第二个和第三个参数,此时这两个参数都默认为 1
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
它的输出为:
|
||||
|
||||
```
|
||||
979899
|
||||
99
|
||||
97
|
||||
|
||||
```
|
||||
|
||||
### table 库
|
||||
|
||||
在 OpenResty 的上下文中,对于Lua 自带的 table 库,除了 `table.concat` 、`table.sort` 等少数几个函数,大部分我都不推荐使用。至于它们的细节,我们留在 LuaJIT 章节中专门来讲。
|
||||
|
||||
这里我简单提一下`table.concat` 。`table.concat`一般用在字符串拼接的场景下,比如下面这个例子。它可以避免生成很多无用的字符串。
|
||||
|
||||
```
|
||||
$ resty -e 'local a = {"A", "b", "C"}
|
||||
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("hello", "he")
|
||||
print(start)'
|
||||
1
|
||||
|
||||
```
|
||||
|
||||
但如果你只想获取结束的下标,那就必须使用虚变量了:
|
||||
|
||||
```
|
||||
$ resty -e 'local _, end_pos = string.find("hello", "he")
|
||||
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))'
|
||||
|
||||
```
|
||||
|
||||
不过,你可能注意到了,这段代码是用当前时间戳作为种子的,那么这种方法是否有问题呢?又该如何生成好的种子呢?要知道,很多时候我们生成的随机数其实并不随机,并且有很大的安全隐患。
|
||||
|
||||
欢迎在留言区来说说你的看法,也欢迎你把这篇文章转发给你的同事、朋友。我们一起交流、一起进步。
|
||||
|
||||
|
||||
190
极客时间专栏/OpenResty从入门到实战/入门篇/08 | LuaJIT分支和标准Lua有什么不同?.md
Normal file
190
极客时间专栏/OpenResty从入门到实战/入门篇/08 | LuaJIT分支和标准Lua有什么不同?.md
Normal 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.3,LuaJIT 的最新版本则是 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 "ngx.say('hello' .. ', world')"
|
||||
hello, world
|
||||
|
||||
```
|
||||
|
||||
在实际的项目开发中,我们一般都会使用多种开发语言,而Lua 这种不走寻常路的设计,总是会让开发者的思维,在字符串拼接的时候卡顿一下,也是让人哭笑不得。
|
||||
|
||||
### 3. 只有 `table` 这一种数据结构
|
||||
|
||||
不同于 Python 这种内置数据结构丰富的语言,Lua 中只有一种数据结构,那就是 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
|
||||
|
||||
```
|
||||
|
||||
如果不显式地用`_键值对_`的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 `color[1]` 就是 blue。
|
||||
|
||||
另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:
|
||||
|
||||
```
|
||||
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 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 还紧密结合了 FFI(Foreign Function Interface),可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。
|
||||
|
||||
下面是一个最简单的例子:
|
||||
|
||||
```
|
||||
local ffi = require("ffi")
|
||||
ffi.cdef[[
|
||||
int printf(const char *fmt, ...);
|
||||
]]
|
||||
ffi.C.printf("Hello %s!", "world")
|
||||
|
||||
```
|
||||
|
||||
短短这几行代码,就可以直接在 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 的时候,是否遇到一些陷阱和坑呢?欢迎留言一起来聊一聊,我在后面也专门写了一篇文章,来分享我遇到过的那些坑。也欢迎你把这篇文章分享给你的同事、朋友,一起学习,一起进步。
|
||||
|
||||
|
||||
169
极客时间专栏/OpenResty从入门到实战/入门篇/09 | 为什么 lua-resty-core 性能更高一些?.md
Normal file
169
极客时间专栏/OpenResty从入门到实战/入门篇/09 | 为什么 lua-resty-core 性能更高一些?.md
Normal 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, "decode_base64");
|
||||
|
||||
```
|
||||
|
||||
上面的代码看着就头大,不过还好,我们不用深究那两个 `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, &src.len);
|
||||
|
||||
p.len = ngx_base64_decoded_length(src.len);
|
||||
|
||||
p.data = lua_newuserdata(L, p.len);
|
||||
|
||||
if (ngx_decode_base64(&p, &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
|
||||
|
||||
```
|
||||
|
||||
你会发现,相比 CFunction,FFI 实现的代码清爽了很多,它具体的实现是 `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 "resty.core"
|
||||
|
||||
```
|
||||
|
||||
当然,姗姗来迟的 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 的性能提升到底有多大。
|
||||
|
||||
欢迎留言和我分享你的思考、收获,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。
|
||||
|
||||
|
||||
205
极客时间专栏/OpenResty从入门到实战/入门篇/10 | JIT编译器的死穴:为什么要避免使用 NYI ?.md
Normal file
205
极客时间专栏/OpenResty从入门到实战/入门篇/10 | JIT编译器的死穴:为什么要避免使用 NYI ?.md
Normal 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("banana", "a", "A"); 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("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 本身,你可以参考[它的源码](https://github.com/openresty/luajit2/blob/v2.1-agentzh/src/jit/dump.lua)来做更深入的了解;后者的输出比较简单,每行对应一个 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() 函数的替代方案时,我有提到过,那里其实**更适合做一层封装,并默认打开优化选项**。那么,这个任务就交给你来小试牛刀了。
|
||||
|
||||
欢迎在留言区写下你的答案,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。
|
||||
|
||||
|
||||
397
极客时间专栏/OpenResty从入门到实战/入门篇/11 | 剖析Lua唯一的数据结构table和metatable特性.md
Normal file
397
极客时间专栏/OpenResty从入门到实战/入门篇/11 | 剖析Lua唯一的数据结构table和metatable特性.md
Normal 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 = "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 项目的[扩展文档](https://github.com/openresty/luajit2/blob/v2.1-agentzh/doc/extensions.html)中,即使你用谷歌也难觅其踪迹,所以知道的工程师并不多。
|
||||
|
||||
下面是一个简单的例子,我来带你看下它该怎么用。首先要说明,这个函数是扩展出来的,所以在使用它之前,你需要先 `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](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 "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)
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
自然,除了今天讲到的这三个元方法外,还有一些不经常使用的元方法,你可以在遇到的时候再去查阅[文档](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 "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 来做一层封装呢?欢迎在留言区一起讨论这个问题,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
334
极客时间专栏/OpenResty从入门到实战/入门篇/12 | 高手秘诀:识别Lua的独有概念和坑.md
Normal file
334
极客时间专栏/OpenResty从入门到实战/入门篇/12 | 高手秘诀:识别Lua的独有概念和坑.md
Normal 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("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 这个阈值是从何而来的呢?这算是我们今天的思考题,欢迎你留言说下你的看法,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
@@ -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)
|
||||
|
||||
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。
|
||||
@@ -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("dog")
|
||||
|
||||
```
|
||||
|
||||
如果返回值 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服务器,又有什么区别呢?
|
||||
|
||||
A:API 网关其实是用来统一管理服务的网关。举个例子,像是支付、用户登录等,都是 API 形式对外提供的服务,它们都需要一个网关来做统一的安全和身份认证。
|
||||
|
||||
API 网关可以替代传统的 NGINX、Apache 来处理南北向流量,也可以在微服务环境下处理东西向的流量,是更加贴近业务的一种中间件,而非底层的 Web 服务器。
|
||||
|
||||
所以,在专栏的最后几篇文章中,我会带着你一起来看下,如何实现一个 API 网关,这是 OpenResty 当前最热门的使用场景之一。
|
||||
|
||||
学习是一个需要反复和刻意练习的过程,就像你高中、大学读书的时候一样,能提出问题、敢于提出问题,是吸收知识的重要步骤。希望你能够体会“把书读厚再读薄”的这个学习过程。
|
||||
|
||||
最后,欢迎你继续在留言区写下你的疑问,我会持续不断地解答。希望可以通过交流和答疑,帮你把所学转化为所得。也欢迎你把这篇文章转发给你的同事朋友,一起交流、一起进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user