openresty学习笔记

本文档参考极客时间openresty专栏和其他博客,中间也有一些坑,记录下学习过程。

1. 概述

Openresty是基于nginx和lua的高性能web平台,同时拥有脚本语言的开发效率和迭代速度,以及 NGINX C 模块的高并发和高性能优势。

2. 入门

0. 前提

需要对nginx.conf配置文件的结构有大致的了解

参考博客:https://juejin.im/post/5c1616186fb9a049a42ef21d

1. 下载和安装

下载和安装,在官方(https://openresty.org/cn/)进行下载和安装,centos版本,最好不要直接下载源码包,会有很多坑。

2. helloworld

首先创建工作目录

mkdir /geektime
cd /geektime
mkdir logs/ conf/

其中 logs 目录用于存放日志,conf 用于存放配置文件。

接着,我们在 conf 目录下创建一个 nginx.conf 文件,如下

worker_connections 1024;
http {
    server {
        listen 8080;
        location / {
            content_by_lua '
                ngx.say("<p>Hello, World!</p>")
            ';
        }
    }
}

启动openresty

默认情况下 openresty 安装在 /usr/local/openresty 目录中,启动命令为:

/usr/local/openresty/nginx/sbin/nginx -p /root/geektime -c conf/nginx.conf

如果没有任何输出,说明启动成功,-p 指定我们的项目目录,-c 指定配置文件。

接下来我们可以使用 curl 来测试是否能够正常返回:

扫描二维码关注公众号,回复: 11579959 查看本文章
curl http://localhost:8080/

输出正常,结果

<p>Hello, World!</p>

3. 调用lua脚本

使用content_by_lua_file指令调用Lua脚本文件。

在geektime目录下创建lua目录,用来存放脚本,在lua文件夹下创建hello.lua:

ngx.say("hello, world")

停止已启动的nginx进程

killall -9 nginx

启动nginx进程

/usr/local/openresty/nginx/sbin/nginx -p /root/geektime/ -c conf/nginx.conf

再次用curl测试,nginx openresty content_by_lua_file 404,百度发现nginx.conf文件中需要添加 user root root;

worker_processes 1;
user root root;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
		content_by_lua_file 'lua/hello.lua';
        	}
	}
	
}

输出正常。

lua脚本内容实时生效

如果修改lua脚本内容后,需要重启nginx才能生效,如何使lua脚本实时生效?

在 nginx.conf 中关闭 lua_code_cache 。

worker_processes 1;
user root root;
events {
    worker_connections 1024;
}
http {
    server {
	lua_code_cache off;
        listen 8080;
        location / {
		content_by_lua_file 'lua/hello.lua';
        	}
	}	
}

重启后可以实时修改,但是每次都会警告,生产环境最好不要这样,影响性能。

3. 模块

1. 自身项目

  • nginx C模块 *-nginx-module命名的就是 NGINX 的 C 模块。

nginx C模块

openresty -V 命令可看到这些C模块,--add-module= 后边跟着的就是C模块,最核心的就是 lua-nginx-module 和
stream-lua-nginx-module,前者用来处理七层流量,后者用来处理四层流量。 cosocket 功能加入之后,大部分C库都已经被 lua-restyredis 和 lua-resty-memcached 替代,处于疏于维护的状态。

lua-resty-周边库

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

自己维护的 LuaJIT 分支

LuaJIT 的作者Mike Pall 宣布退休, 没有找到合适的维护者, 新功能的开发也已经暂停,所以 OpenResty 维护着自己的 LuaJIT 分支。 相对于 Lua,LuaJIT 增加了不少独有的函数,这些函数非常重要 。

2. 第三方类库

OPM(OpenResty Package Manager )是 OpenResty 自带的包管理器,可以试着去找找发送 http 请求的库

opm search http

查看类库的分类,可以参考awesome-resty 这个项目 https://github.com/bungle/awesome-resty

4. 基础

1. 使用的nginx的知识

作用域

每个指令都有自己适用的上下文(Context),也就是NGINX 配置文件中指令的作用域 。

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

多进程模式

