Skip to content

OpenResty 学习笔记(2) - 前置知识

Published: at 08:31 AM21 min read

目录

Hello Word

安装

Terminal window
brew install openresty/brew/openresty

不推荐通过源码安装,因为自己维护了 OpenSSL [打包脚本] zlib 和 PCRE 等(CentOS 中的[打包脚本]),自己打包需要对这些细节比较了解。

基础

Terminal window
$ resty -e "ngx.say('hello world')"
hello world

查看运行进程

Terminal window
$ resty -e "ngx.say('hello world'); ngx.sleep(10)" &
$ 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

可以看到 resty 本质上是启动了一个 NGINX 服务

OpenResty CLI

resty是个 1000 多行的 Perl 脚本(使用 Perl 是因为作者偏好)

Terminal window
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
dict:set("Tom", 56)
print(dict:get("Tom"))'
56

dogs 1m 是 NGINX 的一段配置,声明了一个共享内存空间,名字是 dogs,大小是 1m;在 Lua 代码中用字典的方式使用共享内存。另外还有--http-include--main-include来设置 NGINX 配置文件。所以,上面的例子也可以写为:

Terminal window
resty --http-conf 'lua_shared_dict dogs 1m;' -e 'local dict = ngx.shared.dogs
dict:set("Tom", 56)
print(dict:get("Tom"))'

更正式 Hello Word

Terminal window
# 创建工作目录
mkdir logs/ conf/

最简化的 nginx.conf

events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
content_by_lua '
ngx.say("hello, world")
';
}
}
}

启动

Terminal window
openresty -p `pwd` -c conf/nginx.conf

测试

Terminal window
$ 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

把 Hello Word 改写为常见项目模式

Terminal window
$ 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 的服务就可以了:

Terminal window
$ sudo kill -HUP `cat logs/nginx.pid`

content_by_lua_file 文档

  1. content_by_lua_file lua/hello.lua; 里面写的是相对路径,那么 OpenResty 是如何找到这个 Lua 文件的?

    OpenResty 在启动时,会把 OpenResty 启动的命令行参数中的 -p PATH 作为前缀,将相对路径拼接为绝对路径。

  2. Lua 代码内容的变更,需要重启 OpenResty 服务才会生效,不方便调试,那么有没有即时生效的方法呢?

    Lua 代码在第一个请求时会被加载,并默认缓存起来。所以在你每次修改 Lua 源文件后,都必须重新加载 OpenResty 才会生效。

    其实,在 nginx.conf 中关闭 lua_code_cache 就能避免重新加载。

    这种方法只能临时用于开发和调试,如果是线上部署,一定要记得打开缓存,否则会非常影响性能。

  3. 如何把 lua 代码所在的文件夹,加入到 OpenResty 的查找路径中呢?

    OpenResty 提供了 lua_package_path 指令,可以设置 Lua 模块的查找路径。针对上面的例子,我们可以把 lua_package_path 设置为 $prefix/lua/?.lua;;,其中,

    • $prefix就是启动参数中的 -p PATH;
    • /lua/?.lua表示 lua 目录下所有以 .lua 作为后缀的文件;
    • 最后的两个分号,则代表内置的代码搜索路径。

OpenResty 项目概览

OpenResty 在 GitHub 的 项目主页,OpenResty 包含了 68 个公开的项目,大概分为以下 7 类。

NGINX C 模块

OpenResty 的项目命名都是有规范的,以 *-nginx-module命名的就是 NGINX 的 C 模块。

OpenResty 中一共包含了 20 多个 C 模块,我们在本节最开始使用的 openresty -V 中,也可以看到这些 C 模块:

