Web服务压测神器wrk

wrk是一款开源的高性能http压测工具(也支持https),非常小巧,可以执行文件只有3M(其中主要是luajit和openssl占用绝大多数空间),别看核心代码3-5年没更新了,但依旧非常好用。虽然很早之前我就知道有这么个工具了,当时学习这个工具的时候我还拿它压测了我们的个人网站xindoo.me,发现mysql性能不行后加了wp-cache,通过cache把我网站的承载能力提升了10多倍。但当时之前简单使用它的初级功能,最近工作中恰好有个http服务需要压测,然后就拿wrk做了。这次使用了wrk lua高级功能实现了压测,我们找到了我们服务的瓶颈,同时也被wrk的超高性能所震惊。
Web服务压测神器wrk
如上图,我用单机(40 cores)压90台机器的集群,压到了31w的QPS,最后压不上去不是因为这台机器抗不住了,而是因为我们服务扛不住了。一个有复杂业务逻辑的服务和一个毫无逻辑的压测相比有失公允,但在压测过程中我也干垮了4台机器的nginx集群(这里nginx也只是个方向代理而已),这足见wrk性能之高。依赖lua脚本,wrk也可以完成复杂http请求的压测,接下来跟我一起了解下wrk的具体使用吧。

wrk的一切内容都在githubhttps://github.com/wg/wrk上,不像其他各种流行的工具包包一样,它并没有提供各个平台的可执行包,只有在mac上可以通过brew安装(应该也不是作者提供的)。好在编译wrk并不难,也不需要什么特殊的配置,git clone https://github.com/wg/wrk.git 或从github上直接下载zip包,进入项目目录后直接执行make,你就可以得到一个可执行文件wrk 。

  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      # 指定http请求的header头
        --latency          Print latency statistics
        --timeout     <T>  Socket/request timeout
    -v, --version          Print version details      # 输出版本号,经我测试实际上是用不了的

wrk这个命令提供的参数也不多,运用这些参数可以一行命令完成一个简单http请求的压测,我们以国民检测网络情况最常用的一个网站为例。

> ./wrk https://www.baidu.com -c100 -t10 -d100s
Running 20s test @ https://www.baidu.com
  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   145.48ms   91.46ms   1.24s    93.71%
    Req/Sec    71.11     16.91   144.00     66.95%
  14161 requests in 20.09s, 211.91MB read
  Socket errors: connect 0, read 137, write 0, timeout 0
Requests/sec:    705.00
Transfer/sec:     10.55MB

通过一行shell命令就可以轻而易举完成对百度首页的压测,但如果你需要压一些复杂的http请求时,指定这些参数明显做不到,这时候就需要wrk的高级功能,通过-s指定lua脚本。 当然lua脚本也不是随便写了就能用的,需要按wrk的规范去写wrk才能正常调用。

wrk封装了一个http请求的结构,他是通过wrk这个结构体中的内容去完成一次http请求的,所以你想让http请求不同只需要修改这里面的内容即可,wrk提供了让你修改内容的方法。注意:wrk每个线程都是单独的lua运行环境,互不干扰,没有交集。如果你想在多线程共享一些数据的话,你可以用table这个全局变量来共享。

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

除了上述结构体外,wrk允许你重写有些给的的function来实现你请求的自定义,以下是其方法名和调用时机。

    global setup    -- 线程启动前调用一次 
    global init     -- 线程启动后调用一次 
    global delay    -- 每次发起一个请求都会调用
    global request  -- 每发起一个请求前都会调用
    global response -- 获取到请求响应结果后调用
    global done     -- 压测结束后会调用一次  

每个方法都是可选的, 如果你想重定义某个阶段的行为,你可以选择重写该方法,具体方法介绍如下。

setup

function setup(thread)是有参数传入的,传入的内容就是当前的线程,setup是在ip地址解析后并且所有线程初始化后,但没用启动前执行的,所以这个时候你可以对thread的构造做一些自定义。

    thread.addr             - 设置当前线程压测的ip,可以指定线程只压测某个ip
    thread:get(key)        - 读取线程中某个key对应的值,后面可以用key-value执行不同的逻辑  
    thread:set(key, value) - 在线程环境中设置一个KV
    thread:stop()           - 停掉线程,只能在线程还在运行的情况下调用 

init

function init(args)是在线程启动后调用,这里是可以传参数的,在启动命令后加-- arg1 arg2,你就可以在init里通过args[1], args[2]获取到arg1和arg2,举例如下。

> ./wrk https://www.baidu.com -c100 -t10 -d100s -- 10 20 

function init(args)
    print(args[1])  -- 输出10
    print(args[2])  -- 输出20
end 

所以这里可以通过这种方式定义更多的自定义参数,然后通过init(args)做解析,后续可以实现多的功能。

delay

function delay()就很简单了,它是为了让你去控制请求发送的之间间隔,如果你想隔10ms发送一次请求,直接return 10就行了,通过delay()可以实现qps大小的控制。

request()

