OpenResty:Nginx与lua基础

OpenResty 的两个基石:NGINX 和 LuaJIT。

 

NGINX基础

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

  • 要尽可能少地配置 nginx.conf;
  • 避免使用if、set 、rewrite 等多个指令的配合;
  • 能通过 Lua 代码解决的,就别用 NGINX 的配置、变量和模块来解决。

这样可以最大限度地提高可读性、可维护性和可扩展性。

 

下面这段 NGINX 配置,就是一个典型的反例,可以说是把配置项当成了代码来使用,在使用 OpenResty 进行开发时需要避免。

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;
}    

 

NGINX 配置

NGINX 通过配置文件来控制自身行为,它的配置可以看作是一个简单 的 DSL。NGINX 在进程启动的时候读取配置,并加载到内存中。如果修改了配置文件,需要重启或者重载 NGINX,再次读取后才能生效。只有 NGINX 的商业版本,才会在运行时, 以 API 的形式提供部分动态的能力。

如下为一段简单的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。

指令不能运行在错误的上下文中,NGINX 在启动时会检测 nginx.conf 是否合法。比如把 listen 80; 从 server 上下文换到 main 上下文,然后启动 NGINX 服务,会看到类似这样的报错:

"listen" directive is not allowed here ......

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

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

 

NGINX 支持的功能,OpenResty 并不一定支持,需要看 OpenResty 的版本号。

OpenResty 的版本号是和 NGINX 保持一致的,所以很容易识别。比如 NGINX 在 2018 年 3 月份发布的 1.13.10 版本中,增加了对 gRPC 的支持,但 OpenResty 在 2019 年 4 月份时的最新版本是 1.13.6.2,由此 可以推断 OpenResty 还不支持 gRPC。

 

MASTER-WORKER 模式

NGINX 启动后,会有一个 Master 进程和多个 Worker 进程(也可以只有一个 Worker 进程)。

 

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;

  

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)特性,来节省一些内存。

对于业务代码来说,其实大部分的操作都可以在 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 是从传统静态的负载均衡、反向代理、文件缓存起家的,这倒也可以理解。

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

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

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

在 OpenResty 中用到的都是 Nginx 的基础知识,主要涉及到配置、主从进程、执行阶段等。而 其他能用 Lua 代码解决的,尽量用代码来解决,而非使用Nginx 的模块和配置,这是在学习 OpenResty 中的一个思路转变。

 

LUA基础

Lua是 OpenResty 中使用的脚本语言。Lua 在设计之初,就把自己定位为一个简单、轻量、可嵌入的胶水语言,没有走大而全的路线。虽然平常工作中可能没有直接编写 Lua 代码,但 Lua 的使用其实非常广泛。很多的网 游,比如魔兽世界,都会采用 Lua 来编写插件;而键值数据库 Redis 则是内置了 Lua 来控制逻辑。

另一方面,虽然 Lua 自身的库比较简单,但它可以方便地调用 C 库,大量成熟的 C 代码都可以为其所用。 比如在 OpenResty 中,很多时候都需要你调用 NGINX 和 OpenSSL 的 C 函数,而这都得益于 Lua 和 LuaJIT 这种方便调用 C 库的能力。

 

环境和 hello world

不用专门去安装标准 Lua 5.1 之类的环境,因为 OpenResty 已经不再支持标准 Lua,而只支持 LuaJIT。 这里介绍的 Lua 语法,也是和 LuaJIT 兼容的部分,而不是基于最新的 Lua 5.3。

在 OpenResty 的安装目录下,可以找到 LuaJIT 的目录和可执行文件。

$ which luajit
/usr/local/Cellar/openresty/1.15.8.3_1/luajit/bin/luajit

$ luajit -v
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/

使用resty运行lua脚本,它最终也是用 LuaJIT 来执行的:

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

 

数据类型

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)
> '

12345678910

  

在 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'

true
true

