压测工具wrk的使用

6,083 阅读11分钟

压测工具wrk的使用

2021-05-23 来深圳第二周

主要写作思路,围绕wrk的作用以及安装使用,使用的一些命令参数的含义,以及使用wrk进行程序的基准测试得出的结果内的具体参数的释义,然后对复杂的一些wrk请求需要使用到的脚本功能的脚本进行一个说明,同时对wrk中为了兼容脚本适应一些复杂测试而提供的一些全局的变量以及一些方法,提供几个经典的案例脚本方案做解释;最后对全文内容进行一个总结,结束本文写作!

1. 简介

官方解释 wrk is a modern HTTP benchmarking tool capable of generating significant load when run on a single multi-core CPU. It combines a multithreaded design with scalable event notification systems such as epoll and kqueue.

wrk是一款针对HTTP的基准测试工具,它结合了多线程以及类似epoll、kqueue的多事件模式,可以在单机多核CPU的条件下构造大量的负载。

2. wrk

wrk是用来做系统qps压测的一个比较好用的软件。目前github的star数已经有29.2k了。

在使用wrk之前,我自己听过或者用过的就是JMeter以及Apache的ab压测工具了,但是那些自己只是跑了几个例子,并没有实际的运用到自己写的系统接口中去。而wrk是用到了系统的接口中去。wrk的优势从我这段时间的一个接口压测来看,主要有以下几点:

  • 轻量级的性能测试工具
  • 安装简单(git clone即可)
  • 学习简单,稍微看看文档就可以直接上手
  • 基于系统自带的高性能I/O机制,利用异步的事件驱动框架,通过很少的线程可以压出跟大的并发量(对于测试宿主机几乎无cpu的压力)

存在优势就一定有wrk的相对应的劣势,wrk的劣势主要是目前支持单机压测,即不支持多机器对目标的压测,即每次压测的机器地址很难去改变,可以改变压测的接口地址,但是压测的机器地址变不了,但对于日常我们的接口性能的验证,wrk完全足够。

3. 安装&使用

wrk的git地址:github.com/wg/wrk

# 克隆到本地
git clone https://github.com/wg/wrk
# 进入wrk
cd wrk
# 编译
make


# =====如果是mac的话,可以直接使用下面的方式进行直接安装======
brew install wrk

wrk查看当前版本:

wrk -v

wrk版本展示

3.1 wrk的使用

wrk --help可以查看具体的wrk命令的帮助

./wrk --help
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 # 加载测试用的lua脚本文件
    -H, --header      <H>  Add header to request # 添加请求头
        --latency          Print latency statistics # 打印延迟相关统计信息
        --timeout     <T>  Socket/request timeout # 套接字或请求当前测试可以接受的延迟
    -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) # 时间需要包含时间单位

3.2 wrk测试报告

采用百度官网(www.baidu.com)作为一个测试用例进行压测

得出的测试报告如下:qps=36.92(我这里测出的qps少主要是租住的房子网速慢的很,看延迟分布可以看出来,耗时过长)

-- 表示采用了10个线程,连接数300,持续时间20s
./wrk -t10 -c300 -d20s --latency http://www.baidu.com
Running 20s test @ http://www.baidu.com
  10 threads and 300 connections
-- 线程数据       平均耗时		标准差			最大耗时		正负标准差比例
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   700.87ms  522.63ms   2.00s    64.24%
    Req/Sec     6.84      6.70    40.00     82.99%
  Latency Distribution -- 延迟分布
     50%  588.30ms
     75%    1.05s
     90%    1.51s
     99%    1.96s
  742 requests in 20.10s, 12.42MB read -- 20.10s发送724个请求
  Socket errors: connect 60, read 0, write 0, timeout 247 # socket错误分布
Requests/sec:     36.92 -- qps
Transfer/sec:    632.61KB -- 每秒传输的数据

标准差类似于方差的存在,如果太大表示数据集不稳定,对应到系统表示系统性能波动较大,这个时候可以对自己接口所处理的数据看看是否有一些异样数据。

3.3 借助lua脚本进行复杂测试

wrk默认是采用GET请求方式进行接口测试,如果需要使用POST请求就需要使用到lua脚本,通过加载编写好的lua脚本来进行定制化的请求。采用-s 或者 --script可以加载脚本文件。

wrk的请求构造过程主要是针对每一个线程的,所以对于压测的环境也提供了一些函数方法来进行支持线程的环境定制化。