一个 Master 进程和多个 Worker 进程。Master 进程,一如其名,扮演“管理者”的角色,并不负责处理终端的请求。它是用来管理Worker 进程的,包括接受管理员发送的信号量、监控 Worker 的运行状态。当 Worker 进程异常退出时,Master 进程会重新启动一个新的 Worker 进程。

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

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

减少对外部程序的依赖,尽量在 OpenResty 进程内解决问题,不仅方便部署、降低运维成本,也可以降低程序出错的概率。可以说,OpenResty 中的特权进程、ngx.pipe 等功能,都是出于这个目的。

2. 执行阶段

最好根据不同的功能拆分业务代码:

  • set_by_lua:设置变量;
  • rewrite_by_lua:转发、重定向等;
  • access_by_lua:准入、权限等;
  • content_by_lua:生成返回内容;
  • header_filter_by_lua:应答头过滤处理;
  • body_filter_by_lua:应答体过滤处理;
  • log_by_lua:日志记录。

3. lua的常用数据类型

1. 字符串

单引号、双引号,以及长括号([[]])

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

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

结果:string has \n and \r

如果上面那段字符串中包括了长括号本身,又该怎么处理呢? 在长括号中间增加一个或者多个 = 符号:

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

结果: string has a [[]].

2. 布尔值

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

3. 数字

Lua 的 number 类型,是用双精度浮点数来实现的。 LuaJIT 支持 dual-number(双数)模式,也就是说, LuaJIT 会根据上下文来用整型来存储整数,而用双精度浮点数来存放浮点数。

LuaJIT 还支持⻓整型的大整数 。

4. 函数

可以把函数存放在一个变量中,也可以当作另外一个函数的入参和出参。

5. table

是 Lua 中唯一的数据结构

6. 空值

在 Lua 中,空值就是 nil。如果你定义了一个变量,但没有赋值,它的默认值就是 nil

4. 常用标准库

OpenResty的API > LuaJIT的库函数 > 标准Lua的函数

1. string库

如果涉及到正则表达式的,要使用 OpenResty 提供的 ngx.re.* 来解决 ,不要用 Lua 的 string.* 处理。

  • string.byte(s [, i [, j ]]) 返回字符s[i]、s[i + 1]、s[i + 2]、······、s[j] 对应的ASCII码,i的默认值为1
print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte("abc"))    -- 缺少第二个和第三个参数,此时这两个参数都默认为 1

-->output
97    98    99
99
97

table库

lua自带的table库,只推荐table.concat、table.sort。

table.concat用于字符串拼接,避免生成很多无用的字符串。

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

math库

随机数 math.random()和math.randomseed()较常用

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

虚变量

当一个函数返回多个值,有些返回值不需要,如何接收这些值?lua提供虚变量dummy variable的概念,按照惯例以一个下划线来命名,表示丢弃不需要的数值,起到占位的作用。

以string.find函数为例,它会返回两个值,分别代表开始和结束的下标。

如果只需要获取开始的下标,只声明一个变量接收返回值 :

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

如果你只想获取结束的下标,那就必须使用虚变量了

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

虚变量还经常用于循环中

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

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

5. luaJIT

标准Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法。 而OpenResty 维护了自己的 LuaJIT 分支,并扩展了很多独有的 API。

为什么用luaJIT

最主要的原因,还是LuaJIT的性能优势。 所谓 LuaJIT 的性能优化,本质上就是让尽可能多的 Lua 代码可以被 JIT 编译器生成机器码,而不是回退到 Lua 解释器的解释执行模式。

兼容 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! 可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。实际上,FFI 方式比传
统的 Lua/C API 方式的性能更优,这也是 lua-resty-core 项目存在的意义。

出于性能方面的考虑,LuaJIT 还扩展了 table 的相关函数:table.new 和 table.clear。这是两个在性能优化方面非常重要的函数,在 OpenResty 的 lua-resty 库中会被频繁使用。

检测内存泄露

使用FFI时,需要注意内存泄露。

