wrk 压力测试

wrk 压力测试

一、安装

1
2
3
4
git clone --depth=1 https://github.com/wg/wrk 
cd wrk
make
ln -sf /root/tools/wrk/wrk /usr/local/bin/wrk

查看下版本以及命令帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ wrk -v
wrk 4.1.0 [epoll] Copyright (C) 2012 Will Glozer
Usage: wrk <options> <url>
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use

-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
--latency Print latency statistics # 加上此选项可输出响应时间的分布情况
--timeout <T> Socket/request timeout # wrk 默认超时时间是1秒,用此参数设置超时时间
-v, --version Print version details

Numeric arguments may include a SI unit (1k, 1M, 1G)
Time arguments may include a time unit (2s, 2m, 2h)

二、使用

1. 先来一个简单的测试

1
wrk -t12 -c100 -d30s http://www.baidu.com

30秒钟结束以后可以看到如下输出:

1
2
3
4
5
6
7
8
9
Running 30s test @ http://www.baidu.com
12 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.58ms 155.96ms 1.66s 90.27% # 可以理解为响应时间, 平均值, 标准偏差, 最大值, 正负一个标准差占比
Req/Sec 203.95 150.79 1.55k 76.91% # 每个线程每秒钟的完成的请求数, 均值...
70847 requests in 30.02s, 1.01GB read # 30秒钟总共完成请求数和读取数据量.
Socket errors: connect 0, read 499, write 0, timeout 4 # 错误统计, 499个读错误, 4个超时
Requests/sec: 2359.99 # 总共平均每秒钟完成 2359 个请求
Transfer/sec: 34.40MB # 每秒钟读取 34.4M 数据

wrk 默认超时时间是1秒,可以使用T参数设置,如设置超时为10s:

1
wrk -t12 -c100 -T10s -d30s http://www.baidu.com

输出响应时间分布:

1
wrk -t12 -c100 -T10s -d30s --latency http://www.baidu.com

2. 结合Lua脚本测试

HTTP请求通常不会这么简单,通常还有POST, Header, body 等,可以结合Lua脚本来测试。

建立一个 post.lua文件:

1
2
3
wrk.method = "POST"
wrk.body = "foo=bar&baz=quux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencode"

然后执行:

1
wrk -t12 -c100 -d10s -T30s --script=post.lua --latency http://www.baidu.com

查看 wrk.lua 的源码,https://github.com/wg/wrk/blob/master/src/wrk.lua, 可以看出有以下属性:

1
2
3
4
5
6
7
8
9
10
local wrk = {
scheme = "http",
host = "localhost",
port = nil,
method = "GET",
path = "/",
headers = {},
body = nil,
thread = nil,
}

wrk 可以在lua脚本里添加下面的Hook函数,你可以想象成生命周期,每个生命周期做的事情都不一样, 但是生命周期是有时间顺序的。我们常用一般是 request 和 delay 周期. 另外还提供了以下几个hook函数:

  • setup 线程初始后支持会调用一次

  • init 每次请求发送之前被调用。可以接受 wrk 命令行的额外参数

  • delay 这个函数返回一个数值,在这次请求执行完以后延迟多长时间执行下一个请求,可以对应 thinking time 的场景

  • request 通过这个函数可以每次请求之前修改本次请求体和Header,我们可以在这里写一些要压力测试的逻辑。

  • response 每次请求返回以后被调用,可以根据响应内容做特殊处理,比如遇到特殊响应停止执行测试,或输出到控制台等等。

    1
    2
    3
    4
    5
    6
    function response(status, headers, body)  
    if status ~= 200 then
    print(body)
    wrk.thread:stop()
    end
    end
  • done 在所有请求执行完以后调用, 一般用于自定义统计结果.

    1
    2
    3
    4
    5
    6
    7
    done = function(summary, latency, requests)  
    io.write("------------------------------\n")
    for _, p in pairs({ 50, 90, 99, 99.999 }) do
    n = latency:percentile(p)
    io.write(string.format("%g%%,%d\n", p, n))
    end
    end

wrk 源码中给出的完整示例 ,源码 (这个目录下其他 脚本也可以参考)

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
local counter = 1  
local threads = {}

function setup(thread)
thread:set("id", counter)
table.insert(threads, thread)
counter = counter + 1
end

function init(args)
requests = 0
responses = 0

local msg = "thread %d created"
print(msg:format(id))
end

function request()
requests = requests + 1
return wrk.request()
end

function response(status, headers, body)
responses = responses + 1
end

function done(summary, latency, requests)
for index, thread in ipairs(threads) do
local id = thread:get("id")
local requests = thread:get("requests")
local responses = thread:get("responses")
local msg = "thread %d made %d requests and got %d responses"
print(msg:format(id, requests, responses))
end
end

以通过 lua 实现访问多个 url.

例如这个复杂的 lua 脚本, 随机读取 paths.txt 文件中的 url 列表, 然后访问:

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
counter = 1  

math.randomseed(os.time())
math.random(); math.random(); math.random()

function file_exists(file)
local f = io.open(file, "rb")
if f then f:close() end
return f ~= nil
end

function shuffle(paths)
local j, k
local n = #paths
for i = 1, n do
j, k = math.random(n), math.random(n)
paths[j], paths[k] = paths[k], paths[j]
end
return paths
end

function non_empty_lines_from(file)
if not file_exists(file) then return {} end
lines = {}
for line in io.lines(file) do
if not (line == '') then
lines[#lines + 1] = line
end
end
return shuffle(lines)
end

paths = non_empty_lines_from("paths.txt")

if #paths <= 0 then
print("multiplepaths: No paths found. You have to create a file paths.txt with one path per line")
os.exit()
end

print("multiplepaths: Found " .. #paths .. " paths")

request = function()
path = paths[counter]
counter = counter + 1
if counter > #paths then
counter = 1
end
return wrk.format(nil, path)
end

关于 cookie

有些时候我们需要模拟一些通过 cookie 传递数据的场景. wrk 并没有特殊支持, 可以通过 wrk.headers["Cookie"]="xxxxx"实现.

下面是在网上找的一个取 Response的cookie作为后续请求的cookie :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getCookie(cookies, name)  
local start = string.find(cookies, name .. "=")

if start == nil then
return nil
end

return string.sub(cookies, start + #name + 1, string.find(cookies, ";", start) - 1)
end

response = function(status, headers, body)
local token = getCookie(headers["Set-Cookie"], "token")

if token ~= nil then
wrk.headers["Cookie"] = "token=" .. token
end
end