OpenResty Lua学习笔记

7.9k 词


由于公司C端业务的 Nginx 框架使用的是OpenResty。在上一篇文章学习了 Nginx 基础知识后,决定再学习一下 Lua 和 OpenResty。本文记录了自己学习过程中的一些笔记和总结,尝试了OpenResty在本地搭建简单的API Server框架的demo,包括处理接口路由以及操作 Redis 的的流程。通过学习,未将来可能涉及到OpenResty的开发做一些准备。


Lua

Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 所组成并于 1993 年开发。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua 由标准 C 编写而成,几乎在所有操作系统和平台上都可以编译、运行。Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合作为开发独立应用程序的语言。Lua 有一个同时进行的 JIT 项目,提供在特定平台上的即时编译功能。

Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替 XML、ini 等文件格式,并且更容易理解和维护。标准 Lua 5.1 解释器由标准 C 编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译和运行;一个完整的标准 Lua 5.1 解释器不足 200KB。而本书推荐使用的 LuaJIT 2 的代码大小也只有不足 500KB,同时也支持大部分常见的体系结构。在目前所有脚本语言引擎中,LuaJIT 2 实现的速度应该算是最快的之一。这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。

Lua 语言的各个版本是不相兼容的。因此本书只介绍 Lua 5.1 语言,这是为标准 Lua 5.1 解释器和 LuaJIT 2 所共同支持的。LuaJIT 支持的对 Lua 5.1 向后兼容的 Lua 5.2 和 Lua 5.3 的特性,我们也会在方便的时候予以介绍。

安装LuaJIT遇到的问题

Lua基础数据类型

  • nil(空):nil 是一种类型,Lua 将 nil 用于表示“无效值”。一个变量在第一次赋值前的默认值是 nil,将 nil 赋予给一个全局变量就等同于删除它。
  • boolean(布尔):可选值 true/false;Lua 中 nil 和 false 为“假”,其它所有值均为“真”。(0和空字符串是”真”)
  • number(数字)
  • string(字符串)
  • table (表):Table 类型实现了一种抽象的“关联数组”,通常实现为一个哈希表、一个数组、或者两者的混合。
  • function (函数):在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值。

Lua表达式

  • 算术运算符: + - * / ^ %
  • 关系运算符: < > <= >= == ~=(不等于)

    Lua 字符串总是会被“内化”,即相同内容的字符串只会被保存一份,因此 Lua 字符串之间的相等性比较可以简化为其内部存储地址的比较。这意味着 Lua 字符串的相等性比较总是为 O(1). 而在其他编程语言中,字符串的相等性比较则通常为 O(n),即需要逐个字节(或按若干个连续字节)进行比较。

  • 逻辑运算符: and(与) or(或) not(非) (对于 not,永远只返回 true 或者 false)

控制结构

  • if/else

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    score = 0
    if score == 100 then
    print("Very good!Your score is 100")
    elseif score >= 60 then
    print("Congratulations, you have passed it,your score greater or equal to 60")
    else
    if score > 0 then
    print("Your score is better than 0")
    else
    print("My God, your score turned out to be 0")
    end
    end
  • while

    1
    2
    3
    4
    5
    6
    7
    8
    local t = {1, 3, 5, 8, 11, 18, 21}
    local i
    for i, v in ipairs(t) do
    if 11 == v then
    print("index[" .. i .. "] have right value[11]")
    break
    end
    end

模块

一个 Lua 模块的数据结构是用一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)。一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块。 可以使用内建函数 require() 来加载和缓存模块。简单的说,一个代码模块就是一个程序库,可以通过 require 来加载。模块加载后的结果通过是一个 Lua table,这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量。require 函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。

OpenResty

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

gitbook

与location配合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
location = /sum {
# 只允许内部调用
internal;

# 这里做了一个求和运算只是一个例子,可以在这里完成一些数据库、
# 缓存服务器的操作,达到基础模块和业务逻辑分离目的
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.say(tonumber(args.a) + tonumber(args.b))
}
}

location = /subduction {
internal;
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) - tonumber(args.b))
}
}

location = /app/test_parallels {
content_by_lua_block {
local start_time = ngx.now()
local res1, res2 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}},
{"/subduction", {args={a=3, b=8}}}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}