测试,使用Valgrind检测内存泄露问题。测试框架test::nginx,有专门的内存泄露检测模式,去运行单元测试案例集。只需要设置环境变量 TEST_NGINX_USE_VALGRIND=1 。

而 OpenResty 的 CLI resty 也有 --valgrind 选项,方便你单独运行某段 Lua 代码,即使你没有写测试案例也是没问题的。

调试工具:提供基于 systemtap 的扩展,来对 OpenResty 程序进行活体的动态分析。你可以在这个项目的工具集中,搜索 gc 这个关键字,会看到 lj-gc 和 lj-gc-objs 这两个工具;对于 core dump 这种离线分析,OpenResty 提供了 GDB 的工具集,同样你可以在里面搜索 gc,找到lgc、lgcstat 和 lgcpath 三个工具。

6. lua的特别之处

1.下标从1开始

2. 使用 … 来拼接字符串

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 中的元素都可以用正整数下标访问到,不存在键值对的情况。其次,序列中不包含空洞(hole),即 nil。 上面的 table 中, t1 是一个序列,而 t3 和 t4
是 array,却不是序列(sequence)。

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

table的内部结构参考博客https://blog.csdn.net/wwlcsdn000/article/details/81291756

4. 默认是全局变量

除非你相当确定,否则在 Lua 中声明变量时,前面都要加上 local。 强烈建议你总是使用 local 来声明变量 。

7. NYI

如果你的项目中只用到了 OpenResty 提供的 API,没有自己调用C 函数的需求,那么 FFI 对你而言并没有那么重要,你只需要确保开启了 lua-resty-core 即可。

你可以很快使用 OpenResty 写出逻辑正确的代码,但不明白 NYI,你就不能写出高效的代码,无法发挥OpenResty 真正的威力。

JIT 编译器不支持的这些原语,其实就是 NYI,全称为Not Yet Implemented。 当 JIT 编译器在当前代码路径上遇到它不支持的操作时,便会退回到解释器模式。

如何检测NYI?

LuaJIT 自带的 jit.dump 和 jit.v 模块。它们都可以打印出 JIT 编译器工作的过程。

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,所以结果里,指明了第 7 行中有 NYI 原语。

8. table和metatable特性

可以用ipairs 函数,只遍历数组部分的内容。

openresty的table扩展函数

OpenResty 自己维护的 LuaJIT 分支,也对 table 做了扩展,它新增了几个API:table.isempty、table.isarray、 table.nkeys 和 table.clone。

table.nkeys:获取 table 长度的函数,返回的是 table 的元素个数,包括数组和哈希部分的元素。因此,我们可以用它来替代 table.getn 。

LuaJIT 的 table 扩展函数

table.new(narray, nhash) 新建 table ;table.clear() 清空 table ,避免反复创建和销毁 table 的开销。

local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end

标准table 库函数

table.getn 获取元素个数 ,不推荐使用,时间复杂度O(n),用table.nkeys替换。

table.remove 删除指定元素,只能删除数组,如何删除 table 中的哈希部分呢?把key 对应的 value 设置为 nil 即可。

table.concat 元素拼接函数 。

table.insert 插入一个元素,尽量少用,时间复杂度也是O(n)。

元表

由 table 引申出来的 元表(metatable)。

元表的表现行为类似于操作符重载,比如我们可以重载 __add,来计算两个 Lua 数组的并集;或者重载__tostring,来定义转换为字符串的函数。

Lua 提供了两个处理元表的函数:

  • setmetatable(table, metatable), 用于为一个 table 设置元表;
  • setmetatable(table, metatable), 用于为一个 table 设置元表;
local version = {
major = 1,
minor = 1,
patch = 1
}
print(tostring(version))
version = setmetatable(version, {
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})

结果:

table: 0x7f585889a588
1.1.1

除了__tostring之外,在实际项目中,我们还经常重载元表中的以下两个元方法(metamethod)。

其中一个是__index。我们在 table 中查找一个元素时,首先会直接从 table 中查询,如果没有找到,就继续到元表的 __index 中查询。

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

另一个元方法则是__call。它类似于仿函数,可以让 table 被调用。

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/102887365
今日推荐