wrk是一款开源的高性能http压测工具(也支持https),很是小巧,能够执行文件只有3M(其中主要是luajit和openssl占用绝大多数空间),别看核心代码3-5年没更新了,但依旧很是好用。虽然很早以前我就知道有这么个工具了,当时学习这个工具的时候我还拿它压测了咱们的我的网站xindoo.me,发现mysql性能不行后加了wp-cache,经过cache把我网站的承载能力提高了10多倍。但当时以前简单使用它的初级功能,最近工做中刚好有个http服务须要压测,而后就拿wrk作了。此次使用了wrk lua高级功能实现了压测,咱们找到了咱们服务的瓶颈,同时也被wrk的超高性能所震惊。
如上图,我用单机(40 cores)压90台机器的集群,压到了31w的QPS,最后压不上去不是由于这台机器抗不住了,而是由于咱们服务扛不住了。一个有复杂业务逻辑的服务和一个毫无逻辑的压测相比有失公允,但在压测过程当中我也干垮了4台机器的nginx集群(这里nginx也只是个方向代理而已),这足见wrk性能之高。依赖lua脚本,wrk也能够完成复杂http请求的压测,接下来跟我一块儿了解下wrk的具体使用吧。mysql
wrk的一切内容都在githubhttps://github.com/wg/wrk上,不像其余各类流行的工具包包同样,它并无提供各个平台的可执行包,只有在mac上能够经过brew安装(应该也不是做者提供的)。好在编译wrk并不难,也不须要什么特殊的配置,git clone https://github.com/wg/wrk.git 或从github上直接下载zip包,进入项目目录后直接执行make,你就能够获得一个可执行文件wrk 。nginx
Options:
-c, --connections Connections to keep open # 指定创建多少个网络连接,全部线程复用这些连接
-d, --duration Duration of test # 指定总共起多少个线程
-t, --threads Number of threads to use # 压测持续多长时间
-s, --script Load Lua script file # 指定lua脚本文件,后文会详细介绍
-H, --header Add header to request # 指定http请求的header头
--latency Print latency statistics
--timeout Socket/request timeout
-v, --version Print version details # 输出版本号,经我测试其实是用不了的
wrk这个命令提供的参数也很少,运用这些参数能够一行命令完成一个简单http请求的压测,咱们以国民检测网络状况最经常使用的一个网站为例。git
> ./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才能正常调用。github
wrk封装了一个http请求的结构,他是经过wrk这个结构体中的内容去完成一次http请求的,因此你想让http请求不一样只须要修改这里面的内容便可,wrk提供了让你修改内容的方法。注意:wrk每一个线程都是单独的lua运行环境,互不干扰,没有交集。若是你想在多线程共享一些数据的话,你能够用table这个全局变量来共享。web
wrk = {
scheme = "http",
host = "localhost",
port = nil,
method = "GET",
path = "/",
headers = {},
body = nil,
thread = ,
}
除了上述结构体外,wrk容许你重写有些给的的function来实现你请求的自定义,如下是其方法名和调用时机。redis
global setup -- 线程启动前调用一次
global init -- 线程启动后调用一次
global delay -- 每次发起一个请求都会调用
global request -- 每发起一个请求前都会调用
global response -- 获取到请求响应结果后调用
global done -- 压测结束后会调用一次
每一个方法都是可选的, 若是你想重定义某个阶段的行为,你能够选择重写该方法,具体方法介绍以下。sql
setup
function setup(thread)是有参数传入的,传入的内容就是当前的线程,setup是在ip地址解析后而且全部线程初始化后,但没用启动前执行的,因此这个时候你能够对thread的构造作一些自定义。shell
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