location = /app/test_queue {
content_by_lua_block {
local start_time = ngx.now()
local res1 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}}
})
local res2 = ngx.location.capture_multi( {
{"/subduction", {args={a=3, b=8}}}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}

利用 ngx.location.capture_multi 函数,直接完成了两个子请求并行执行。当两个请求没有相互依赖,这种方法可以极大提高查询效率。

获取uri参数

获取请求uri参数

  • 方法ngx.req.get_uri_args:获取来自 uri 请求的参数
  • 方法ngx.req.get_post_args:获取来自 post 请求的内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    location /print_param {
    content_by_lua_block {
    local arg = ngx.req.get_uri_args()
    for k,v in pairs(arg) do
    ngx.say("[GET ] key:", k, " v:", v)
    end
    ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body
    local arg = ngx.req.get_post_args()
    for k,v in pairs(arg) do
    ngx.say("[POST] key:", k, " v:", v)
    end
    }
    }

传递请求uri参数

  • 方法ngx.encode_args:进行规则转义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    location /test {
    content_by_lua_block {
    local res = ngx.location.capture(
    '/print_param',
    {
    method = ngx.HTTP_POST,
    args = ngx.encode_args({a = 1, b = '2&'}),
    body = ngx.encode_args({c = 3, d = '4&'})
    }
    )
    ngx.say(res.body)
    }
    }

获取请求body

在 Nginx 的典型应用场景中,几乎都是只读取 HTTP 头即可,例如负载均衡、正反向代理等场景。但是对于 API Server 或者 Web Application ,对 body 可以说就比较敏感了。由于 OpenResty 基于 Nginx ,所以天然的对请求 body 的读取细节与其他成熟 Web 框架有些不同。

  • 方法ngx.req.get_body_data:获取请求的body部分

    注意:获取body部分需要添加指令 lua_need_request_body on;,因为主要是 Nginx 诞生之初主要是为了解决负载均衡情况,而这种情况,是不需要读取 body 就可以决定负载策略的,所以默认是不能获取请求体的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 默认读取 body
    lua_need_request_body on;

    location /test {
    content_by_lua_block {
    local data = ngx.req.get_body_data()
    ngx.say("hello ", data)
    }
    }

如果你只是某个接口需要读取 body(并非全局行为),那么这时候也可以显示调用 ngx.req.read_body() 接口:

1
2
3
4
5
6
7
location /test {
content_by_lua_block {
ngx.req.read_body()
local data = ngx.req.get_body_data()
ngx.say("hello ", data)
}
}

输出响应体

在OpenResty 中调用ngx.sayngx.print可以输出响应体,二者区别在于:ngx.say 输出的响应体会多一个换行符 n。

注意ngx.sayngx.print 都是异步输出

处理响应体过大的输出

  • 输出内容本身体积很大,例如超过 2G 的文件下载
    利用 HTTP 1.1 特性 CHUNKED 编码,把一个大的响应体拆分成多个小的应答体,分批、有节制的响应给请求方。

  • 输出内容本身是由各种碎片拼凑的,碎片数量庞大,例如应答数据是某地区所有人的姓名
    利用 ngx.print 输入参数可以是单个或多个字符串参数,也可以是 table 对象的这一特性,把碎片数据直接存放在 table 中,用数组的方式把这些碎片数据统一起来,直接调用 ngx.print(table)即可。

简单API Server框架

  • 首先,配置多个API时,为了保持 nginx 配置文件的简洁,要把这些接口的实现放到各自的独立lua文件中;
  • 其次,对每个 API 都写一个 location会让Nginx配置变得复杂,所以需要把它们都合并到一个location配置中;

首先,定义lua文件搜索路径,设置location正则匹配,根据匹配到的路由名称指向对应的lua文件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
# 设置默认 lua 搜索路径,添加 lua 路径
# 此处写相对路径时,对启动 nginx 的路径有要求,必须在 nginx 目录下启动,require 找不到
# comm.param 绝对路径当然也没问题,但是不可移植,因此应使用变量 $prefix 或
# ${prefix},OR 会替换为 nginx 的 prefix path
# lua_package_path 'lua/?.lua;/blah/?.lua;;';
lua_package_path '$prefix/lua/?.lua;/blah/?.lua;;';
server {
listen 80;
location ~ ^/api/([-_a-zA-Z0-9/]+) {
# 准入阶段完成参数验证 (断言??)
access_by_lua_file lua/access_check.lua;
#内容生成阶段
content_by_lua_file lua/$1.lua;
}
}
}

第二步,新建实现接口的lua文件,编写接口逻辑,输出响应体;

1
2
3
--========== {$prefix}/lua/addition.lua
local args = ngx.req.get_uri_args()
ngx.say(args.a + args.b)

最后,还可以加一个参数的检查验证的逻辑;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--========== {$prefix}/lua/comm/param.lua
local _M = {}

-- 对输入参数逐个进行校验,只要有一个不是数字类型,则返回 false
function (...)
local arg = {...}

local num
for _,v in ipairs(arg) do
num = tonumber(v)
if nil == num then
return false
end
end

return true
end

return _M

--========== {$prefix}/lua/access_check.lua
local param= require("comm.param")
local args = ngx.req.get_uri_args()

if not args.a or not args.b or not param.is_number(args.a, args.b) then
ngx.exit(ngx.HTTP_BAD_REQUEST)
return
end

子查询

Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求访问目标 location。但是子请求只是模拟 HTTP 接口的形式, 没有额外的 HTTP/TCP 流量,也没有 IPC (进程间通信) 调用,所有工作在内部高效地在 C 语言级别完成。API方法ngx.location.capturengx.location.capture_multi

连接Redis

首先将连接Redis的方法封装在redis文件夹下的main.lua中,并导出redis连接实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local Redis = {}
function Redis.connect()
-- 引入redis包
local redis = require "resty.redis"
-- 创建新的redis连接
local red = redis:new()
-- 设置redis连接超时时间
red:set_timeout(1000) -- 1 sec
-- 通过ip地址和端口建立连接
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
return red
end
return Redis

然后再新建一个redis操作文件,引用上面导出的redis连接对象,做一些对键值对的 set 和 get 操作(其实,还可以单独封装set、get方法),尝试操作 redis:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local Redis = require("redis.main") -- 引用上面导出的redis连接对象
local red = Redis.connect() -- 调用connect连接方法
-- 获取url参数
local args = ngx.req.get_uri_args()
local key = args.key
local value = args.value

-- set一个键值对
ok, err = red:set(key, value)
if not ok then
ngx.say("failed to set key: ", err)
return
end
ngx.say("set ", key, ": ", ok)

-- 取出刚刚set的键值对
local res, err = red:get(key)
if not res then
ngx.say("failed to get key: ", err)
return
end
ngx.say("get ", key, ": ", res)

最后,在Nginx.conf里配置location的Lua代码块:

1
2
3
4
location ~ ^/api/([-_a-zA-Z0-9/]+) {
access_by_lua_file lua/access_check.lu