Terminal window
nginx version: openresty/1.25.3.2
built by clang 15.0.0 (clang-1500.1.0.2.5)
built with OpenSSL 1.1.1w 11 Sep 2023
TLS SNI support enabled
configure arguments: --prefix=/usr/local/Cellar/openresty/1.25.3.2_1/nginx --with-cc-opt='-O2 -I/usr/local/include -I/usr/local/opt/pcre/include -I/usr/local/opt/openresty-openssl111/include' --add-module=../ngx_devel_kit-0.3.3 --add-module=../echo-nginx-module-0.63 --add-module=../xss-nginx-module-0.06 --add-module=../ngx_coolkit-0.2 --add-module=../set-misc-nginx-module-0.33 --add-module=../form-input-nginx-module-0.12 --add-module=../encrypted-session-nginx-module-0.09 --add-module=../srcache-nginx-module-0.33 --add-module=../ngx_lua-0.10.26 --add-module=../ngx_lua_upstream-0.07 --add-module=../headers-more-nginx-module-0.37 --add-module=../array-var-nginx-module-0.06 --add-module=../memc-nginx-module-0.20 --add-module=../redis2-nginx-module-0.15 --add-module=../redis-nginx-module-0.3.9 --add-module=../ngx_stream_lua-0.0.14 --with-ld-opt='-Wl,-rpath,/usr/local/Cellar/openresty/1.25.3.2_1/luajit/lib -L/usr/local/lib -L/usr/local/opt/pcre/lib -L/usr/local/opt/openresty-openssl111/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-stream --without-pcre2 --with-http_ssl_module

这里--add-module=后面跟着的,就是 OpenResty 的 C 模块。其中,最核心的就是 lua-nginx-modulestream-lua-nginx-module,前者用来处理七层流量,后者用来处理四层流量。

这些 C 模块中,有些是需要特别注意的,虽然默认编译进入了 OpenResty,但并不推荐使用。 比如 redis2-nginx-moduleredis-nginx-modulememc-nginx-module,它们是用来和 redis 以及 memcached 交互使用的。这些 C 库是 OpenResty 早期推荐使用的,但在 cosocket 功能加入之后,它们都已经被 lua-resty-redislua-resty-memcached 替代,处于疏于维护的状态。

OpenResty 后面也不会开发更多的 NGINX C 库,而是专注在基于 cosocket 的 Lua 库上,后者才是未来。

lua-resty-周边库

OpenResty 官方仓库中包含 18 个 lua-resty-* 库,涵盖 Redis、MySQL、memcached、websocket、dns、流量控制、字符串处理、进程内缓存等常用库。

自己维护的 LuaJIT 分支

OpenResty 除了维护自己的 OpenSSL patch 外,还维护了自己的 LuaJIT 分支。在 2015 年,LuaJIT 的作者 Mike Pall 宣布退休,寻找新的 LuaJIT 维护者,但 Mike 并没有找到合适的维护者,他现在主要是做 bugfix 的维护工作,新功能的开发也已经暂停,所以 OpenResty 维护着自己的 LuaJIT 分支。

相对于 Lua,LuaJIT 增加了不少独有的函数,这些函数非常重要

测试框架

OpenResty 的测试框架是test-nginx,同样也是用 Perl 语言来开发的,从名字上就能看出来,它是专门用来测试 NGINX 相关的项目。OpenResty 官方的所有 C 模块和 lua-resty 库的测试案例,都是由 test-nginx 驱动的。

除了 test-nginx 之外,mockeagain 这个项目可以模拟慢速的网络,让程序每次只读写一个字节。对于 web 服务器来说,这是一个很有用的工具。

调试工具链

OpenResty 项目在如何科学和动态地调试代码上,花费了大量的精力。

OpenResty 的作者章亦春专门写了一篇文章,来介绍动态追踪技术。

openresty-systemtap-toolkitstapxx 这两个 OpenResty 的项目,都基于 systemtap 这个动态调试和追踪工具。使用 systemtap 最大的优势,便是实现活体分析,同时对目标程序完全无侵入。

打包相关

openresty-packaging home-brew

工程化工具

openresty-devel-utils 就是开发 OpenResty 和 NGINX 的工具集。

lj-releng 是一个简单有效的 LuaJIT 代码检测工具,类似 luacheck,可以找出全局变量等潜在的问题。

reindex 从名字来看是重建索引的意思,它其实是格式化 test-nginx 测试案例的工具,可以重新排列测试案例的编号,以及去除多余的空白符。

