Test::Nginx 使用

https://github.com/openresty/test-nginx

https://openresty.gitbooks.io/programming-openresty/content/testing/test-nginx.html

Test::Nginx 使用 Perl 便携的 nginx 测试组件

安装

1
cpan Test::Nginx

测试布局

按照惯例,会在项目的根目录创建一个 t/ 的目录来作为测试文件的目录. 当有很多测试文件时,可以在 t/ 中进一步的分组 ,比如 t/001-test/

本质上每个 .t 文件都是一个 Perl 脚本文件 ,可以使用 Prove (perl 的通用测试工具)来运行

比如 prove t/001.t

测试文件布局

这里有一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Test::Nginx::Socket 'no_plan';

run_tests;

__DATA__

=== TEST 1: max pool size is 200
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
ngx.print("hello world")
}
}

--- request
GET /t

--- response_body chomp
hello world

测试文件通常以 .t 作为扩展名 ,每个测试文件本身就是一个 Perl 脚本.

每个测试文件分为两个部分,两个部分使用 __DATA__ 来分隔

  • 第一部分是简短的 Perl 代码
1
2
3
4
# 调用对应的测试模块 一般使用 'no_plan' 即可
use Test::Nginx::Socket 'no_plan';
# 运行测试用例
run_tests;
  • 第二部分是测试用例

测试用例格式 https://openresty.gitbooks.io/programming-openresty/content/testing/test-file-layout.html

数据块通过三个横线来设置某种数据块,比如如下的四种类型:

1
2
3
4
5
6
7
8
9
10
11
12
--- config
location = /t {
echo "hello, world!";
}

--- request
GET /t

--- response_body
hello, world!

--- error_code: 200

数据块的过滤器

在类型后可以跟一个和多个过滤器

1
2
3
4
5
6
7
# chomp : 去掉换行符
--- error_code chomp
200

# eval: 执行一个 Perl 代码 :以下是生产 4096 个 a 字符串的body .
--- response_body eval
"a" x 4096

运行

Test::Nginx 依赖 Perl 的 prove 命令来运行测试用例.

运行时 Test::Nginx 会调用 nginx 服务器和 socket 客户端来运行测试,它会自动在系统环境的 Path 中找到 nginx . 所有需要在环境中指定 nginx 的程序路径

1
export PATH=/usr/local/openresty/nginx/sbin:$PATH

可以在程序的根目录添加一个 go.sh 的文件

1
2
3
4
5
6
7
8
#!/usr/bin/env bash

export PATH=/usr/local/Cellar/openresty/1.21.4.2_1/nginx/sbin:$PATH

# 部分系统perl环境会提示(Can't locate t/.pm in @INC)
export PERL5LIB=$(pwd):$PERL5LIB

exec prove "$@"

然后使用这个脚本来做测试 ,比如测试 t/aa.t 这个文件.

1
2
3
4
5
$ ./go.sh t/aa.t 
t/aa.t .. ok
All tests successful.
Files=1, Tests=2, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.19 cusr 0.10 csys = 0.32 CPU)
Result: PASS

可以通过 -v 的选项来生成详细的测试报告

prove -v t/aa.t

运行多个文件

prove -v t/foo.t t/bar.t t/baz.t

通配符运行多个文件