启动阶段

function setup(thread) -- 这个方法可以用来对线程进行不同的设置

-- 线程中的方法以及属性
thread:addr --线程地址
thread:get(name) -- 获取name的value对象
thread:set(name,value) -- 设置name的value值
thread:stop() -- 停止当前线程

假如我们需要构造注册请求,但用户名是一个unique,如果我们不设置各个线程的取值范围,在测试过程中,会发现系统会报错很多Duplicate key,因为并发条件下,多个线程拿到的值是一样的,所以需要在setup中定制每个线程的取值。这个在后面的例子中会给出具体的脚本案例

运行阶段

function init(args) -- 初始化
function delay() -- 延迟方法
function request() -- 请求方法构造,这个是脚本编写中最重要的一个函数
function response(status,headers,body) -- 响应处理,如果没有定义wrk不会解析返回的响应

3.3.1 wrk的全局变量

在我们编写lua脚本的时候,如何对所有的请求都保持一些参数的一致呢?这个就依靠wrk的全局变量来实现。

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

注意:wrk的全局变量修改,会影响所有的请求!

3.3.1 wrk支持lua脚本的方法

除了全局变量,wrk还提供了一些方法来支持lua脚本定制化的请求。

function wrk.format(method, path, headers, body)

    wrk.format returns a HTTP request string containing the passed parameters
    merged with values from the wrk table.
    -- 这个表示定义的方法,请求url路径、请求头以及请求体都可以通过format方法构造出一个想要的HTTP请求体(这个是用的最多的)

function wrk.lookup(host, service)

    wrk.lookup returns a table containing all known addresses for the host
    and service pair. This corresponds to the POSIX getaddrinfo() function.
    -- 给定 host 和 service(port/well known service name),返回所有可用的服务器地址信息。

function wrk.connect(addr)

    wrk.connect returns true if the address can be connected to, otherwise
    it returns false. The address must be one returned from wrk.lookup().
    -- 测试给定的服务器地址信息是否可以成功创建连接(如果连接不成功可以不发送请求,可以使用这个方法来判断)

3.4 lua脚本压测实例

3.4.1 单纯的利用POST提交方法

-- 全局设定
wrk.method = "POST"
wrk.body = "name=zhangsan&password=123456"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

或者可以采用以下的方式:

wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
request = function()
	method = "POST"
  body = "name=zhangsan&password=123456"
  path = "/user/login"
  return wrk.format(method,path,nil,body)
end

3.4.2 自定义请求和参数

要求每一次的参数不同如何处理

request = function()
  uid = math.random(1,1000000)
  path = "/login?uid"..uid
  return wrk.formate(nil,path,nil,nil)
end

3.4.3 设置延迟

function delay()
	return 10 -- 表示设置10ms的延迟
end

3.4.4 先登录后请求

这个地方是为了对一些需要先登录获取token,后续的请求都需要携带token请求的接口进行测试的,参照官方给出的lua脚本案例:auth.lua

-- example script that demonstrates response handling and
-- retrieving an authentication token to set on all future
-- requests

token = nil
path  = "/authenticate" -- 初始的请求url

request = function()
   return wrk.format("GET", path) -- 发送第一次authenticate认证请求
end

response = function(status, headers, body)
   if not token and status == 200 then
      token = headers["X-Token"]
      path  = "/resource" -- 拿到之后做修改
      wrk.headers["X-Token"] = token -- 修改头,携带token进入后续的请求
   end
end

3.4.5 pipeline请求

即一次发送多个请求,参照官方给出的pipeline.lua

-- example script demonstrating HTTP pipelining

init = function(args)
   local r = {}
   r[1] = wrk.format(nil, "/?foo")
   r[2] = wrk.format(nil, "/?bar")
   r[3] = wrk.format(nil, "/?baz")

   req = table.concat(r) -- 利用table的concat实现
end

request = function()
   return req
end

3.5 压测线程定制化

在网上的很多博客中,我发现大家都对线程定制化都是只给出了wrk对线程的一些操作的方法,但是并没有给出具体的案例来展示这些方法到底怎么用。

举一个很简单的例子,如果你需要注册此时用户表中username是设置了unique key的,对你的注册请求你肯定是想构造一批不同的用户,然后分别模拟注册:

wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
request = function()
  num = math.random(1,1000000)
	username = "user".. num
  password = "pwd"..num
  body = string.format("username=%s&password=%",username,password)
  path = "/user/register"
  return wrk.format("POST",path,nil,body)