opsboy 是一个用 Perl 实现的 DSL(领域特定语言),主要用于自动化部署。OpenResty 每次发布版本前,都会在 AWS EC2 集群上做完整的回归测试,详细的文档可以参考官方文档,而这个回归测试正是由 opsboy 来部署和驱动的。

包管理工具 luarocks 和 opm

OpenResty只是把 NGINX 当作底层的网络库来使用,并非单纯 nginx 的 fork。

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

OPM

OPM(OpenResty Package Manager)是 OpenResty 自带的包管理器,在你安装好 OpenResty 之后,就可以直接使用。

Terminal window
$ 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

接着对比 star 和更新时间 agentzh/lua-resty-http 停更,pintsized/lua-resty-http 和 ledgetech/lua-resty-http 其实指向了同一个仓库。

OPM 的网站 没有提供包的下载次数,需要花费更多的时间,来甄别出应该使用哪些 lua-resty 库。

LUAROCKS

LuaRocks 和 OPM 不同点在于,OPM 里只包含 OpenResty 相关的包,LuaRocks 里面还包含 Lua 世界的库。

包管理配置的例子:可以在 https://github.com/Kong/kong 的项目下找到最新的 .rockspec 后缀的文件。

LuaRocks 也支持在 rockspec 文件中,指定 C 源码的路径和名称,这样 LuaRocks 就会帮忙本地编译。而 OPM 暂时还不支持这种特性。

AWESOME-RESTY

awesome-resty 这个项目,就维护了几乎所有 OpenResty 可用的包,并且都分门别类地整理好了。当你不确定是否存在适合的第三方包时,来这里“按图索骥”,可以说是最好的办法。

编码风格—第三方包的方法变成局部变量,这样的优点有

  1. lua 中默认的变量是全局变量,全局变量容易被多个地方同名引用,造成难查找的错误
  2. 文件开头引用一次,第二次使用会有性能上的提升

没有做面向对象的类的封装

前置 Nginx 知识

在 OpenResty 的开发中,我们需要注意下面几点:

NGINX 配置

我们首先来看下 NGINX 的配置文件。NGINX 通过配置文件来控制自身行为,它的配置可以看作是一个简单的 DSL。NGINX 在进程启动的时候读取配置,并加载到内存中。如果修改了配置文件,需要你重启或者重载 NGINX,再次读取后才能生效

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;
}
}
  1. 每个指令都有自己适用的上下文(Context),也就是 NGINX 配置文件中指令的作用域。

    最上层的是 main,里面是和具体业务无关的一些指令,比如上面出现的 worker_processes、pid 和 error_log,都属于 main 这个上下文。另外,上下文是有层级关系的,比如 location 的上下文是 server,server 的上下文是 http,http 的上下文是 main。

  2. NGINX 不仅可以处理 HTTP 请求 和 HTTPS 流量,还可以处理 UDP 和 TCP 流量。

    其中,七层的放在 HTTP 中,四层的放在 stream 中。在 OpenResty 里面, lua-nginx-module 和 stream-lua-nginx-module 分别和这俩对应。

上面 nginx.conf 涉及到的配置指令,都在 NGINX 的核心模块 ngx_core_modulengx_http_core_modulengx_stream_core_module

MASTER-WORKER 模式

NGINX 启动后,会有一个 Master 进程和多个 Worker 进程。

Master 进程,不负责处理终端的请求。它是用来管理 Worker 进程的,包括接受管理员发送的信号量、监控 Worker 的运行状态。当 Worker 进程异常退出时,Master 进程会重新启动一个新的 Worker 进程。

Worker 进程,用来处理终端用户的请求。它是从 Master 进程 fork 出来的,彼此之间相互独立,互不影响。多进程的模式比 Apache 多线程的模式要先进很多,没有线程间加锁,也方便调试。即使某个进程崩溃退出了,也不会影响其他 Worker 进程正常工作。

而 OpenResty 在 NGINX Master-Worker 模式的前提下,又增加了独有的特权进程(privileged agent)。这个进程并不监听任何端口,和 NGINX 的 Master 进程拥有同样的权限,所以可以做一些需要高权限才能完成的任务,比如对本地磁盘文件的一些写操作等。

如果特权进程与 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;