function request()主要功能是为了定制每次请求的参数数据,如果你想构造一些复杂的请求,request()是不得不改的,你可以再request()中修改上文wrk 结构体中的所有值,基本上最长改动的就是wrk.header, wrk.path, wrk.body。这里需要注意,request()是要求有返回值的,其返回值是wrk.format(method, path, headers, body),wrk.format会将这些参数构造成一个http请求可用的请求数据。

response

function response(status, headers, body)是在每次wrk收到http请求响应后调用,wrk会将请求响应中的http status、headers和body作为参数传递进来,你可以通过这些参数信息做响应统计、调整压测流量、甚至停止压测……等比较自动化的操作。

done

function done(summary, latency, requests)是在压测结束后wrk会调用一次,即便有多个线程也只调用一次。wrk会将压测过程中的统计信息通过参数传递给你,你可以挑其中有用的部分输出。也可以输出你在response()中自行统计的内容。
wrk已经为你提供了以下的统计信息:

  latency.min              -- 最小延迟
  latency.max              -- 最大延迟
  latency.mean             -- 平均延迟
  latency.stdev            -- 延迟的标准差
  latency:percentile(99.0) -- 99分位的延迟
  latency(i)               -- raw value and count

  summary = {
    duration = N,  -- 运行的时间ms
    requests = N,  -- 总请求数
    bytes    = N,  -- 总过收到的字节数
    errors   = {
      connect = N, -- 链接错误数
      read    = N, -- socket数据读取出错数量
      write   = N, -- socket数据写入出错数量 
      status  = N, -- http code 大于399的数量  
      timeout = N  -- 超时请求的总数量 
    }
  }

流量控制方法

wrk使用了多路复用的技术。多路复用使得用一个线程可以异步发起很多个请求,所以不太好用线程数来控制请求数。但一个http连接同时只能处理一个请求,所以可以按一次请求的latency估算出一个连接可以承载的qps数,调整连接数即可控制压测请求大小qps = 1000/latency * Connectnum。 这里需要注意的是单个线程只能占用一个cpu核心,当cpu到瓶颈时也可能压不上去,需要调整线程数。

另外一个方法,把连接数设置的非常大,让连接数不再是发压的瓶颈,然后调整脚本中的delayTime和线程数,可以精确控制qps。 qps = 1000/delayTime * threadnum

总结

在实际压测过程中,我曾用一个线程压出过几十万qps,也好奇过为什么一个线程能压出这么高的qps。我们每次请求需要5ms,所以按道理一个线程只能压出200qps,那实际上几百倍的差异是如何来的?后来大致了解到wrk的作者使用了多路复用的技术(epoll,kqueue),每次请求后并不是阻塞等在在那里,而且异步等待结果,同时也可以发起下一个请求,这和redis很像吧,其实wrk的作者代码都是抄的redis的,哈哈。

所以这里要注意-c和-t连接数和参数的设置,一个线程只能占用一个cpu核,如果还没到cpu的瓶颈,决定qps的是连接处和瓶颈响应时间,举个例子,如果只有一个线程,链接数10,平均响应时间10ms,那么一个链接一秒能过100个请求,所以总共能压出1000qps。当cpu到瓶颈后,不管怎么去调大连接数qps都不会上去,这个时候就需要考虑调大线程数了,利用多核心的资源提升qps。

最后附上我们压测中实际使用的lua脚本,结构也比较简单,大家可以大致参考下。

local list = {}
local delaytime = 0             -- 默认delay是0ms
local filename = "reqdata.txt"  -- 默认请求数据文件

setup = function(thread)
    for k,v in  pairs(wrk.addrs)
    do
        print(v)
    end
end

init = function(args)
    if (args[1] ~= nil) then 
        delaytime = args[1]          -- 启动命令中可以指定延迟时间,如未指定,使用默认文件
    end 
    if (args[2] ~= nil) then      
        filename = args[2]           -- 启动命令中可以指定请求文件目录,如未指定,使用默认文件
    end
    math.randomseed(os.time())
    local i = 0
    for line in io.lines(filename)   -- 把请求包体读入后写到list里,方便后续使用  
    do
        list[i] = line
        i = i+1
    end
end

request = function()
    wrk.body = list[math.random(0, #list)]    -- 随机使用一个包体  
    wrk.method = "POST"
    wrk.scheme = "http"
    wrk.path = "/appstore/uploadLogSDK"
    wrk.headers["Content-Type"]="application/x-www-form-urlencoded"
    return wrk.format()
end

delay = function()
    return delaytime
end

response = function(status, headers, body)      --这里我没做特殊统计,只是在调试过程中输出了一些内容
    --print(status)
    --print(body)
    --print(wrk.format(wrk.method, wrk.path, wrk.headers, wrk.body))
    --wrk.thread:stop()
end

done = function(summary, latency, requests)
    print("99 latency:"..latency:percentile(99.0))  -- 这里我只是额外输出了99分位的延时,貌似数据不太对  
end
上一篇:httpd2.4+阿里云免费ssl证书配置


下一篇:01.MySql连接错误:Cannot get hostname for your address