0%

初探OpenResty

什么是OpenResty

在官方文档中,对Openresty的定义是——通过 Lua 扩展 NGINX 实现的可伸缩的 Web 平台。

OpenResty 并不是 NGINX 的 fork,也不是在 NGINX 的基础上加了一些常用库重新打包,而只是把 NGINX 当作底层的网络库来使用。这里有必要说明,OpenResty 的两个基石是:NGINX 和 LuaJIT。

一个简单的使用案例

对于OpenResty的使用,最简单的方式就是在安装目录下,执行:

1
resty -e "ngx.say('hello world')"

便可以看到输出 hello world,从这里,可以看到 ngx 这个词,也很容易想到OpenResty是通过nginx实现的这些效果,我们可以验证一下,使用命令加了一行 sleep 休眠的代码,让 resty 运行的程序打印出字符串后,并不退出:

1
resty -e "ngx.say('hello world'); ngx.sleep(10)" &

可以看到如下输出:

1
2
$ 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 服务。

更为正式的 hello world

最开始我们使用resty写的第一个 OpenResty 程序,没有 master 进程,也不会监听端口。

下面再看下正式一点的怎么来写,要写出这样的程序需要三步:

  • 创建工作目录,创建lua文件,在里面插入代码;
1
2
3
$ mkdir lua
$ cat lua/hello.lua
ngx.say("hello, world")
  • 修改 NGINX 的配置文件,配置 Lua 文件位置;
1
2
3
4
5
6
7
8
9
10
11
12
13
pid logs/nginx.pid;
events {
worker_connections 1024;
}

http {
server {
listen 8080;
location / {
content_by_lua_file lua/hello.lua;
}
}
}
  • 重启 OpenResty 服务。

包管理工具

如果把 OpenResty 用于生产环境,OpenResty 安装包自带的这些库是远远不够的,比如没有 lua-resty 库来发起 HTTP 请求,也没有办法和 Kafka 交互。

在使用OpenResty的过程中,我们不应该使用任何 Lua 的库来解决问题,而是应该使用 cosocket 的 lua-resty-* 库。Lua 的库很可能会带来阻塞,让原本高性能的服务,直接下降几个数量级。

包管理工具主要是 OPMLUAROCKS ,对此不过多记录。

Nginx Lua的执行原理

在OpenResty中,每个Worker进程使用一个Lua VM(Lua虚拟机),当请求被分配到Worker时,将在这个Lua VM中创建一个协程,协程之间数据隔离,每个协程都具有独立的全局变量。

ngx_lua是将Lua嵌入Nginx,让Nginx执行Lua脚本,并且高并发、非阻塞地处理各种请求。Lua内置协程可以很好地将异步回调转换成顺序调用的形式。ngx_lua在Lua中进行的IO操作都会委托给Nginx的事件模型,从而实现非阻塞调用。开发者可以采用串行的方式编写程序,ngx_lua会在进行阻塞的IO操作时自动中断,保存上下文,然后将IO操作委托给Nginx事件处理机制,在IO操作完成后,ngx_lua会恢复上下文,程序继续执行,这些操作对用户程序都是透明的。

每个Nginx的Worker进程持有一个Lua解释器或LuaJIT实例,被这个Worker处理的所有请求共享这个实例。每个请求的context上下文会被Lua轻量级的协程分隔,从而保证各个请求是独立的。

标准 Lua 和 LuaJIT 的关系

标准 Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法。

标准 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 是动态语言,动态语言无法在未运行时推断出变量类型,因此只能在 runtime 中进行 type checking 的工作。

慎用阻塞函数

OpenResty 编程中有一个重要原则:避免使用阻塞函数

OpenResty 之所以可以保持很高的性能,简单来说,是因为它借用了 Nginx 的事件处理和 Lua 的协程机制,所以:

  • 在遇到网络 I/O 等需要等待返回才能继续的操作时,就会先调用 Lua 协程的 yield 把自己挂起,然后在 Nginx 中注册回调;
  • 在 I/O 操作完成(也可能是超时或者出错)后,由 Nginx 回调 resume,来唤醒 Lua 协程。

这样的流程,保证了 OpenResty 可以一直高效地使用 CPU 资源,来处理所有的请求。在这个处理流程中,如果没有使用 cosocket 这种非阻塞的方式,而是用阻塞的函数来处理 I/O,那么 LuaJIT 就不会把控制权交给 Nginx 的事件循环。

这就会导致,其他的请求要一直排队等待阻塞的事件处理完,才会得到响应。综上所述,在 OpenResty 的编程中,对于可能出现阻塞的函数调用,也要特别谨慎;否则,一行阻塞的代码,就会把整个服务的性能拖垮。

阻塞函数举例

比如杀掉某个进程:

1
os.execute("kill -HUP " .. pid) 

或者是拷贝文件、使用 OpenSSL 生成密钥等耗时更久的一些操作:

1
2
os.execute(" cp test.exe /tmp ")
os.execute(" openssl genrsa -des3 -out private.pem 2048 ")

表面上看, os.execute 是 Lua 的内置函数,而在 Lua 中,也确实是用这种方式来调用外部命令的。但是,Lua 是一种嵌入式语言,它在不同的上下文环境中,会有完全不同的推荐用法。

在 OpenResty 的环境中,os.execute 会阻塞当前请求。所以,如果这个命令的执行时间特别短,那么影响还不是很大;可如果这个命令,需要执行几百毫秒甚至几秒钟的时间,那么性能就会有急剧的下降。

诸如此类的例子也非常多,不一一举例。

实际使用

要让请求走nginx的Lua脚本,需要使用nginx的Lua模块。以下是一些步骤:

  1. 安装nginx的Lua模块。可以使用openresty的版本,它已经包含了Lua模块以及其他常用模块。也可以通过源代码编译nginx并添加Lua模块。
  2. 配置nginx的配置文件,使其支持Lua脚本。可以在nginx配置文件中使用“lua_package_path”和“lua_package_cpath”指令设置Lua库路径。
  3. 在nginx配置文件中编写Lua脚本。可以使用“location”指令指定需要执行Lua脚本的URL路径,并使用“content_by_lua_block”或“content_by_lua_file”指令加载脚本。
  4. 在Lua脚本中编写处理请求的代码。可以使用nginx提供的Lua API,例如“ngx.req”和“ngx.say”等来处理请求和响应。

下面是一个简单的示例nginx配置文件,演示了如何使用Lua脚本处理请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduinoCopy codeworker_processes 1;

events {
worker_connections 1024;
}

http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

server {
listen 8080;
server_name localhost;

location /hello {
content_by_lua_block {
ngx.say("Hello, world!")
}
}
}
}

在上面的示例中,“/hello”路径的请求将会被Lua脚本处理,并输出“Hello, world!”。

参考引用:

【1】https://time.geekbang.org/column/article/98416

欢迎关注我的其它发布渠道