这种判断方式和很多常见的开发语言并不一致,所以,为了避免在这种问题上出错,可以显式地写明比较 的对象,比如下面这样:

$ 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 比较小巧,内置的标准库并不多。而且,在 OpenResty 的环境中,Lua 标准库的优先级是很低的。对 于同一个功能,更推荐优先使用 OpenResty 的 API 来解决,然后是 LuaJIT 的库函数,最后才是标准 Lua 的函数。

OpenResty的API > LuaJIT的库函数 > 标准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 等少数几个函 数,大部分都不推荐使用。

table.concat一般用在字符串拼接的场景下,可以避免生成很多无用的字符串。

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

ABC

math 库

Lua math 库由一组标准的数学函数构成。数学库的引入,既丰富了 Lua 编程语言的功能,同时也方便了程序的编写。

在 OpenResty 的实际项目中,我们很少用 Lua 去做数学方面的运算,其中和随机数相关的 math.random() 和 math.randomseed() 两个函数比较常用,比如下面的这段代码可以在指 定的范围内,随机地生成两个数字。

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

0.6389552204975
39

  

虚变量

设想这么一个场景,当一个函数返回多个值的时候,有些返回值我们并不需要,这时候,应该怎么接收这些值呢?

Lua 提供了一个虚变量(dummy variable)的概念, 按照惯例以一个下划线来命名,用来表示丢弃不需要的数值,仅仅起到占位的作用。

以 string.find 这个标准库函数为例,来看虚变量的用法。这个标准库函数会返回两个值,分别代表开始和结束的下标。

如果我们只需要获取开始的下标,只声明一个变量来接收 string.find 的返回值即可:

$ resty -e 'local s = string.find("hello","he") print(s)'

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

 

而当有多个返回值需要忽略时,你可以重复使用同一个虚变量。

 

LuaJIT

LuaJIT是OpenResty 的另一块基石。在 OpenResty 中,写出正确的 LuaJIT 代码的门槛并不高,但要写出高效的 LuaJIT 代码绝非易事。

LuaJIT 在 OpenResty 整体架构中的位置:

 

OpenResty 的 worker 进程都是 fork master 进程而得到的, 其实, master 进程中的 LuaJIT 虚拟机也会一起 fork 过来。在同一个 worker 内的所有协程,都会共享这个 LuaJIT 虚拟机,Lua 代 码的执行也是在这个虚拟机中完成的。这可以算是 OpenResty 的基本原理。

标准 Lua 和 LuaJIT 的关系

标准 Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法,并对 Lua 5.2 和 5.3 做了选择性支持。

在 OpenResty 几年前的老版本中, 编译的时候,可以选择使用标准 Lua VM ,或者 LuaJIT VM 来作为执行环境,不过,现在已经去掉了对标 准 Lua 的支持,只支持 LuaJIT。

OpenResty 并没有直接使用 LuaJIT 官方提供的 2.1.0-beta3 版本,而是在此基础上,扩展了 自己的 fork: [openresty-luajit2]

为什么选择 LuaJIT

不直接使用Lua,而是要用open resty维护的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 解释器的解释执行模式。

Lua 特别之处

Lua 的下标从 1 开始

$ resty -e 'local t={100};ngx.say(t[1])'

100

使用 .. 来拼接字符串

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

hello, world

只有 table 这一种数据结构

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

$ resty -e '
>     local color = {first = "red", "blue", third = "green", "yellow"}
> print(color["first"])  
> print(color[1])  
> print(color["third"]) 
> print(color[2]) 
> print(color[3])
> '

red
blue
green
yellow

如果不显式地用_键值对_的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 color[1] 就是 blue。

另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:

