背景

最近手里有个活需要做压力测试,压力测试已经做过很多了,由于是自己做,也不需要发什么正规的测试报告,简单起见会用shell 脚本结合管道和 curl 自行构建请求发压力,这样可以利用线上数据快速构建大量接近真实的请求,也可以自定义并发度和请求数,方便快捷,参见之前的“并行执行任务的脚本”一文。

而这次是由 QA 来做,在压测工具上遇到了些问题,主要是请求构造方面,我们的场景下是将图片 base64 encode 成文本然后发送 POST 请求,请求的 body 比较大,QA 所使用的 JMeter 似乎在 body size 上会有问题导致卡死,总之花了一天时间问题没有解决。于是考虑帮他们处理一下压测工具的问题。

之前用过 ab,wrk,ab 过于简单且存在压力压不上去的问题,不合适;wrk 性能强大,可以定制请求,可以一试;另外看看到了 siege,看介绍说是生成一个 urls.txt就可以满足需求,准备尝试一下(但最终实践证明,siege也不是那么好使,至少按要求生成了 url POST body格式的文件(注意我们的 body 很大),siege 并没有正常执行,试了几次直接放弃了)。最终决定还是用 wrk,之前项目中 wrk 模拟百万提交的能力令人印象深刻,加之有脚本定制功能,满足需求肯定是没问题的。

wrk 相关

wrk(https://github.com/wg/wrk.git)介绍很多,这里不重复了,特点就是性能强,能用较少的资源就可以发出强大的压力,支持定制请求。CentOs 下安装比较容易:

yum install openssl-devel git -y
git clone https://github.com/wg/wrk.git wrk
cd wrk && make
# 把生成的 wrk 拷贝的PATH目录,例如
cp wrk /usr/local/bin

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   
    -s, --script      <S>  Load Lua script file
    -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)

选项比较简单,-t 可以指定并发线程数,-d 可以指定压力持续时间,然后就是我们需要关注的定制部分了,-s,使用 lua 脚本定制发送的请求。

wrk 压测脚本有3个生命周期:

  1. 启动阶段:在脚本文件中实现 setup 方法,wrk 就会在测试线程初始化后但未启动时调用该方法,wrk 会为每一个线程调用一次 setup 方法,并传入代表测试线程的对象 thread 作为参数,在方法中可以操作 thread获取/存储信息,甚至关闭该线程
function setup(thread)
    # thread 提供了1个属性,3个方法
    #   thread.addr             设置请求的ip
    #   thread.get(name)        获取线程全局变量
    #   thread.set(name, value) 设置线程全局变量
    #   thread.stop()           终止线程
end
  1. 运行阶段:如下,该阶段包括四种方法
function init(args)
    # 每个线程在进入运行阶段后仅调用一次,args 用于获取命令行中传入的参数
    # 可以在这个阶段加载一些数据等等
end

function delay()
    # 每次请求调用1次,发送下一个请求之前的延迟,单位为 ms
    # 例如需要随机间隔请求时,可以使用该函数
end

function request()
    # 每次调用请求1次,返回具体的 http 请求,可以使用 wrk.format 函数生成请求,见下文介绍
    # 我们的场景下需要定制每个 post 请求的 body,就需要利用这个函数
    # 注意不要在这个请求中做耗时的操作,数据的加载可以放到 init 里去做,在这里直接读结果
end

function response(status, headers, body)
    # 每次调用请求1次,返回 http 响应
    # 为提升性能,如果没有定义该方法,wrk 不会解析响应的 headers 和 body
end
  1. 结束阶段:整个测试过程中只调用一次,获取压测结果,生成定制化的报告,报告中的参数如下
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
    #     }
    # }
end

整个生命周期总结可以见下图:

3C8FBB2B-AD66-44A1-A6AB-FB41AA1E8BC5.png

如下是在 wrk 线程各阶段可以使用的变量,例如,对于我们的场景,每个请求POST 的数据都是不一样的,所以需要在运行阶段中,利用 wrk.format 来生成每一次的具体请求。

# 定义请求的主要字段,例如可以用 wrk.method="POST" 将所有请求改为 POST 请求
wrk = {
    scheme  = "http",
    host    = "localhost",
    port    = nil,
    method  = "GET",
    path    = "/",
    headers = {},
    body    = nil,
    thread  = <userdata>,
}

# method: http方法, 如GET/POST/DELETE 等
# path:   url的路径, 如 /index, /index?a=b&c=d
# headers: 一个header的table
# body:    一个http body, 字符串类型
function wrk.format(method, path, headers, body)

# 获取域名的IP和端口,返回table,例如:返回 `{127.0.0.1:80}`
# host:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
# service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
function wrk.lookup(host, service)

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

问题解决

熟悉了 wrk 的工作情况,我们的问题就好处理了。实际需求也很简单,就是有一批图片,需要发压力的时候随机挑选图片,然后按指定压力 post 出去即可。发压力可以使用命令wrk -t N -d M --latency --timeout=5s "http://ip:port/path",具体定制请求,就需要使用到-s 参数,编写 lua 脚本。

我们的POST 参数格式是id=***&content=***,其中 content 就是图片的 base64编码。因此,我们可以先将图片按该格式预处理好,存成文本文件,然后lua 脚本直接读文件构造请求即可。假设我们的图片已经按照该格式存储在./data目录下,一个图片对应一个文本,文件名就是从1到 N 的数字

那么自定义脚本 post. lua 如下:

# 指定请求方法都为 POST,MIME 类型为简单的 url-encode
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

# 定义读取 POST body 内容的函数,按上文所述,POST 的 body 已预处理好并且分别存在单独的文件中
function get_body(path)
    local file, errorMessage = io.open(path, "r")
    if not file then
        error("Could not read file: "..errorMessage.."\n")
    end
    local content = file:read "*all"
    file:close()
    return content
end

# wrk 运行阶段初期,先加载所有的 body 内容到一个 table 中,运行时直接随机读取
init = function (args)
    body_table={}
    for i = 1, 1322 do
        body_table[i]=get_body("./data/"..i);
    end
    math.randomseed(tostring(os.time()):reverse():sub(1, 7))
end

# wrk 运行阶段,随机选取 POST body,构造具体发送的请求
request = function ()
    local n = math.random(1322)
    return wrk.format(nil, nil, nil, body_table[n])
end

这样,我们就可以使用命令wrk -t N -d M -s post.lua --latency --timeout=5s "http://ip:port/path"发送随机图片的压力了,其中 N 是并发线程数,M 是持续的时间,比如30s。

输出报告如下:

BDC0F8ED-D52A-48A6-807E-C1BA4A69572D.png