OpenResty 也有 11 个 *_by_lua指令,它们和 NGINX 阶段的关系如下图所示(图片来自 lua-nginx-module 文档):

其中, init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。

所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。

二进制热升级

热升级通过向旧的 Master 进程发送 USR2 和 WINCH 信号量来完成。对于这两步,前者的作用,是启动新的 Master 进程;后者的作用,是逐步关闭 Worker 进程。

执行完这两步后,新的 Master 和新的 Worker 就已经启动了。不过此时,旧的 Master 并没有退出。不退出的原因也很简单,如果你需要回退,依旧可以给旧的 Master 发送 HUP 信号量。当然,如果你已经确定不需要回退,就可以给旧 Master 发送 KILL 信号量来退出。

至此,二进制的热升级就完成了。

详见官方文档

前置 Lua 知识

下文使用使用 openresty 中的 luajit 来操作, 可以在前文提到的安装目录中找到他/usr/local/Cellar/openresty/1.25.3.2_1/luajit/bin/luajit

Terminal window
$ cat 1.lua
print("hello world")
$ luajit 1.lua
hello world

也可以

$ resty -e 'print("hello world")'
hello world

数据类型

Terminal window
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 中,字符串是不可变的值,如果你要修改某个字符串,就等于创建了一个新的字符串。

注意:循环进行字符串拼接,中间会创建无用字符串,有损性能(todo: 链接性能优化)

Terminal window
$ resty -e 'local s = ""
for i = 1, 10 do
s = s .. tostring(i)
end
print(s)'

Lua 中表达字符串 单引号、双引号,以及长括号([[]]

Terminal window
$ resty -e 'print([[string has \n and \r]])'
string has \n and \r

长括号中的字符串不会做任何的转义处理。

长括号中的长括号如何表示?在长括号中间增加一个或者多个 = 符号

Terminal window
$ resty -e 'print([=[ string has a [[]]. ]=])'
string has a [[]].

布尔值

在 Lua 中,只有 nil 和 false 为假,其他都为真,包括 0 和空字符串也为真

Terminal window
$ resty -e 'local a = 0
if a then
print("true")
end
a = ""
if a then
print("true")
end'
true
true

数字

Lua 的 number 类型,是用双精度浮点数来实现的。LuaJIT 支持 dual-number(双数)模式: 根据上下文来用整型来存储整数,而用双精度浮点数来存放浮点数。此外,LuaJIT 还支持长长整型的大整数。

Terminal window
$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL

函数

下面两个函数的声明是完全等价的:

function foo()
end
foo = function ()
end

table

见下

空值

在 Lua 中,空值就是 nil。未赋值变量,默认值是 nil。

Terminal window
$ resty -e 'local a
print(type(a))'
nil

Todo: 补充 ngx.null 等空值

常用标准库

为了性能,使用 API 的优先级:

OpenResty的API > LuaJIT的库函数 > 标准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

Terminal window
$ resty -e 'print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte("abc")) -- 缺少第二个和第三个参数,此时这两个参数都默认为 1
'

它的输出为:

Terminal window
979899
99
97

table 库

除了 table.concattable.sort 等少数几个函数,大部分不推荐使用。

table.concat 一般用来拼接字符串,它可以避免生成很多无用的字符串。

Terminal window
$ resty -e 'local a = {"A", "b", "C"}
print(table.concat(a))'

lua 内置 table 库函数和 LuaJIT 扩展函数

math 库

随机数相关的 math.random()math.randomseed() 两个函数,是比较常用

$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
0.23799673023264
81

虚变量

函数返回多个值的时候,有些返回值我们并不需要, 使用一个占位符,丢弃不需要的值,这个就是虚变量(dummy variable)。

string.find返回两个值,开始和结束的下标。

Terminal window
$ resty -e 'local start = string.find("hello", "he")
print(start)'
1
Terminal window
$ resty -e 'local _, end_pos = string.find("hello", "he")
print(end_pos)'
2

或者用于循环

Terminal window
$ resty -e 'for _, v in ipairs({4,5,6}) do
print(v)
end'
4
5
6

Previous Post
OpenResty 学习笔记
Next Post
OpenResty 学习笔记(3) - LuaJIT