prove -v t/*.t

递归运行目录 -r

prove -r t/

测试单个用例,只需要在用例中为单个用例添加 --- ONLY

测试文件运行的顺序

测试文件通常按照字母顺序运行, 可以通过在测试文件名前面添加数字序列来控制运行的顺序. 例如

1
2
3
4
5
6
t/000-sanity.t
t/001-set.t
t/002-content.t
t/003-errors.t
...
t/139-ssl-cert-by.t

虽然 Prove 可以通过 -jN 来支持作业并行运行,但是 Test::Nginx 并真正不支持这种模式.

测试块运行的顺序

测试块会打乱每个文件中的测试块 ,可以通过加入 no_shuffle() 来禁用这种行为

1
2
3
4
5
6
7
use Test::Nginx::Socket 'no_plan';

no_shuffle();
run_tests();

__DATA__
...

测试组件

Test::Nginx::Socket::Lua

用于 ngx_lua 相关的测试框架

https://metacpan.org/pod/Test%3A%3ANginx%3A%3ASocket%3A%3ALua

通过环境变了 TEST_NGINX_INIT_BY_LUA 导入 ngx_lua module

1
export TEST_NGINX_INIT_BY_LUA="package.path = '$PWD/../lua-resty-core/lib/?.lua;' .. (package.path or '') require 'resty.core'"

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Test::Nginx::Socket::Lua;

repeat_each(2);
plan tests => repeat_each() * 3 * blocks();

no_shuffle();
run_tests();

__DATA__

=== TEST 1: sanity
--- config
location = /t {
content_by_lua '
ngx.say("hello world")
';
}
--- request
GET /t
--- response_body
hello world
--- error_code: 200
--- no_error_log
[error]

Test::Nginx::Socket::Lua::Stream

测试 stream

example

1
2
3
4
5
6
stream {
server {
listen 1985;
echo "Hello, stream echo!";
}
}

Test::Nginx::Socket::Lua::Dgram

测试 upd stream

example

1
2
3
4
5
6
stream {
server {
listen 1985 udp;
echo "Hello, stream echo!";
}
}
openresty lua_resty_counter 源码

openresty lua_resty_counter 源码

无锁的累加库

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
local ngx_shared = ngx.shared
local pairs = pairs
local ngx = ngx
local error = error
local setmetatable = setmetatable
local tonumber = tonumber
-- table 清理函数
local clear_tab
do
local ok
ok, clear_tab = pcall(require, "table.clear")
if not ok then
clear_tab = function(tab)
for k in pairs(tab) do
tab[k] = nil
end
end
end
end

local _M = {
_VERSION = '0.2.1'
}
local mt = { __index = _M }

-- local cache of counters increments
-- worker 全局存储,key 为 shdict_name ,value 为对应的 table
-- <shdict_name>={}
local increments = {}
-- boolean flags of per worker sync timers
-- 用来记录是否开启的自动同步 timer_started[shdict_name]=true
local timer_started = {}

local id

-- 同步函数: 将 worker 的计数器同步到 ngx.shared 中
local function sync(_, self)
local err, _, forcible
local ok = true
-- 循环 worker 全局字典,将 increments 的计数同步到 dict 中
for k, v in pairs(self.increments) do
_, err, forcible = self.dict:incr(k, v, 0)
if forcible then
ngx.log(ngx.ERR, "increasing counter in shdict: lru eviction: key=", k)
ok = false
end
if err then
ngx.log(ngx.ERR, "error increasing counter in shdict key: ", k, ", err: ", err)
ok = false
end
end
-- 同步完成后清理字典,并设置 error_metric_name 为 1
clear_tab(self.increments)
if ok == false then
self.dict:incr(self.error_metric_name, 1, 0)
end

return ok
end


function _M.new(shdict_name, sync_interval, error_metric_name)
id = ngx.worker.id()

if not ngx_shared[shdict_name] then
error("shared dict \"" .. (shdict_name or "nil") .. "\" not defined", 2)
end

if not increments[shdict_name] then
increments[shdict_name] = {}
end
-- 构建当前对象的计数器
local self = setmetatable({
dict = ngx_shared[shdict_name],
increments = increments[shdict_name],
error_metric_name = error_metric_name,
}, mt)

-- 更具设置的同步时间,设置定时同步 ngx.timer.every
if sync_interval then
sync_interval = tonumber(sync_interval)
if not sync_interval or sync_interval < 0 then
error("expect sync_interval to be a positive number", 2)
end
if not timer_started[shdict_name] then
ngx.log(ngx.DEBUG, "start timer for shdict ", shdict_name, " on worker ", id)
ngx.timer.every(sync_interval, sync, self)
timer_started[shdict_name] = true
end
end

return self
end
-- 主动同步内容
function _M:sync()
return sync(false, self)
end
-- incr 只执行 increments 的累加
function _M:incr(key, step)
step = step or 1
local v = self.increments[key]
if v then
step = step + v
end

self.increments[key] = step
return true
end
-- reset 清理 dict
function _M:reset(key, number)
if not number then
return nil, "expect a number at #2"
end
return self.dict:incr(key, -number, number)
end
-- get 获取 dict 的内容
function _M:get(key)
return self.dict:get(key)
end

function _M:get_keys(max_count)
return self.dict:get_keys(max_count)
end

return _M

openresty 变量和进程

变量的范围

在 OpenResty 里面,只有在 init_by_lua*
 和 init_worker_by_lua*
 阶段才能定义真正的全局变量。 这是因为其他阶段里面,OpenResty 会设置一个隔离的全局变量表,以免在处理过程污染了其他请求。

在 openresty 中共享变量的范围

  1. ngx.shared.DICT 用于整个 nginx 的共享
  2. ngx.var 或者是模块的变量,对应到每个单独的 lua VM ,只能在单独的 worker 进程共享。
  3. 使用数据库来做全局的共享,比如 memecached 、redis 、mysql 等。

init 阶段初始化的变量和 init_worker 阶段的有什么区别。

init 在 Nginx fork 工作进程之前运行,因此此处加载的数据或者代码将在所有工作进程中享受到操作系统提供的写复制(COW)特点,

共享内存

1
2
-- lua_shared_dict dict size;
lua_shared_dict foo 1m; # 定一个名字为 foo 的 1m 的共享内存

定时器

ngx.timer.at 在指定的时间后运行

1
2
3
4
5
6
7
8
ok, err = ngx.timer.at(delay, handler, ...) -- 启动定时器,delay 秒后执行handler函数

local function handler(premature, ...) -- 定时器的回调函数
if premature then -- 检查进程是否处于退出阶段
return -- 最适当的收尾工作
end
... -- 任意的 Lua 代码
end

timer 阶段执行的回调函数 handler 是与前台请求处理完全分离的,所以在函数里就不能使用 ngx.var、ngx.req、ngx.ctx 和 ngx.print 等请求相关的函数,但其他的大多数功能都是可用的,比如 cosocker、共享内存等,所以可以利用定时器绕过 init_by_lua、log_by_lua、header_filter_by_lua 等指令的限制,在这些阶段里 “间接” 使用 cosocket 访问后端。

在 openresty 中,有很多优秀的库都是使用了共享内存和 worker 变量之间的同步,根据不同的场景形成不同的策略

例如:

  • lua-resty-counter
  • lua-resty-event
  • lua-resty-lmcache

进程管理

OpenResty 的进程模式基于 Nginx,通常以 master/worker 多进程方式提供服务,各个 worker 进程相互平等且独立,由 master 进程通过信号管理各个 worker 进程。

在这个基础上 OpenResty 做了一些很有使用价值的改进,新增一个拥有 root 权限的特权进程,worker 进程也可以有自己的唯一标识,一定程度上能够实现服务的自我感知自我管理。

进程管理功能主要位于 ngx.process 库,它是 lua-resty-core 库的一部分,必须显式加载之后才能使用,即:

1
local process = require "ngx.process"   -- 显式加载 ngx.process 库

进程类型

OpenResty 里的进程分为如下六种类型:

  • single:单一进程,即非 master/worker 模式;
  • master:监控进程,即 master 进程;
  • signaller:信号进程,即 “-s” 参数时的进程;
  • worker:工作进程,最常用的进程,对外提供服务;
  • helper:辅助进程,不对外提供服务,例如 cache 进程;
  • privileged agent:特权进程,OpenResty 独有的进程类型。

当前代码所在的进程类型可以用函数 ngx.process.type 获取,例如:

1
2
3
local process require "ngx.process"        -- 显示加载 ngx.process 库
local str = process.type() -- 获取当前的进程类型
ngx.say("type is", str) -- 通常就是 "worker"

特权进程

特权进程是一种特殊的 worker 进程,权限与 master 进程一致(通常就是 root),拥有其他 worker 进程相同的数据和代码,但关闭了所有的监听端口,不对外提供服务,像是一个 “沉默的聋子”。

特权进程必须显式调用函数 ngx.process.enable_privileged_agent 才能启用。而且只能在 “init_by_lua” 阶段里运行,通常的形式是:

1
2
3
4
5
6
7
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent() -- 启动特权进程
if not ok then -- 检查是否启动成功
ngx.log(ngx.ERR, "failed: ", err)
end
}

因为关闭了所有的监听端口,特权进程不能接受请求,“rewrite_by_lua” “access_by_lua” “content_by_lua” “log_by_lua” 等请求处理相关的执行阶段没有意义,这些阶段里的代码在特权进程里都不会运行。

但有一个阶段是它可以使用的,那就是 “init_worker_by_lua”,特权进程要做的工作就是 ngx.timer.* 启动若干个定时器,运行周期任务,通过共享内存等方式与其他 worker 进程通信,利用自己的 root 权限做其他 worker 进程想做而不能做的工作。

例如可以用 get_master_pid 获取 master 进程的 pid,然后在特权进程里调用系统命令 kill 发送 SIGHUP/SIGQUIˇ/SIGUSRl 等信号,实现服务的自我管理。