mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-15 21:53:49 +08:00
mod
This commit is contained in:
188
极客时间专栏/OpenResty从入门到实战/测试篇/26 | 代码贡献者的拦路虎:test::nginx 简介.md
Normal file
188
极客时间专栏/OpenResty从入门到实战/测试篇/26 | 代码贡献者的拦路虎:test::nginx 简介.md
Normal file
@@ -0,0 +1,188 @@
|
||||
<audio id="audio" title="26 | 代码贡献者的拦路虎:test::nginx 简介" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/64/b3fb7ed797152551cbee231d8a946864.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
测试,是软件开发中必不可少的一个重要环节。测试驱动开发(TDD)的理念已经深入人心,几乎每家软件公司都有 QA 团队来负责测试的工作。
|
||||
|
||||
测试也是 OpenResty 质量稳定和好口碑的基石,不过同时,它也是 OpenResty 众多开源项目中最被人忽视的部分。很多开发者每天都在使用 lua-nginx-module,偶尔跑一跑火焰图,但有几个人会去运行测试案例呢?甚至很多基于 OpenResty 的开源项目,都是没有测试案例的。但没有测试案例和持续集成的开源项目,显然是不值得信赖的。
|
||||
|
||||
不过,和商业公司不同的是,大部分的开源项目都没有专职的测试工程师,那么它们是如何来保证代码质量的呢?答案很简单,就是“自动化测试”和“持续集成”,关键点在于自动和持续,而OpenResty 在这两个方面都做到了极致。
|
||||
|
||||
OpenResty 有 70 个开源项目,它们的单元测试、集成测试、性能测试、mock 测试、fuzz 测试等工作量,是无法靠社区的人力解决的。所以,OpenResty 一开始在自动化测试上的投入就比较大。这样做短期看起来会拖慢项目进度,但可以说是一劳永逸,长期来看在这方面的投入是非常划算的。因此,每当我和其他工程师聊起 OpenResty 在测试方面的思路和工具集时,他们都会惊叹不已。
|
||||
|
||||
下面,我们就先来说说OpenResty的测试理念。
|
||||
|
||||
## 理念
|
||||
|
||||
`test::nginx` 是 OpenResty 测试体系中的核心,OpenResty 本身和周边的 lua-rety 库,都是使用它来组织和编写测试集的。虽然它一个是测试框架,但它的**门槛非常高**。这是因为, `test::nginx` 和一般的测试框架不同,并非基于断言,也不使用 Lua 语言,这就要求开发者从零开始学习和使用 `test::nginx`,并得扭转自身对测试框架固有的认知。
|
||||
|
||||
我认识几个 OpenResty 的贡献者,他们可以流畅地给 OpenResty 提交 C 和 Lua 代码,但在使用 `test::nginx` 编写测试用例时都卡壳了,要么不知道怎么写,要么遇到测试跑不过时不知道如何解决。所以,我把 `test::nginx` 称为代码贡献者的拦路虎。
|
||||
|
||||
`test::nginx` **糅合了Perl、数据驱动以及 DSL(领域小语言)**。对于同一份测试案例集,通过对参数和环境变量的控制,可以实现乱序执行、多次重复、内存泄漏检测、压力测试等不同的效果。
|
||||
|
||||
## 安装和示例
|
||||
|
||||
说了这么多概念,让我们来对 `test::nginx` 有一个直观的认识吧。在使用前,我们先来看下如何安装。
|
||||
|
||||
关于 OpenResty 体系内软件的安装,只有官方 CI 中的安装方法才是最及时和有效的,其他方式的安装总是会遇到各种各样的问题。所以,我总是推荐你去参考它在 travis 中的[方法](https://github.com/openresty/lua-resty-core/blob/master/.travis.yml)。
|
||||
|
||||
`test::nginx` 的安装和使用也不例外,在 travis 中,它可以分为 4 步。
|
||||
|
||||
**1. **先安装 Perl 的包管理器 cpanminus。<br>
|
||||
**2. **然后,通过 cpanm 来安装 `test::nginx`:
|
||||
|
||||
```
|
||||
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
|
||||
|
||||
```
|
||||
|
||||
**3. **再接着, clone 最新的源码:
|
||||
|
||||
```
|
||||
git clone https://github.com/openresty/test-nginx.git
|
||||
|
||||
```
|
||||
|
||||
**4. **最后,通过 Perl 的 `prove` 命令来加载 test-nginx 的库,并运行 `/t` 目录下的测试案例集:
|
||||
|
||||
```
|
||||
prove -Itest-nginx/lib -r t
|
||||
|
||||
```
|
||||
|
||||
安装完以后,让我们看下 `test::nginx` 中最简单的测试案例。下面这段代码改编自[官方文档](https://metacpan.org/pod/Test::Nginx::Socket),我已经把个性化的控制参数都去掉了:
|
||||
|
||||
```
|
||||
use Test::Nginx::Socket 'no_plan';
|
||||
|
||||
|
||||
run_tests();
|
||||
|
||||
__DATA__
|
||||
|
||||
=== TEST 1: set Server
|
||||
--- config
|
||||
location /foo {
|
||||
echo hi;
|
||||
more_set_headers 'Server: Foo';
|
||||
}
|
||||
--- request
|
||||
GET /foo
|
||||
--- response_headers
|
||||
Server: Foo
|
||||
--- response_body
|
||||
hi
|
||||
|
||||
```
|
||||
|
||||
虽然 `test::nginx` 是用 Perl 编写的,并且是其中的一个模块,但从上面的测试中,你是不是完全看不到,Perl 或者其他任何其他语言的影子呀?有这个感觉这就对了。因为,`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`,是测试案例的标题,是为了注明这个测试的目的,它里面的数字编号有工具可以自动排列。
|
||||
|
||||
`--- config` 是 Nginx 配置段。在上面的案例中,我们用的都是 Nginx 的指令,没有涉及到 Lua。如果你要添加 Lua 代码,也是在这里用类似 content_by_lua 的指令完成的。
|
||||
|
||||
`--- request` 用于模拟终端来发送一个请求,下面紧跟的 `GET /foo` ,则指明了请求的方法和 URI。
|
||||
|
||||
`--- response_headers`,是用来检测响应头的。下面的 `Server: Foo` 表示在响应头中必须出现的 header 和 value,如果没有出现,测试就会失败。
|
||||
|
||||
最后的`--- response_body`,是用来检测相应体的。下面的 `hi` 则是响应体中必须出现的字符串,如果没有出现,测试就会失败;
|
||||
|
||||
好了,到这里,最简单的测试案例就分析完了,你看明白了吗?如果哪里还不清楚,一定要及时留言提问暴露出来,毕竟,能够看懂测试案例,是完成 OpenResty 相关开发工作的前提。
|
||||
|
||||
## 编写自己的测试案例
|
||||
|
||||
光说不练假把式,接下来,我们就该进入动手试验环节了。还记得上节课中,我们是如何测试 memcached server 的吗?没错,我们是用 `resty` 来手动发送请求的,也就是用下面这段代码表示:
|
||||
|
||||
```
|
||||
$ resty -e 'local memcached = require "resty.memcached"
|
||||
local memc, err = memcached:new()
|
||||
|
||||
memc:set_timeout(1000) -- 1 sec
|
||||
local ok, err = memc:connect("127.0.0.1", 11212)
|
||||
local ok, err = memc:set("dog", 32)
|
||||
if not ok then
|
||||
ngx.say("failed to set dog: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local res, flags, err = memc:get("dog")
|
||||
ngx.say("dog: ", res)'
|
||||
|
||||
```
|
||||
|
||||
不过,是不是觉得手动发送还不够智能呢?没关系,在学习完 `test::nginx` 之后,我们就可以尝试把手动的测试变为自动化的了,比如下面这段代码:
|
||||
|
||||
```
|
||||
use Test::Nginx::Socket::Lua::Stream;
|
||||
|
||||
run_tests();
|
||||
|
||||
__DATA__
|
||||
|
||||
=== TEST 1: basic get and set
|
||||
--- config
|
||||
location /test {
|
||||
content_by_lua_block {
|
||||
local memcached = require "resty.memcached"
|
||||
local memc, err = memcached:new()
|
||||
if not memc then
|
||||
ngx.say("failed to instantiate memc: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
memc:set_timeout(1000) -- 1 sec
|
||||
local ok, err = memc:connect("127.0.0.1", 11212)
|
||||
|
||||
local ok, err = memc:set("dog", 32)
|
||||
if not ok then
|
||||
ngx.say("failed to set dog: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local res, flags, err = memc:get("dog")
|
||||
ngx.say("dog: ", res)
|
||||
}
|
||||
}
|
||||
|
||||
--- stream_config
|
||||
lua_shared_dict memcached 100m;
|
||||
|
||||
--- stream_server_config
|
||||
listen 11212;
|
||||
content_by_lua_block {
|
||||
local m = require("memcached-server")
|
||||
m.go()
|
||||
}
|
||||
|
||||
--- request
|
||||
GET /test
|
||||
--- response_body
|
||||
dog: 32
|
||||
--- no_error_log
|
||||
[error]
|
||||
|
||||
```
|
||||
|
||||
在这个测试案例中,我新增了 `--- stream_config`、`--- stream_server_config`、`--- no_error_log` 这些配置项,但它们的本质上都是一样的,即:
|
||||
|
||||
**通过抽象好的原语(也可以看做配置),把测试的数据和检测进行剥离,让可读性和扩展性变得更好。**
|
||||
|
||||
这就是 `test::nginx` 和其他测试框架的根本不同之处。这种 DSL 是一把双刃剑,它可以让测试逻辑变得清晰和方便扩展,但同时也提高了学习的门槛,你需要重新学习新的语法和配置才能开始编写测试案例。
|
||||
|
||||
## 写在最后
|
||||
|
||||
不得不说,`test::nginx` 虽然强大,但很多时候,它可能不一定适合你的场景。杀鸡焉用宰牛刀?在 OpenResty 中,你也选择使用断言风格的测试框架 `busted`。`busted`结合 `resty` 这个命令行工具,也可以满足不少测试的需求。
|
||||
|
||||
最后,给你留一个作业题,你可以在本地把 memcached 的这个测试跑起来吗?如果你能新增一个测试案例,那就更棒了。
|
||||
|
||||
欢迎在留言区记录你的操作和心得,也可以写下你今天学习的疑惑地方。同时,欢迎你把这篇文章分享给更多对OpenResty感兴趣的人,我们一起交流和探讨。
|
||||
|
||||
|
||||
300
极客时间专栏/OpenResty从入门到实战/测试篇/27 | test::nginx 包罗万象的测试方法.md
Normal file
300
极客时间专栏/OpenResty从入门到实战/测试篇/27 | test::nginx 包罗万象的测试方法.md
Normal file
@@ -0,0 +1,300 @@
|
||||
<audio id="audio" title="27 | test::nginx 包罗万象的测试方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dc/9d/dcf24f0a273be28566f2b8424e0ef39d.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
通过上节课的学习,你已经对 `test::nginx` 有了一个初步的认识,并运行了最简单的示例。不过,在实际的开源项目中,`test::nginx` 编写的测试案例显然要比示例代码复杂得多,也更加难以掌握,不然它也就称不上是拦路虎了。
|
||||
|
||||
在本节课中,我会带你来熟悉下 `test::nginx` 中经常用到的指令和测试方法,目的是让你可以看明白 OpenResty 项目中大部分的测试案例集,并有能力来编写更真实的测试案例。即使你还没有给 OpenResty 贡献过代码,但熟悉了 OpenResty 的测试框架,对于你平时工作中设计和编写测试案例,还是会有不少启发的。
|
||||
|
||||
`test::nginx` 的测试,本质上是根据每一个测试案例的配置,先去生成 nginx.conf,并启动一个 Nginx 进程;然后,模拟客户端发起请求,其中包含指定的请求体和请求头;紧接着,测试案例中的 Lua 代码会处理请求并作出响应,这时,`test::nginx` 解析响应体、响应头、错误日志等关键信息,并和测试配置做对比。如果发现不符,就报错退出,测试失败;否则就算成功。
|
||||
|
||||
`test::nginx` 中提供了很多 DSL 的原语,我按照 Nginx 配置、发送请求、处理响应、检查日志这个流程,做了一个简单的分类。这 20% 的功能可以覆盖 80% 的应用场景,所以你一定要牢牢掌握。至于其他更高级的原语和使用方法,我们留到下一节再来介绍。
|
||||
|
||||
## Nginx 配置
|
||||
|
||||
我们首先来看下 Nginx 配置。`test::nginx` 的原语中带有 `config` 这个关键字的,就和 Nginx 配置相关,比如上一节中提到的 `config`、`stream_config`、`http_config` 等。
|
||||
|
||||
它们的作用都是一样的,即在 Nginx 的不同上下文中,插入指定的 Nginx 配置。这些配置可以是 Nginx 指令,也可以是 `content_by_lua_block` 封装起来的 Lua 代码。
|
||||
|
||||
在做单元测试的时候,`config` 是最常用的原语,我们会在其中加载 Lua 库,并调用函数来做白盒测试。下面是节选的一段测试代码,并不能完整运行。它来自一个真实的开源项目,如果你对此有兴趣,可以点击[链接](https://github.com/iresty/apisix/blob/master/t/plugin/key-auth.t#L11)查看完整的测试,也可以尝试在本机运行。
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local plugin = require("apisix.plugins.key-auth")
|
||||
local ok, err = plugin.check_schema({key = 'test-key'})
|
||||
if not ok then
|
||||
ngx.say(err)
|
||||
end
|
||||
ngx.say("done")
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个测试案例的目的,是为了测试代码文件 `plugins.key-auth` 中, `check_schema` 这个函数能否正常工作。它在`location /t` 中使用 `content_by_lua_block` 这个 Nginx 指令,require 需要测试的模块,并直接调用需要检查的函数。
|
||||
|
||||
这就是在 `test::nginx` 进行白盒测试的通用手段。不过,只有这段配置自然是无法完成测试的,下面我们继续看下,如何发起客户端的请求。
|
||||
|
||||
## 发送请求
|
||||
|
||||
模拟客户端发送请求,会涉及到不少的细节,所以,我们就先从最简单的发送单个请求入手吧。
|
||||
|
||||
### request
|
||||
|
||||
还是继续上面的测试案例,如果你想要单元测试的代码被运行,那就要发起一个 HTTP 请求,访问的地址是 config 中注明的 `/t`,正如下面的测试代码所示:
|
||||
|
||||
```
|
||||
--- request
|
||||
GET /t
|
||||
|
||||
```
|
||||
|
||||
这段代码在 `request` 原语中,发起了一个 GET 请求,地址是 `/t`。这里,我们并没有注明访问的 ip 地址、域名和端口,也没有指定是 HTTP 1.0 还是 HTTP 1.1,这些细节都被 `test::nginx` 隐藏了,你不用去关心。这就是 DSL 的好处之一——你只需要关心业务逻辑,不用被各种细节所打扰。
|
||||
|
||||
同时,这也提供了部分的灵活性。比如默认是 HTTP 1.1 的协议,如果你想测试 HTTP 1.0,也可以单独指定:
|
||||
|
||||
```
|
||||
--- request
|
||||
GET /t HTTP/1.0
|
||||
|
||||
|
||||
```
|
||||
|
||||
除了 GET 方法之外,POST 方法也是需要支持的。下面这个示例,可以 POST `hello world` 这个字符串到指定的地址:
|
||||
|
||||
```
|
||||
--- request
|
||||
POST /t
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
同样的, `test::nginx` 在这里为你自动计算了请求体长度,并自动增加了 `host` 和 `connection` 这两个请求头,以保证这是一个正常的请求。
|
||||
|
||||
当然,出于可读性的考虑,你可以在其中增加注释。以 `#` 开头的,就会被识别为代码注释:
|
||||
|
||||
```
|
||||
--- request
|
||||
# post request
|
||||
POST /t
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
`request` 还支持更为复杂和灵活的模式,那就是配合 `eval` 这个 filter,直接嵌入 perl 代码,毕竟 `test::nginx` 就是perl 编写的。这种做法,类似于在 DSL 之外开了一个后门,如果当前的 DSL 原语都不能满足你的需求,那么 `eval` 这种直接执行 perl 代码的方法,就可以说是“终极武器”了。
|
||||
|
||||
关于 `eval`的用法,这里我们先看几个简单的例子,其他更复杂的,我们下节课继续介绍:
|
||||
|
||||
```
|
||||
--- request eval
|
||||
"POST /t
|
||||
hello\x00\x01\x02
|
||||
world\x03\x04\xff"
|
||||
|
||||
```
|
||||
|
||||
第一个例子中,我们用 `eval` 来指定不可打印的字符,这也是它的用处之一。双引号之间的内容,会被当做 perl 的字符串来处理后,再传给 `request` 来作为参数。
|
||||
|
||||
下面是一个更有趣的例子:
|
||||
|
||||
```
|
||||
--- request eval
|
||||
"POST /t\n" . "a" x 1024
|
||||
|
||||
```
|
||||
|
||||
不过,要看懂这个例子,需要懂一些 perl 的字符串知识,这里我简单提两句:
|
||||
|
||||
- 在 perl 中,我们用一个点号来表示字符串拼接,这是不是和 Lua 的两个点号有些类似呢?
|
||||
- 用小写的 x 来表示字符的重复次数。比如上面的 `"a" x 1024`,就表示字符 a 重复 1024 次。
|
||||
|
||||
所以,第二个例子的含义是,用 POST 方法,向 `/t` 地址,发送包含 1024 个字符 a 的请求。
|
||||
|
||||
### pipelined_requests
|
||||
|
||||
了解完如何发送单个请求后,我们再来看下如何发送多个请求。在 `test::nginx` 中,你可以使用 `pipelined_requests` 这个原语,在同一个 `keep-alive` 的连接里面,依次发送多个请求:
|
||||
|
||||
```
|
||||
--- pipelined_requests eval
|
||||
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
|
||||
|
||||
```
|
||||
|
||||
比如这个示例就会在同一个连接中,依次访问这 4 个接口。这样做会有两个好处:
|
||||
|
||||
- 第一是可以省去不少重复的测试代码,把 4 个测试案例压缩到一个测试案例中完成;
|
||||
- 第二也是最重要的原因,你可以用流水线的请求,来检测代码逻辑在多次访问的情况下,是否会有异常。
|
||||
|
||||
你可能会奇怪,我依次写多个测试案例,那么执行的时候,代码也会被多次执行,不也可以覆盖上面的第二个问题吗?
|
||||
|
||||
其实,这就涉及到 `test::nginx` 的执行模式了,它并非像你想象中的那样去运转。事实上,在执行完每一个测试案例后, `test::nginx` 都会关闭当前的 Nginx 进程,自然的,内存中所有数据也都随之消失了。当运行下一个测试案例时,又会重新生成 `nginx.conf`,并启动新的 Nginx worker。这种机制是为了保证测试案例之间不会互相影响。
|
||||
|
||||
所以,当你要测试多个请求时,就需要用到 `pipelined_requests` 这个原语了。基于它,你可以模拟出限流、限速、限并发等多种情况,用更真实和复杂的场景来检测你的系统是否正常。这一点,我们也留在下节课继续拆解,因为它会涉及到多个指令和原语的配合。
|
||||
|
||||
### repeat_each
|
||||
|
||||
刚才我们提到了测试多个请求的情况,那么应该如何对同一个测试执行多次呢?
|
||||
|
||||
针对这个问题,`test::nginx` 提供了一个全局的设置:`repeat_each`。它其实是一个 perl 函数,默认情况下是 `repeat_each(1)`,表示测试案例只运行一次。所以之前的测试案例中,我们都没有去单独设置它。
|
||||
|
||||
自然,你可以在 `run_test()` 函数之前来设置它,比如将参数改为2:
|
||||
|
||||
```
|
||||
repeat_each(2);
|
||||
run_tests();
|
||||
|
||||
```
|
||||
|
||||
那么,每个测试案例就都会被运行两次,以此类推。
|
||||
|
||||
### more_headers
|
||||
|
||||
聊完了请求体,我们再来看下请求头。上面我们提到,`test::nginx` 在发送请求的时候,默认会带上 `host` 和 `connection` 这两个请求头。那么其他的请求头如何设置呢?
|
||||
|
||||
其实,`more_headers` 就是专门做这件事儿的:
|
||||
|
||||
```
|
||||
--- more_headers
|
||||
X-Foo: blah
|
||||
|
||||
```
|
||||
|
||||
你可以用它来设置各种自定义的头。如果想设置多个头,那设置多行就可以了:
|
||||
|
||||
```
|
||||
--- more_headers
|
||||
X-Foo: 3
|
||||
User-Agent: openresty
|
||||
|
||||
```
|
||||
|
||||
## 处理响应
|
||||
|
||||
发送完请求后,`test::nginx` 中最重要的部分就来了,那就是处理响应,我们会在这里判断响应是否符合预期。这里我们分为 4 个部分依次介绍,分别是响应体、响应头、响应码和日志。
|
||||
|
||||
### response_body
|
||||
|
||||
与 `request` 原语对应的就是 `response_body`,下面是它们两个配置使用的例子:
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
ngx.say("hello")
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
--- response_body
|
||||
hello
|
||||
|
||||
|
||||
```
|
||||
|
||||
这个测试案例,在响应体是 `hello` 的情况下会通过,其他情况就会报错。但如何返回体很长,我们怎么检测才合适呢?别着急,`test::nginx` 已经为你考虑好了,它支持用用正则表达式来检测响应体,比如下面这样的写法:
|
||||
|
||||
```
|
||||
--- response_body_like
|
||||
^he\w+$
|
||||
|
||||
```
|
||||
|
||||
这样你就可以对响应体进行非常灵活的检测了。不仅如此,`test::nginx` 还支持 unlike 的操作:
|
||||
|
||||
```
|
||||
--- response_body_unlike
|
||||
^he\w+$
|
||||
|
||||
```
|
||||
|
||||
这时候,如果响应体是`hello`,测试就不能通过了。
|
||||
|
||||
同样的思路,了解完单个请求的检测后,我们再来看下多个请求的检测。下面是配合 `pipelined_requests` 一起使用的示例:
|
||||
|
||||
```
|
||||
--- pipelined_requests eval
|
||||
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
|
||||
--- response_body eval
|
||||
["hello", "world", "oo", "bar"]
|
||||
|
||||
```
|
||||
|
||||
当然,这里需要注意的是,你发送了多少个请求,就需要有多少个响应来对应。
|
||||
|
||||
### response_headers
|
||||
|
||||
第二个我们来说说响应头。响应头和请求头类似,每一行对应一个 header 的 key 和 value:
|
||||
|
||||
```
|
||||
--- response_headers
|
||||
X-RateLimit-Limit: 2
|
||||
X-RateLimit-Remaining: 1
|
||||
|
||||
```
|
||||
|
||||
和响应体的检测一样,响应头也支持正则表达式和 unlike 操作,分别是 `response_headers_like` 、`raw_response_headers_like` 和 `raw_response_headers_unlike`。
|
||||
|
||||
### error_code
|
||||
|
||||
第三个来看响应码。响应码的检测支持直接的比较,同时也支持 like 操作,比如下面两个示例:
|
||||
|
||||
```
|
||||
--- error_code: 302
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
--- error_code_like: ^(?:500)?$
|
||||
|
||||
```
|
||||
|
||||
而对于多个请求的情况,`error_code` 自然也需要检测多次:
|
||||
|
||||
```
|
||||
--- pipelined_requests eval
|
||||
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
|
||||
--- error_code eval
|
||||
[200, 200, 503, 503]
|
||||
|
||||
```
|
||||
|
||||
### error_log
|
||||
|
||||
最后一个检测项,就是错误日志了。在大部分的测试案例中,都不会产生错误日志。我们可以用 `no_error_log` 来检测:
|
||||
|
||||
```
|
||||
--- no_error_log
|
||||
[error]
|
||||
|
||||
```
|
||||
|
||||
在上面的例子中,如果 Nginx 的错误日志 error.log 中,出现 `[error]` 这个字符串,测试就会失败。这是一个很常用的功能,建议在你正常的测试中,都加上对错误日志的检测。
|
||||
|
||||
自然,另一方面,我们也需要编写很多异常的测试案例,以便验证在出错的情况下,我们的代码是否正常处理。这种情况下,我们就需要错误日志中出现指定的字符串,这就是 `error_log` 的用武之地了:
|
||||
|
||||
```
|
||||
--- error_log
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
上面这段配置,其实就在检测 error.log 中是否出现了 `hello world`。当然,你可以在其中,用 `eval` 嵌入 perl 代码的方式,来实现正则表达式的检测,比如下面这样的写法:
|
||||
|
||||
```
|
||||
--- error_log eval
|
||||
qr/\[notice\] .*? \d+ hello world/
|
||||
|
||||
```
|
||||
|
||||
## 写在最后
|
||||
|
||||
今天,我们学习的是如何在 `test::nginx` 中发送请求和检测响应,包含了 body、header、响应码和错误日志等。通过这些原语的组合,你可以实现比较完整的测试案例集。
|
||||
|
||||
最后,给你留一个思考题:`test::nginx` 这种抽象一层的 DSL,你觉得有什么优势和劣势吗?欢迎留言和我探讨,也欢迎你把这篇文章分享出去,一起交流和思考。
|
||||
|
||||
|
||||
211
极客时间专栏/OpenResty从入门到实战/测试篇/28 | test::nginx 还可以这样用?.md
Normal file
211
极客时间专栏/OpenResty从入门到实战/测试篇/28 | test::nginx 还可以这样用?.md
Normal file
@@ -0,0 +1,211 @@
|
||||
<audio id="audio" title="28 | test::nginx 还可以这样用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3f/21/3f20a1b788027e4950de581b9bce6621.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
在前面两个章节中,你已经掌握了 `test::nginx` 的大部分使用方法,我相信你已经能够看明白 OpenResty 项目中大部分的测试案例集了。这对于学习 OpenResty 和它的周边库而言,已经足够了。
|
||||
|
||||
但如果你有志于成为 OpenResty 的代码贡献者,或者你正在自己的项目中使用 `test::nginx` 来编写测试案例,那么你还需要来学习一些更高级、更复杂的用法。
|
||||
|
||||
今天的内容,可能会是这个专栏中最“高冷”的部分,因为这都是从来没有人分享过的内容。 以 lua-nginx-module 这个 OpenResty 中最核心的模块为例,全球一共有 70 多个贡献者,但并非每个贡献者都写过测试案例。所以,如果学完今天的课程,你在 `test::nginx` 上的理解,绝对可以进入全球 Top 100。
|
||||
|
||||
## 测试中的调试
|
||||
|
||||
首先,我们来看几个最简单、也是开发者最常用到的原语,它们在平时的调试中会被使用到。下面,我们就来依次介绍下,这几个调试相关的原语的使用场景。
|
||||
|
||||
### ONLY
|
||||
|
||||
很多时候,我们都是在原有的测试案例集基础上,新增了一个测试案例。如果这个测试文件包含了很多的测试案例,那么从头到尾跑一遍显然是比较耗时的,这在你需要反复修改测试案例的时候尤为明显。
|
||||
|
||||
那么,有没有什么方法只运行你指定的某一个测试案例呢? `ONLY` 这个标记可以轻松实现这一点:
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
=== TEST 2: get
|
||||
--- ONLY
|
||||
|
||||
```
|
||||
|
||||
上面这段伪码就展示了如何使用这个原语。把 `--- ONLY` 放在需要单独运行的测试案例的最后一行,那么使用 prove 来运行这个测试案例文件的时候,就会忽略其他所有的测试案例,只运行这一个测试了。
|
||||
|
||||
不过,这只适合在你做调试的时候使用。所以, prove 命令发现 ONLY 标记的时候,也会给出提示,告诉你不要忘记在提交代码时把它去掉。
|
||||
|
||||
### SKIP
|
||||
|
||||
与只执行一个测试案例对应的需求,就是忽略掉某一个测试案例。`SKIP` 这个标记,一般用于测试尚未实现的功能:
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
=== TEST 2: get
|
||||
--- SKIP
|
||||
|
||||
```
|
||||
|
||||
从这段伪码你可以看到,它的用法和ONLY类似。因为我们是测试驱动开发,需要先编写测试案例;而在集体编码实现时,可能由于实现难度或者优先级的关系,导致某个功能需要延后实现。那么这时候,你就可以先跳过对应的测试案例集,等实现完成后,再把 SKIP 标记去掉即可。
|
||||
|
||||
### LAST
|
||||
|
||||
还有一个常用的标记是 `LAST`,它的用法也很简单,在它之前的测试案例集都会被执行,后面的就会被忽略掉:
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
=== TEST 2: get
|
||||
--- LAST
|
||||
=== TEST 3: set
|
||||
|
||||
```
|
||||
|
||||
你可能疑惑,ONLY和SKIP我能理解,但LAST这个功能有什么用呢?实际上,有时候你的测试案例是有依赖关系的,需要你执行完前面几个测试案例后,之后的测试才有意义。那么,在这种情况下去调试的话,LAST 就非常有用了。
|
||||
|
||||
## 测试计划 plan
|
||||
|
||||
在 `test::nginx` 所有的原语中,`plan` 是最容易让人抓狂、也是最难理解的一个。它源自于 perl 的 `Test::Plan` 模块,所以文档并不在 `test::nginx`中,找到它的解释并不容易,所以我把它放在靠前的位置来介绍。我见过好几个 OpenResty 的代码贡献者,都在这个坑里面跌倒,甚至爬不出来。
|
||||
|
||||
下面是一个示例,在 OpenResty 官方测试集的每一个文件的开始部分,你都能看到类似的配置:
|
||||
|
||||
```
|
||||
plan tests => repeat_each() * (3 * blocks());
|
||||
|
||||
```
|
||||
|
||||
这里 plan 的含义是,在整个测试文件中,按照计划应该会做多少次检测项。如果最终运行的结果和计划不符,整个测试就会失败。
|
||||
|
||||
拿这个示例来说,如果 `repeat_each` 的值是 2,一共有 10 个测试案例,那么 plan 的值就应该是2 x 3 x 10 = 60。这里估计你唯一搞不清楚的,就是数字 3 的含义吧,看上去完全是一个 magic number!
|
||||
|
||||
别着急,我们继续看示例,一会儿你就能搞懂了。先来说说,你能算清楚下面这个测试案例中,plan 的正确值是多少吗?
|
||||
|
||||
```
|
||||
=== TEST 1: sanity
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
ngx.say("hello")
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
--- response_body
|
||||
hello
|
||||
|
||||
```
|
||||
|
||||
我相信所有人都会得出 plan = 1 的结论,因为测试中只对 `response_body` 进行了校验。
|
||||
|
||||
但,事实并非如此!正确的答案是, plan = 2。为什么呢?因为 `test::nginx` 中隐含了一个校验,也就是`--- error_code: 200`,它默认检测 HTTP 的 response code 是否为 200。
|
||||
|
||||
所以,上面的 magic number 3,真实含义是在每一个测试中都显式地检测了两次,比如 body 和 error log;同时,隐式地检测了 response code。
|
||||
|
||||
由于这个地方太容易出错,所以,我的建议是,推荐你用下面的方法,直接关闭掉 plan:
|
||||
|
||||
```
|
||||
use Test::Nginx::Socket 'no_plan';
|
||||
|
||||
```
|
||||
|
||||
如果无法关闭,比如在 OpenResty 的官方测试集中遇到 plan 不准确的情况,建议你也不要去深究原因,直接在 plan 的表达式中增加或者减少数字即可:
|
||||
|
||||
```
|
||||
plan tests => repeat_each() * (3 * blocks()) + 2;
|
||||
|
||||
```
|
||||
|
||||
这也是官方会使用到的方法。
|
||||
|
||||
## 预处理器
|
||||
|
||||
我们知道,在同一个测试文件的不同测试案例之间,可能会有一些共同的设置。如果在每一个测试案例中都重复设置,就会让代码显得冗余,后面修改起来也比较麻烦。
|
||||
|
||||
这时候,你就可以使用 `add_block_preprocessor` 指令,来增加一段 perl 代码,比如下面这样来写:
|
||||
|
||||
```
|
||||
add_block_preprocessor(sub {
|
||||
my $block = shift;
|
||||
|
||||
if (!defined $block->config) {
|
||||
$block->set_value("config", <<'_END_');
|
||||
location = /t {
|
||||
echo $arg_a;
|
||||
}
|
||||
_END_
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这个预处理器,就会为所有的测试案例,都增加一段 config 的配置,而里面的内容就是 `location /t`。这样,在你后面的测试案例里,就都可以省略掉 config,直接访问即可:
|
||||
|
||||
```
|
||||
=== TEST 1:
|
||||
--- request
|
||||
GET /t?a=3
|
||||
--- response_body
|
||||
3
|
||||
|
||||
=== TEST 2:
|
||||
--- request
|
||||
GET /t?a=blah
|
||||
--- response_body
|
||||
blah
|
||||
|
||||
```
|
||||
|
||||
## 自定义函数
|
||||
|
||||
除了在预处理器中增加 perl 代码之外,你还可以在 `run_tests` 原语之前,随意地增加 perl 函数,也就是我们所说的自定义函数。
|
||||
|
||||
下面是一个示例,它增加了一个读取文件的函数,并结合 `eval` 指令,一起实现了 POST 文件的功能:
|
||||
|
||||
```
|
||||
sub read_file {
|
||||
my $infile = shift;
|
||||
open my $in, $infile
|
||||
or die "cannot open $infile for reading: $!";
|
||||
my $content = do { local $/; <$in> };
|
||||
close $in;
|
||||
$content;
|
||||
}
|
||||
|
||||
our $CONTENT = read_file("t/test.jpg");
|
||||
|
||||
run_tests;
|
||||
|
||||
__DATA__
|
||||
|
||||
=== TEST 1: sanity
|
||||
--- request eval
|
||||
"POST /\n$::CONTENT"
|
||||
|
||||
```
|
||||
|
||||
## 乱序
|
||||
|
||||
除了上面几点外,`test::nginx` 还有一个鲜为人知的坑:默认乱序、随机来执行测试案例,而非按照测试案例的前后顺序和编号来执行。
|
||||
|
||||
它的初衷是想测试出更多的问题。毕竟,每一个测试案例运行完后,都会关闭 Nginx 进程,并启动新的 Nginx 来执行,结果不应该和顺序相关才对。
|
||||
|
||||
对于底层的项目而言,确实如此。但是,对于应用层的项目来说,外部存在数据库等持久化存储。这时候的乱序执行,就会导致错误的结果。由于每次都是随机的,所以可能报错,也可能不报错,每次的报错还可能不同。这显然会给开发者带来困惑,就连我都在这里跌倒过好多次。
|
||||
|
||||
所以,我的忠告就是:请关闭掉这个特性。你可以用下面这两行代码来关闭:
|
||||
|
||||
```
|
||||
no_shuffle();
|
||||
run_tests;
|
||||
|
||||
```
|
||||
|
||||
其中,`no_shuffle` 原语就是用来禁用随机,让测试严格按照测试案例的前后顺序来运行。
|
||||
|
||||
## reindex
|
||||
|
||||
最后,让我们聊一个不烧脑的、轻松一点儿的话题。OpenResty 的测试案例集,对格式有着严格的要求。每个测试案例之间都需要有 3 个换行来分割,测试案例的编号也要严格保持自增长。
|
||||
|
||||
幸好,我们有对应的自动化工具 `reindex` 来做这些繁琐的事情,它隐藏在 [[ openresty-devel-utils]](https://github.com/openresty/openresty-devel-utils) 项目中,因为没有文档来介绍,知道的人很少。
|
||||
|
||||
有兴趣的同学,可以尝试着把测试案例的编号打乱,或者增删分割的换行个数,然后用这个工具来整理下,看看是否可以还原。
|
||||
|
||||
## 写在最后
|
||||
|
||||
关于 `test::nginx` 的介绍就到此结束了。当然,它的功能其实还有更多,我们只讲了最核心最重要的一些。授人以鱼不如授人以渔,学习测试的基本方法和注意点我都已经教给你了,剩下的就需要你自己去官方的测试案例集中去挖掘了。
|
||||
|
||||
最后给你留一个问题。在你的项目开发中,是否有测试?你又是使用什么框架来测试的呢?欢迎留言和我交流这个问题,也欢迎你把这篇文章分享给更多的人,一起交流和学习。
|
||||
|
||||
|
||||
199
极客时间专栏/OpenResty从入门到实战/测试篇/29 | 最容易失准的性能测试?你需要压测工具界的“悍马”wrk.md
Normal file
199
极客时间专栏/OpenResty从入门到实战/测试篇/29 | 最容易失准的性能测试?你需要压测工具界的“悍马”wrk.md
Normal file
@@ -0,0 +1,199 @@
|
||||
<audio id="audio" title="29 | 最容易失准的性能测试?你需要压测工具界的“悍马”wrk" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/db/be440b24e1d2edb06c0f5361a7e0fedb.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
在测试章节的最后一节课,我和你来聊聊性能测试。这部分内容并非 OpenResty 独有,对于其他的后端服务来说,都是一样适用的。
|
||||
|
||||
性能测试很常见,在我们交付产品的时候,都会带有性能指标的要求,比如 QPS、TPS 达到多少,延时要低于多少毫秒,可以并发支持多少用户的连接等等。对于开源项目而言,我们发版本之前也会做一次性能测试,和上一个版本对比,看是否有明显的衰退。也有一些中立的网站,会发布同类产品的性能对比数据。不得不说,性能测试离我们真的很近。
|
||||
|
||||
在我的十几年的工作中,针对不同的产品做过很多次性能测试,中间也踩过不少坑。后来,我逐渐地发现,性能测试做起来简单,但做对却并不容易,甚至可以说,很多性能测试的结果都是失准的。
|
||||
|
||||
那么,如何做一个科学严谨的性能测试呢?今天这节课,且听我娓娓道来。
|
||||
|
||||
## 性能测试工具
|
||||
|
||||
工欲善其事,必先利其器。选择一个趁手的性能测试工具,是成功的一半。
|
||||
|
||||
`ab` 这个 Apache Benchmark 工具你应该很熟悉,可以说是最简单的性能测试工具,但可惜的是并不好用。这是因为,当前服务端基本都基于协程和异步 I/O 来开发,性能不差;而 ab 利用不到机器的多核,生成的请求压力不够大。这种情况下,ab 测试得到的结果,并不真实,反而变成了 ab 自身的性能测试。
|
||||
|
||||
所以,我们可以明确选择压测工具的一个标准,那就是:**工具自身的性能非常强悍,可以生成足够大的压力,压垮服务端程序。**
|
||||
|
||||
当然,你也可以有钱任性,启动很多压测客户端,变为分布式压测系统。这自然是可行的,但不要忘记,与此同时的复杂度也跟着上去了。
|
||||
|
||||
回到 OpenResty 的实践,我们推荐使用的性能测试工具是 wrk。先来说说,为什么选择它呢?
|
||||
|
||||
首先, wrk 满足工具选型的标准。单机的 wrk 产生的压力,可以轻松让 Nginx 跑满 CPU,其他服务端程序更是不在话下。
|
||||
|
||||
其次, wrk 和 OpenResty 有很多类似的地方。wrk 也不是从零开始编写的一个开源项目,它站在 LuaJIT 和 Redis 这两个巨人的肩膀上,充分利用了系统的多核资源来生成请求。除此之外,wrk 还暴露了 Lua API,你可以嵌入自己的 Lua 脚本,来自定义请求的头和内容,使用非常灵活。
|
||||
|
||||
那么该如何使用 wrk呢?也很简单,看下面这段代码的内容:
|
||||
|
||||
```
|
||||
wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html
|
||||
|
||||
```
|
||||
|
||||
这意味着 wrk 会使用 12 个线程,保持 400 个长连接,持续 30 秒钟,来给指定的 API 接口发送 HTTP 请求。当然,如果你不指定参数的话,wrk 会默认启动 2 个线程和 10 个长连接。
|
||||
|
||||
## 测试环境
|
||||
|
||||
找好测试工具后,我们还不能直接开始压力测试,还需要把测试环境给检查一遍,测试环境需要检查的主要有四项,下面我分别来详细讲讲。
|
||||
|
||||
### 检查项一:关闭 SELinux
|
||||
|
||||
如果你是 CentOS/RedHat 系列的操作系统,建议你关闭 SELinux,不然可能会遇到不少诡异的权限问题。
|
||||
|
||||
我们通过下面这个命令,查看 SELinux 是否开启:
|
||||
|
||||
```
|
||||
$ sestatus
|
||||
SELinux status: disabled
|
||||
|
||||
```
|
||||
|
||||
如果显示是开启的(enforcing),你可以通过`$ setenforce 0`来临时关闭;同时修改 `/etc/selinux/config` 文件来永久关闭,将 `SELINUX=enforcing` 改为 `SELINUX=disabled`。
|
||||
|
||||
### 检查项二:最大打开文件数
|
||||
|
||||
然后,你需要用下面的命令,查看下当前系统的全局最大打开文件数:
|
||||
|
||||
```
|
||||
$ cat /proc/sys/fs/file-nr
|
||||
3984 0 3255296
|
||||
|
||||
```
|
||||
|
||||
这里的最后一个数字,就是最大打开文件数。如果你的机器中这个数字比较小,那就需要修改 `/etc/sysctl.conf` 文件来增大:
|
||||
|
||||
```
|
||||
fs.file-max = 1020000
|
||||
net.ipv4.ip_conntrack_max = 1020000
|
||||
net.ipv4.netfilter.ip_conntrack_max = 1020000
|
||||
|
||||
```
|
||||
|
||||
修改完以后,还需要重启系统服务来生效:
|
||||
|
||||
```
|
||||
sudo sysctl -p /etc/sysctl.conf
|
||||
|
||||
```
|
||||
|
||||
### 检查项三:进程限制
|
||||
|
||||
除了系统的全局最大打开文件数,一个进程可以打开的文件数也是有限制的,你可以通过命令 `ulimit` 来查看:
|
||||
|
||||
```
|
||||
$ ulimit -n
|
||||
1024
|
||||
|
||||
```
|
||||
|
||||
你会发现,这个值默认是 1024,是一个很低的数值。因为每一个用户请求都会对应着一个文件句柄,而压力测试会产生大量的请求,所以我们需要增大这个数值,把它改为百万级别,你可以用下面的命令来临时修改:
|
||||
|
||||
```
|
||||
$ ulimit -n 1024000
|
||||
|
||||
```
|
||||
|
||||
也可以修改配置文件 `/etc/security/limits.conf` 来永久生效:
|
||||
|
||||
```
|
||||
* hard nofile 1024000
|
||||
* soft nofile 1024000
|
||||
|
||||
```
|
||||
|
||||
### 检查项四:Nginx 配置
|
||||
|
||||
最后,你还需要对 Nginx 的配置,做一个小的修改,也就是下面这两行代码的操作:
|
||||
|
||||
```
|
||||
events {
|
||||
worker_connections 10240;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这样,我们就可以把每个 worker 的连接数增大了。因为它的默认值只有 512,这在大压力的测试下显然是不够的。
|
||||
|
||||
## 压测前检查
|
||||
|
||||
到此为止,测试环境已经准备好了。一定有人蠢蠢欲动想要上手测试了吧?且慢,在使用 wrk 发起测试之前,让我们最后再来检测一次。毕竟,人总会犯错,换个角度来做一次交叉测试,是非常重要的。
|
||||
|
||||
最后的这次检测,可以分为两步。
|
||||
|
||||
### 第一步,使用自动化工具 `c1000k`。
|
||||
|
||||
它来自 SSDB 的作者:[https://github.com/ideawu/c1000k](https://github.com/ideawu/c1000k)。从名字你就能看出来,这个工具的目的,就是用来检测你的环境是否可以满足100万并发连接的要求。
|
||||
|
||||
这个工具的使用也很简单。我们分别启动一个 server 和 client,对应着监听 7000 端口的服务端程序,以及发起压力测试的客户端程序,目的是为了模拟真实环境下的压力测试:
|
||||
|
||||
```
|
||||
./server 7000
|
||||
./client 127.0.0.1 7000
|
||||
|
||||
```
|
||||
|
||||
紧接着,client 会向 server 发送请求,检测当前的系统环境能否支持 100 万并发连接。你可以自己去运行一下,看看结果。
|
||||
|
||||
### 第二步,检测服务端程序是否正常运行。
|
||||
|
||||
如果服务端的程序不正常,那么压力测试可能就成了错误日志刷新测试,或者是 404 响应测试。
|
||||
|
||||
所以,测试环境检测的最后一步,也是最重要的一步,就是**跑一遍服务端的单元测试集,或者手动调用几个主要的接口,来保证 wrk 测试的所有接口、返回的内容和 http 响应码都正常,并且在 `logs/error.log` 中没有出现任何错误级别的信息。**
|
||||
|
||||
## 发送请求
|
||||
|
||||
好了,到现在,万事俱备,只欠东风了。让我们开始用 wrk 来做压力测试吧!
|
||||
|
||||
```
|
||||
$ wrk -d 30 http://127.0.0.2:9080/hello
|
||||
Running 30s test @ http://127.0.0.2:9080/hello
|
||||
2 threads and 10 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 595.39us 178.51us 22.24ms 90.63%
|
||||
Req/Sec 8.33k 642.91 9.46k 59.80%
|
||||
499149 requests in 30.10s, 124.22MB read
|
||||
Requests/sec: 16582.76
|
||||
Transfer/sec: 4.13MB
|
||||
|
||||
```
|
||||
|
||||
这里,我并没有指定参数,所以wrk会默认启动 2 个线程和 10 个长连接。其实,你也并不需要把wrk 的线程数和连接数调整得很大,只要能够让目标程序跑满 CPU 就达到要求了。
|
||||
|
||||
但压测的时间一定不能太短,几秒钟的压测是没有意义的,不然很有可能服务端的程序还没加载完热数据,压测就已经结束了。同时,在压测期间,你需要使用 top 或者 htop 这样的监控工具,来确认服务端目标程序是否跑满 CPU。
|
||||
|
||||
从现象上来看,如果 CPU 满载,而且压测停止后,CPU 和内存占用迅速降低,那么恭喜你,这次压测顺利完成。但如果有下面这样的异常,作为服务端开发的你就得特别留意了。
|
||||
|
||||
- CPU 不能满载。这不会是 wrk 的问题,可能是网络的限制,更可能是你的代码中有阻塞的操作。你可以通过 review 代码来确定,也可以使用 off CPU 火焰图来确定。
|
||||
- CPU 一直满载,即使压测停止仍然如此。这说明在代码中存在热循环,可能是正则表达式引起的,也可能是 LuaJIT 的 bug 引起的,这两点都是我在真实的环境中遇到过的问题。这时,你就需要用 on CPU 火焰图来确定了。
|
||||
|
||||
最后再来一起看下 wrk 的统计结果。关于这个结果,我们一般会关注两个值:
|
||||
|
||||
第一个是 QPS,也就是 `Requests/sec: 16582.76`,这个数据很直接,表示服务端每秒钟处理了多少请求。
|
||||
|
||||
第二个是延时 `Latency 595.39us 178.51us 22.24ms 90.63%`,这个数据和 QPS 一样重要,它体现了系统的响应速度。比如对于网关的应用来讲,我们就希望能够把延时控制在 1 毫秒以内。
|
||||
|
||||
另外, wrk 还提供了 latency 参数,可以把延时的分布百分比详细地打印出来,比如:
|
||||
|
||||
```
|
||||
Latency Distribution
|
||||
50% 134.00us
|
||||
75% 180.00us
|
||||
90% 247.00us
|
||||
99% 552.00us
|
||||
|
||||
```
|
||||
|
||||
不过,wrk 的延时分布数据并不准确,因为它人为地加入了网络和工具的扰动,放大了延时,这一点需要你特别注意。关于wrk Latency Distribution,你可以通过我以前写的[这篇文章](https://mp.weixin.qq.com/s/n8a4wzmf6I8kUc-T47PylA)来了解详细内容。
|
||||
|
||||
## 写在最后
|
||||
|
||||
性能测试是个技术活儿,能做对、做好的人不多。希望今天这节课,能让你对性能测试有一个更全面的认识。
|
||||
|
||||
最后给你留一个作业题:wrk 支持自定义 Lua 脚本来做压力测试,那么,你可以根据它的文档,写一段简单的 Lua 脚本吗?这可能会有一些难度,但完成的同时,你一定能更深刻地理解 wrk 暴露接口的用意。
|
||||
|
||||
欢迎留言写下你的答案和思考,也欢迎你把这篇文章分享给更多的人,我们共同进步。
|
||||
|
||||
|
||||
93
极客时间专栏/OpenResty从入门到实战/测试篇/30 | 答疑(三)如何搭建测试的网络结构?.md
Normal file
93
极客时间专栏/OpenResty从入门到实战/测试篇/30 | 答疑(三)如何搭建测试的网络结构?.md
Normal file
@@ -0,0 +1,93 @@
|
||||
<audio id="audio" title="30 | 答疑(三)如何搭建测试的网络结构?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2e/79/2e02e822fac2bf19a9d4a2edd780b279.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
专栏更新到现在,OpenResty第三版块 OpenResty 测试篇,我们就已经学完了。恭喜你没有掉队,仍然在积极学习和实践操作,并且热情地留下了你的思考。
|
||||
|
||||
很多留言提出的问题很有价值,大部分我都已经在App里回复过,一些手机上不方便回复的或者比较典型、有趣的问题,我专门摘了出来,作为今天的答疑内容,集中回复。另一方面,也是为了保证所有人都不漏掉任何一个重点。
|
||||
|
||||
下面我们来看今天的这 5 个问题。
|
||||
|
||||
## 问题一:如何搭建测试的网络结构?
|
||||
|
||||
Q:跑 wrk 的客户端,是应该放在外网上的机器上,还是和服务端同一局域网内的机器上呢?这两者,哪个更有性能测试意义?
|
||||
|
||||
A:其实,对于测试 web 相关的服务来说,选择正确的测试工具,只能算得上是一个好的开端,如何搭建测试的网络结构,也是后续的重要一环。
|
||||
|
||||
一般来说,我们肯定希望排除所有网络的干扰,单独测试出服务的性能极限来。出于这个目的,我们可以有两种搭建网络的方法来做压测。
|
||||
|
||||
- 第一种方法,把 wrk 和服务端程序都部署在同一台性能比较好的机器上。比如, 我们在 Nginx 中开启 8 个 worker,剩下的几个 CPU 资源分给 wrk。这样一来,就只有本地的网络通信,可以把网络的影响降到最低。
|
||||
- 第二种方法,用专门的路由器搭建一个局域网,把 wrk 所在的机器和服务端所在的机器连在一起。
|
||||
|
||||
之所以不推荐你在已有的网络中直接测试,是因为大部分的网络中都存在交换机和防火墙,它们可能会对大流量的压测进行限制,造成测试结果的不准确。
|
||||
|
||||
另外,关于性能测试工具,我还想再多提几句。性能测试工具可能存在 Coordinated Omission 问题,在分析工具的延时数据的时候,你一定要特别留意。
|
||||
|
||||
简单地说,Coordinated Omission(协调遗漏) 是指,在做压力测试时,对于响应来说,只统计发送和收到回复之间的时间是不够的,这只是服务时间,这样统计会遗漏很多潜在的问题。因此,我们还需要把测试请求的等待时间也计算在内,这个整体才算是用户关心的响应时间。当然,如果你的服务端程序可能会出现阻塞,一定需要考虑这个问题,否则就可以忽略掉了。
|
||||
|
||||
## 问题二:`test::nginx` 可以测试 ssl 相关功能吗?
|
||||
|
||||
Q:ssl相关功能,用`test::nginx`是不是测不了?
|
||||
|
||||
A:事实显然不是这样的,`test::nginx` 可以测试 ssl 的相关功能,你可以参考 [https://github.com/iresty/apisix/blob/master/t/node/ssl.t](https://github.com/iresty/apisix/blob/master/t/node/ssl.t),这个测试案例文件测试了 ssl 证书的全过程。你可以看到,测试案例使用 Lua 代码,来读取本地证书的公钥和私钥;然后,再通过 http API 设置好证书;最后,用 cosocket 来 ssl 握手和访问,验证证书是否生效。
|
||||
|
||||
其实,不仅仅是 ssl 这个功能,只要是 OpenResty 中包含的功能,使用 `test::nginx` 都是可以覆盖的。
|
||||
|
||||
当你不确定某个功能用 `test::nginx`能不能实现时,可以先去 lua-nginx-module 和其他的 OpenResty 开源项目的测试案例集中搜索,一般都能找到对应的示例。我也是用这种方法来解决这类问题的,毕竟,`test::nginx`的可玩性和变化性比较大,总有一些意想不到的使用组合和奇技淫巧在等着你发掘。
|
||||
|
||||
## 问题三:DSL究竟是什么?
|
||||
|
||||
Q:DSL的翻译是领域专用语言吗?文中讲了它是领域小语言,但我搜这个词没有搜到,只搜到了领域专用语言DSL(Domain Specific Language)。
|
||||
|
||||
A:DSL 确实是领域专用语言的缩写,而小语言是 DSL 的俗称。之所以在前面加了一个“小”字,是因为 DSL 的目的和常用的开发语言不同,它不是为了解决通用领域的需求,而是要解决某个领域的需求。最著名的 DSL 就是 SQL,结构化查询语言,用在数据库领域。
|
||||
|
||||
至于`test::nginx`,它其实是为了解决 Nginx 和 OpenResty 的测试需求而创造出来的 DSL。实际上,OpenResty 的作者发明了很多小语言,这种 DSL 的思路,也将会给 OpenResty 社区带来不少新的尝试和解决方案。不过,正如之前文章中提到的一样,DSL 是把双刃剑,能否给最终使用者带来生产力的提升,才是衡量 DSL 是否有价值的主要标准。
|
||||
|
||||
## 问题四:`test::nginx`的安装问题
|
||||
|
||||
Q:在执行完`git clone`后,是否需要执行下面的命令,才能安装`test::nginx`呢?
|
||||
|
||||
```
|
||||
cd test-nginx
|
||||
perl Makefile.PL
|
||||
make
|
||||
sudo make install
|
||||
|
||||
```
|
||||
|
||||
A:事实上并非如此,这里其实你可以参考一些开源项目中 travis 的做法。
|
||||
|
||||
第一步,先通过包管理器安装 [https://github.com/iresty/apisix/blob/master/.travis/linux_runner.sh#L20](https://github.com/iresty/apisix/blob/master/.travis/linux_runner.sh#L20):
|
||||
|
||||
```
|
||||
sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1)
|
||||
|
||||
```
|
||||
|
||||
第二步,`git clone` 最新的 `test::nginx` [https://github.com/iresty/apisix/blob/master/.travis/linux_runner.sh#L35](https://github.com/iresty/apisix/blob/master/.travis/linux_runner.sh#L35):
|
||||
|
||||
```
|
||||
git clone https://github.com/openresty/test-nginx.git test-nginx
|
||||
|
||||
```
|
||||
|
||||
第三步,用 prove 命令的时候,把 test nginx 的目录包含进去:
|
||||
|
||||
```
|
||||
prove -Itest-nginx/lib -r t
|
||||
|
||||
```
|
||||
|
||||
前面我也提到过,OpenResty 以及周边的项目,安装的最佳指南都存在于 travis CI 中,而不是文档中。这一点可能与其他项目的做法不同,主要是因为, OpenResty 自己维护了一些周边项目的 fork 或者特定版本;同时, OpenResty 也是强依赖 travis CI的。所以,你应该按照 travis CI 中构建的方法来使用和测试 OpenResty,才能保证和官方一致。
|
||||
|
||||
## 问题五:ab测试工具到底好不好用?
|
||||
|
||||
Q:我怎么记得春哥在 Google Groups 里,多次提到 ab 是当前最佳测试工具呢?
|
||||
|
||||
A:文章中我也提到过了,单从工具特性来说,ab 并不是一个好的性能测试工具。因为它不能够产生足够大的请求压力,而现在的服务端程序性能却已经非常强悍了。我们在 `test::nginx` 中确实会用到 ab,而不是 wrk,这是因为在 TEST_NGINX_BENCHMARK 模式下,`test::nginx` 会根据 HTTP 协议版本,选择使用 ab 或者 weighttp ,来作为压力测试的工具。
|
||||
|
||||
另外,希望你注意到的是,互联网技术的更新换代非常快,我们身在其中的每个人,都需要及时更新自己的知识和技能数。比如说`test::nginx` 的这个选择,在我看来现在已经需要更新了,而春哥当时可能还不知道 wrk 的存在。当然,也许再过一段时间,会有比 wrk 更好的性能测试工具出现,我们自然也应该抱着积极开放的心态去学习和选择。
|
||||
|
||||
今天主要解答这几个问题。最后,欢迎你继续在留言区写下你的疑问,我会持续不断地解答。希望可以通过交流和答疑,帮你把所学转化为所得。也欢迎你把这篇文章转发出去,我们一起交流、一起进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user