$ resty -e 'local t1 = {1,2,3};print("t1 length:" .. table.getn(t1))'
t1 length:3
$ resty -e 'local t2 = { 1, a = 2, 3 };print("t2 length: " .. table.getn(t2))'
t2 length: 2
$ resty -e 'local t3 = { 1, nil };print("t3 length: " .. table.getn(t3))'
t3 length: 1
$ resty -e 'local t4 = { 1, nil, 2 };print("t4 length: " .. table.getn(t4))'
t4 length: 1

  

想 要在Lua 中获取 table 长度,必须注意到,只有在 table 是 序列 的时候,才能返回正确的值。

先序列是数组(array)的子集,也就是说,table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。

序列中不包含空洞(hole),即 nil。综合这两点来看,上面的 table 中, t1 是一个序列,而 t3 和 t4 是 array,却不是序列(sequence)。

t4 的长度是 1 是因为,在遇到 nil 时,获取长度的逻辑就不继续往下运行,而是直接返回了。

默认是全局变量

除非相当确定,否则在 Lua 中声明变量时,前面都要加上 local,是因为在 Lua 中,变量默认是全局的,会被放到名为 _G 的 table 中。不加 local 的变量会在全局表中查 找,这是昂贵的操作。如果再加上一些变量名的拼写错误,就会造成难以定位的 bug。

所以,在 OpenResty 编程中,应该总是使用 local 来声明变量,即使在 require module 的时候也是一样:

-- Recommended
local xxx = require('xxx')
-- Avoid
require('xxx')

LuaJIT

除了兼容 Lua 5.1 的语法并支持 JIT 外,LuaJIT 还紧密结 合了 FFI(Foreign Function Interface),可以直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。

$ resty -e '
    local ffi = require("ffi");
ffi.cdef[[
int printf(const char *fmt, ...);
]];
ffi.C.printf("Hello %s! \n", "world");
'

Hello world!

这几行代码,就可以直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。

类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。实际上,FFI 方式比传 统的 Lua/C API 方式的性能更优,这也是 lua-resty-core 项目存在的意义。

出于性能方面的考虑,LuaJIT 还扩展了 table 的相关函数:table.new 和 table.clear。这是两 个在性能优化方面非常重要的函数。

 

为什么lua-resty-core性能更高一些

在 Lua 中,可以用 Lua C API 来调用 C 函数,而在 LuaJIT 中还可以使用 FFI。对 OpenResty 而言:

  • 在核心的 lua-nginx-module 中,调用 C 函数的 API,都是使用 Lua C API 来完成的;
  • 而在 lua-resty-core 中,则是把 lua-nginx-module 已有的部分 API,使用 FFI 的模式重新实现了一遍。

以 ngx.base64_decode 这个很简单的 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");

这里注册了一个 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。

先来看下前者里面实现的代码:

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    

  

OpenResty 中的函数都是有命名规范的:

  • ngx_http_lua_ffi_ ,是用 FFI 来处理 NGINX http 请求的 Lua 函数;
  • ngx_http_lua_ngx_ ,是用 Cfunction 来处理 NGINX http 请求的 Lua 函数;
  • 其他 ngx_ 和 lua_ 开头的函数,则分别属于 NGINX 和 Lua 的内置函数。

LuaJIT FFI GC

在 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。原因也很明显:

  • ffi.new 返回的是一个 cdata,这部分内存由 LuaJIT 管理;
  • LuaJIT GC 的管理内存是有上限的,OpenResty 中的 LuaJIT 并未开启 GC64 选项,所以单个 worker 内存 的上限只有2G。一旦超过 LuaJIT 的内存管理上限,就会导致报错。

在使用 FFI 的时候,我们还需要特别注意内存泄漏的问题。

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 指令,默认开启了 luaresty-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 等等。

 

FFI 虽然好,却也并不是性能银弹。它之所以高效,主要原因就是可以被 JIT 追踪并优化。如果你写的 Lua 代码不能被 JIT,而是需要在解释模式下执行,那么 FFI 的效率反而会更低。 

posted @ 2020-04-17 23:47  liekkas01  阅读(2145)  评论(0编辑  收藏  举报