(1). Wrk是什么?

wrk是一款简单的HTTP压测工具,它能用很少的线程压出很大的并发量.原因是它使用了一些操作系统特定的高性能io机制,比如:select/epoll/kqueue等.

(2). wrk参数简介

lixin-macbook:~ lixin$ wrk
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(压测机器开启多少个线程去做压测,建议是:CPU核心数)

    -s, --script      <S>  Load Lua script file(lua脚本)
    -H, --header      <H>  Add header to request(添加协议头信息)
        --latency          Print latency statistics
        --timeout     <T>  Socket/request timeout(设置timeout)
    -v, --version          Print version details(打印详细信息)

  Numeric arguments may include a SI unit (1k, 1M, 1G)
  # s 秒
  # m 分钟
  # h 小时
  Time arguments may include a time unit (2s, 2m, 2h)

(3). 压测(/hello请求)

lixin-macbook:~ lixin$ wrk -c200 -d30s -t8 http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
  # 8个线程,200个请求叠加
  8 threads and 200 connections
  
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    29.02ms   31.71ms 310.96ms   88.78%
    Req/Sec     1.12k   322.68     3.28k    76.53%
	
	Latency Distribution
	     50%  131.15ms
	     75%  205.00ms
	     90%  304.25ms
	     99%  544.24ms
	
   # 30秒执行了265017个请求,读取了32.4M的数据
  265017 requests in 30.08s, 32.40MB read

# 服务器每秒处理请求数(也就是:QPS)
Requests/sec:   8811.21
# 每少读取了1.08M的数据.
Transfer/sec:      1.08MB

(4). wrk生命周期

wrk中执行http请求的时候,调用lua分为3个阶段:setup/running/done,每个wrk线程中都有独立的脚本环境. 图片摘抄于:“舒润(博客园)”

"Wrk生命周期"

wrk全局属性

wrk = {
  scheme  = "http",
  host    = "localhost",
  port    = nil,
  method  = "GET",
  path    = "/",
  headers = {},
  body    = nil,
  thread  = <userdata>,
}

wrk的全局方法

-- 生成整个request的string,例如:返回
-- GET / HTTP/1.1
-- Host: tool.lu
function wrk.format(method, path, headers, body)

-- 获取域名的IP和端口,返回table,例如:返回 `{127.0.0.1:80}`
function wrk.lookup(host, service)

-- 判断addr是否能连接,例如:`127.0.0.1:80`,返回 true 或 false
function wrk.connect(addr)

Setup阶段(setup是在线程创建之后,启动之前)

function setup(thread)

-- thread提供了1个属性,3个方法
-- thread.addr 设置请求需要打到的ip
-- thread:get(name) 获取线程全局变量
-- thread:set(name, value) 设置线程全局变量
-- thread:stop() 终止线程

Running阶段

function init(args)
-- 每个线程仅调用1次,args 用于获取命令行中传入的参数, 例如 --env=pre

function delay()
-- 每个线程调用多次,发送下一个请求之前的延迟, 单位为ms

function request()
-- 每个线程调用多次,返回http请求

function response(status, headers, body)
-- 每个线程调用多次,返回http响应

Done阶段(可以用于自定义结果报表,整个过程中只执行一次)

function done(summary, latency, requests)


latency.min              -- minimum value seen
latency.max              -- maximum value seen
latency.mean             -- average value seen
latency.stdev            -- standard deviation
latency:percentile(99.0) -- 99th percentile value
latency(i)               -- raw value and count

summary = {
  duration = N,  -- run duration in microseconds
  requests = N,  -- total completed requests
  bytes    = N,  -- total bytes received
  errors   = {
    connect = N, -- total socket connection errors
    read    = N, -- total socket read errors
    write   = N, -- total socket write errors
    status  = N, -- total HTTP status codes > 399
    timeout = N  -- total request timeouts
  }
}

(5). wrk使用lua简单案例

wrk.method = "POST"
wrk.body = "{\"name\" : \"lixin\",\"age\" : 25 }" -- 直接写死,如果不需要请求数据的差异化
wrk.headers["Content-Type"] = "application/json"

response = function(status, headers, body)
  if status ~= 200 then
    print(body)
    wrk.thread:stop()
  end
end

(6). wrk使用lua复杂案例

读取本地文件,发送请求.

-- save-user-request.txt
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"

-- 请求体内容
local bodys = {}

-- 读取json文件,保存结果到:bodys数组里.
-- init仅每个线程启动一次,不知道为什么不设计成:所有的线程共用一个.
-- 当然,这样设计的目的比较简单.
function init(args)
  local i = 1;
  for line in io.lines("users.json") do
    bodys[i] = line;
    i = i + 1;
  end
end

-- 每个线程发起请求前之前,都执行一次.
-- #bodys : 代表获得数组的长度 
local i = 1;
function request()
   local body = wrk.format(nil, nil, nil, bodys[i % #bodys + 1])
   i = i + 1
   return body
end

response = function(status, headers, body)
  if status ~= 200 then
    print(body)
    wrk.thread:stop()
  end
end
-- users.json
{ "name" : "lixin-1", "age" : 20 }
{ "name" : "lixin-2", "age" : 21 }
{ "name" : "lixin-3", "age" : 22 }
{ "name" : "lixin-4", "age" : 23 }
{ "name" : "lixin-5", "age" : 24 }
{ "name" : "lixin-6", "age" : 25 }
{ "name" : "lixin-7", "age" : 26 }
{ "name" : "lixin-8", "age" : 27 }
{ "name" : "lixin-9", "age" : 28 }
// http://localhost:8080/save
@RestController
public class HelloController {
	@PostMapping("/save")
	public User save(@RequestBody User user) {
		user.setAge(user.getAge() + 1);
		return user;
	}
}

class User implements Serializable {
	private static final long serialVersionUID = 5114311963929663694L;
	private String name;
	private int age;
    // ... setting/getting
}

(7). 总结

相比ab压测,wrk能应对比较复杂的场景,唯一不足:缺少编排功能.