目录
test__nginx
test::nginx
糅合了 Perl、数据驱动以及 DSL(领域小语言)。对于同一份测试案例集,通过对参数和环境变量的控制,可以实现乱序执行、多次重复、内存泄漏检测、压力测试等不同的效果。
安装
参考官方 CI travis 中的方法
-
先安装 Perl 的包管理器 cpanminus。
-
然后,通过 cpanm 来安装
test::nginx
: -
再接着, clone 最新的源码:
-
最后,通过 Perl 的
prove
命令来加载 test-nginx 的库,并运行/t
目录下的测试案例集:
示例解析
下面这段代码改编自官方文档
test::nginx
本身就是作者自己用 Perl 实现的 DSL(小语言),是专门针对 Nginx 和 OpenResty 的测试而抽象出来的。
use Test::Nginx::Socket;
,这是 Perl 里面引用库的方式,就像 Lua 里面 require 一样。这也在提醒我们,test::nginx
是一个 Perl 程序。run_tests();
,是test::nginx
中的一个 Perl 函数,它是测试框架的入口函数。如果你还想调用test::nginx
中其他的 Perl 函数,都要放在run_tests
之前才有效。__DATA__
是一个标记,表示它下面的都是测试数据。Perl 函数都应该在这个标记之前完成。=== TEST 1: set Server
,是测试案例的标题,是为了注明这个测试的目的,它里面的数字编号使用工具reindex
可以自动排列。--- config
是 Nginx 配置段。如果要添加 Lua 代码,也是在这里用类似 content_by_lua 的指令完成的。--- request
用于模拟终端来发送一个请求,下面紧跟的GET /foo
,则指明了请求的方法和 URI。--- response_headers
,是用来检测响应头的。下面的Server: Foo
表示在响应头中必须出现的 header 和 value,如果没有出现,测试就会失败。--- response_body
,是用来检测相应体的。下面的hi
则是响应体中必须出现的字符串,如果没有出现,测试就会失败;
编写测试案例
上节的测试
使用 test::nginx
改编
其它的测试框架:断言风格的测试框架
busted
可见,test::nginx
的测试,本质上是根据每一个测试案例的配置,先去生成 nginx.conf,并启动一个 Nginx 进程;然后,模拟客户端发起请求,其中包含指定的请求体和请求头;紧接着,测试案例中的 Lua 代码会处理请求并作出响应,这时,test::nginx
解析响应体、响应头、错误日志等关键信息,并和测试配置做对比。如果发现不符,就报错退出,测试失败;否则就算成功。
原语和使用方法
Nginx 配置
test::nginx
的原语中带有 config
这个关键字的,就和 Nginx 配置相关,比如前文中提到的 config
、stream_config
、http_config
等。
它们的作用都是一样的,即在 Nginx 的不同上下文中,插入指定的 Nginx 配置。这些配置可以是 Nginx 指令,也可以是 content_by_lua_block
封装起来的 Lua 代码。
节选自Apisix 项目 key-auth 插件的的一份测试用例:
这个测试案例的目的,是为了测试代码文件 plugins.key-auth
中, check_schema
这个函数能否正常工作。它在location /t
中使用 content_by_lua_block
这个 Nginx 指令,require 需要测试的模块,并直接调用需要检查的函数。
发送请求
request
这段代码在 request
原语中,发起了一个 GET 请求,地址是 /t
。
test::nginx
隐藏了 ip 地址、域名和端口、HTTP 版本号,这就是 DSL 的好处之一,你只需要关心业务逻辑,不用被各种细节所打扰。
当然也支持单独指定:
POST 示例
同样, test::nginx
在这里自动计算了请求体长度,并自动增加了 host
和 connection
这两个请求头,以保证这是一个正常的请求。
注释:
配合 eval
嵌入 perl 代码
这里使用 eval
来指定不可打印的字符,双引号之间的内容,会被当做 perl 的字符串来处理后,再传给 request
来作为参数。
用 POST 方法,向 /t
地址,发送包含 1024 个字符 a 的请求。
- 在 perl 中,用一个点号来表示字符串拼接
- 用小写的 x 来表示字符的重复次数。比如上面的
"a" x 1024
,就表示字符 a 重复 1024 次。
pipelined_requests
在同一个 keep-alive
的连接里面,依次发送多个请求
好处:
-
省去重复的测试代码,把 4 个测试案例压缩到一个测试案例中完成;
-
你可以用流水线的请求,来检测代码逻辑在多次访问的情况下,是否会有异常
test::nginx
在执行完每一个测试案例后, 都会关闭当前的 Nginx 进程,内存中所有数据也都随之消失了。当运行下一个测试案例时,又会重新生成 nginx.conf
,并启动新的 Nginx worker。这种机制是为了保证测试案例之间不会互相影响。
repeat_each
如何对同一个测试执行多次?
test::nginx
提供了一个全局的设置:repeat_each
。它其实是一个 perl 函数,默认情况下是 repeat_each(1)
,表示测试案例只运行一次。
在 run_test()
函数之前来设置它,比如将参数改为 2:
那么,每个测试案例就都会被运行两次,以此类推。
more_headers
添加请求头
多个头多行
处理响应
response_body
这个测试案例,在响应体是 hello
的情况下会通过,其他情况就会报错。
支持用正则来检测响应体:
支持 unlike 的操作:
这时候,如果响应体是hello
,测试就不能通过了。
多个请求的检测。下面是配合 pipelined_requests
一起使用的示例:
注意的是,发送了多少个请求,就需要有多少个响应来对应。
response_headers
响应头和请求头类似,每一行对应一个 header 的 key 和 value:
和响应体的检测一样,响应头也支持正则表达式和 unlike 操作,分别是 response_headers_like
、raw_response_headers_like
和 raw_response_headers_unlike
。
error_code
响应码也支持 like 操作
对于多个请求的情况,error_code
也需要检测多次:
error_log
没有错误日志,使用no_error_log
来检测:
在上面的例子中,如果 Nginx 的错误日志 error.log 中,出现 [error]
这个字符串,测试就会失败。建议在正常的测试中,都加上对错误日志的检测。
error_log
:检测错误日志中出现指定的字符串
上面这段配置,其实就在检测 error.log 中是否出现了 hello world
。
支持用 eval
嵌入 perl 代码:
调试相关原语
ONLY
只运行指定的某一个测试案例呢
把 --- ONLY
放在需要单独运行的测试案例的最后一行,那么使用 prove 来运行这个测试案例文件的时候,就会忽略其他所有的测试案例,只运行这一个测试了。
SKIP
忽略掉某一个测试案例。一般用于测试尚未实现的功能:
LAST
在它之前的测试案例集都会被执行,后面的就会被忽略掉:
测试计划 plan
这里 plan 的含义是,在整个测试文件中,按照计划应该会做多少次检测项。如果最终运行的结果和计划不符,整个测试就会失败。
拿这个示例来说,如果 repeat_each
的值是 2,一共有 10 个测试案例,那么 plan 的值就应该是 2 x 3 x 10 = 60。这里估计你唯一搞不清楚的,就是数字 3 的含义吧,看上去完全是一个 magic number!
上面这个测试案例中,plan = 2。
test::nginx
中隐含了一个校验,也就是--- error_code: 200
,它默认检测 HTTP 的 response code 是否为 200。
所以,上面的 magic number 3,真实含义是在每一个测试中都显式地检测了两次,比如 body 和 error log;同时,隐式地检测了 response code。
由于这个地方太容易出错,所以,我的建议是,推荐你用下面的方法,直接关闭掉 plan:
如果无法关闭,比如在 OpenResty 的官方测试集中遇到 plan 不准确的情况,建议你也不要去深究原因,直接在 plan 的表达式中增加或者减少数字即可:
这也是官方会使用到的方法。
预处理器
在同一个测试文件的不同测试案例之间,有一些共同的设置,使用 add_block_preprocessor
指令,来增加一段 perl 代码来抽出公共部分。比如:
这个预处理器,就会为所有的测试案例,都增加一段 config 的配置,而里面的内容就是 location /t
。这样,在后面的测试案例里,就都可以省略掉 config,直接访问即可:
自定义函数
可以在 run_tests
原语之前,随意地增加 perl 函数,也就是我们所说的自定义函数。
下面是一个示例,它增加了一个读取文件的函数,并结合 eval
指令,一起实现了 POST 文件的功能:
乱序
test::nginx
还有一个鲜为人知的坑:默认乱序、随机来执行测试案例,而非按照测试案例的前后顺序和编号来执行。
请关闭掉这个特性。可以用下面这两行代码来关闭:
reindex
工具 reindex
自动格式化测试用例,在 [openresty-devel-utils]中
(3 个换行分割测试用例,测试案例的编号自增长)
性能测试工具
ab
Apache Benchmark 利用不到机器的多核,生成的请求压力不够大,得到的结果,并不真实。
推荐使用 wrk
。
上面表示 wrk 使用 12 个线程,保持 400 个长连接,持续 30 秒钟,来给指定的 API 接口发送 HTTP 请求。
检查项一:关闭 SELinux
CentOS/RedHat 系列的操作系统,建议关闭 SELinux,不然可能会遇到不少诡异的权限问题。
检查项二:最大打开文件数
这里的最后一个数字,就是最大打开文件数。如果你的机器中这个数字比较小,那就需要修改 /etc/sysctl.conf
文件来增大:
修改完以后,还需要重启系统服务来生效:
检查项三:进程限制
除了系统的全局最大打开文件数,一个进程可以打开的文件数也是有限制的,你可以通过命令 ulimit
来查看:
这个值默认是 1024,是一个很低的数值。因为每一个用户请求都会对应着一个文件句柄,而压力测试会产生大量的请求,所以我们需要增大这个数值,把它改为百万级别,你可以用下面的命令来临时修改:
也可以修改配置文件 /etc/security/limits.conf
来永久生效:
检查项四:Nginx 配置
最后,你还需要对 Nginx 的配置,做一个小的修改,也就是下面这两行代码的操作:
这样,就可以把每个 worker 的连接数增大了。因为它的默认值只有 512,这在大压力的测试下显然是不够的。
设置完成后,也可以检测下当前的系统环境能否支持 100 万并发连接
https://github.com/ideawu/c1000k
开始测试
这里没有指定参数,wrk 会默认启动 2 个线程和 10 个长连接。
不需要把 wrk 的线程数和连接数调整得很大,只要能够让目标程序跑满 CPU 就达到要求了。
压测的时间一定不能太短。
在压测期间,你需要使用 top 或者 htop 这样的监控工具,来确认服务端目标程序是否跑满 CPU。比较理想的情况是,从现象上来看,如果 CPU 满载,而且压测停止后,CPU 和内存占用迅速降低。
但如果有下面这样的异常,需要留意
- CPU 不能满载。这不会是 wrk 的问题,可能是网络的限制,更可能是代码中有阻塞的操作。可以通过 review 代码来确定,也可以使用 off CPU 火焰图来确定。
- CPU 一直满载,即使压测停止仍然如此。这说明在代码中存在热循环,可能是正则表达式引起的,也可能是 LuaJIT 的 bug 引起的。这时,就需要用 on CPU 火焰图来确定了。
测试结果分析
我们一般会关注两个值:
第一个是 QPS,也就是 Requests/sec: 16582.76
,这个数据很直接,表示服务端每秒钟处理了多少请求。
第二个是延时 Latency 595.39us 178.51us 22.24ms 90.63%
,这个数据和 QPS 一样重要,它体现了系统的响应速度。比如对于网关的应用来讲,我们就希望能够把延时控制在 1 毫秒以内。
另外, wrk 还提供了 latency 参数,可以把延时的分布百分比详细地打印出来,比如:
不过,wrk 的延时分布数据并不准确,因为它人为地加入了网络和工具的扰动,放大了延时,这一点需要你特别注意。关于 wrk Latency Distribution,这篇文章了解详细。
性能测试工具可能存在 Coordinated Omission (协调遗漏)问题,在分析工具的延时数据的时候,你一定要特别留意。
Coordinated Omission(协调遗漏) 是指,在做压力测试时,对于响应来说,只统计发送和收到回复之间的时间是不够的,这只是服务时间,这样统计会遗漏很多潜在的问题。因此,我们还需要把测试请求的等待时间也计算在内,这个整体才算是用户关心的响应时间。当然,如果你的服务端程序可能会出现阻塞,一定需要考虑这个问题,否则就可以忽略掉了。