end

大家以为这样就可以了,实际上如果线程数给多一些,你所认为的好像大家都会相安无事的取到随机值,然后分别就注册success,但实际上,可以查看压测结束的日志,会有很多Duplicate key报错,这是因为线程取到了同样的随机值,插入数据库报错了,此时压测的结果实际上是不准确的。

3.5.1 线程取值定制化

为了获取更加准确的值以测试注册接口,官方给出了一个比较好的例子:setup.lua

-- example script that demonstrates use of setup() to pass
-- data to and from the threads

local counter = 1
local threads = {}

function setup(thread)
   thread:set("id", counter) -- 设置线程id
   table.insert(threads, thread) -- 用threads的表格存储
   counter = counter + 1 -- 为避免id相同,使用counter来实现递增
end

function init(args)
  -- 注意init中初始化的值都是针对一个线程而言的!即每个线程都是隔离的
   requests  = 0 -- 初始化,注意这里的初始化的变量都将成为wrk的全局变量,即后续可以直接用
   responses = 0

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

function request()
   requests = requests + 1 -- 构造请求数,因为init中定义了,所以这里可以直接使用
   return wrk.request()
end

function response(status, headers, body)
   responses = responses + 1 -- 获取响应数,因为init中定义了,所以可以直接使用
end

function done(summary, latency, requests)
   for index, thread in ipairs(threads) do
      local id        = thread:get("id") -- 获取线程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

这个例子也比较简单,可以跑一下看看结果:

/wrk -t10 -c300 -d10s --latency -s setup.lua  http://www.baidu.com
thread 1 created
thread 2 created
thread 3 created
thread 4 created
thread 5 created
thread 6 created
thread 7 created
thread 8 created
thread 9 created
thread 10 created
Running 10s test @ http://www.baidu.com
  10 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   771.03ms  571.07ms   1.98s    66.37%
    Req/Sec     6.09      5.60    30.00     81.28%
  Latency Distribution
     50%  649.28ms
     75%    1.10s
     90%    1.72s
     99%    1.96s
  296 requests in 10.09s, 5.40MB read
  Socket errors: connect 60, read 0, write 0, timeout 73
Requests/sec:     29.34
Transfer/sec:    548.04KB
thread 1 made 88 requests and got 59 responses
thread 2 made 75 requests and got 50 responses
thread 3 made 51 requests and got 29 responses
thread 4 made 51 requests and got 31 responses
thread 5 made 71 requests and got 47 responses
thread 6 made 38 requests and got 22 responses
thread 7 made 20 requests and got 8 responses
thread 8 made 33 requests and got 20 responses
thread 9 made 37 requests and got 24 responses
thread 10 made 16 requests and got 6 responses

上面的结果发现线程隔离了。实现了线程隔离,也就是我们知道了线程隔离是依靠function setup()这个方法实现的。

3.5.2

所以在这个基础之上,我们可以对我们的注册脚本的逻辑进行修改。

-- register.lua脚本
local counter = 1
local threads = {}
-- 设置thread
function setup(thread)
	thread:set("id",counter)
  thread:set("num",(counter-1)*1000000) -- num在后续的操作中可以直接使用,因为已经设置了
  table.insert(threads,thread)
  counter = counter + 1
end

wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

-- request
function request()
  num = num + 1 -- num可以直接使用,因为每个线程都是不同的
  username = "user-"..num
  password = "pwd-"..num
  path = "/user/register"
  body = string.format("username=%s&password=%s",username,password)
  return wrk.format("POST",path,nil,body)
end

上面脚本的修改可以实现每个线程取到不同区间的一个值,具体可以看(counter-1)*1000000,然后每个线程内部都是自增的,可以保证一定不会取到相同的值。

4. 总结

本文主要是对性能压测工具wrk进行了简单的介绍,对wrk的优点和缺点分别进行了介绍,以及其对应的安装和使用方法,使用的命令参数以及结果参数进行了说明;同时对其支持的lua脚本定制化的复杂请求也进行了案例的说明和讲解,最后利用wrk给出的一些官方案例,拓展性的给出了一些定制化的请求lua脚本。

希望本文可以给在wrk路上前行的朋友一些帮助,也可以让还未了解过wrk的朋友一些知识拓展!加油,打工人。

5. 参考文档

Keep thinking, keep coding! 2021年05月23日10:50:38写于深圳