如何给http接口或页面作简单的压力测试

测试先行是软件系统质量保证的有效手段. 在单元测试方面, 我们有非常成熟的 xUnit 方案. 在集成测试方面, 我们 selenium 等自动化方案. 在性能测试方面也有很多成熟的工具, 比如 LoadRunner, Jmeter 等. 但是很多工具都是给专门的性能测试人员使用的, 功能虽然强大, 但是安装和操作不太方便. 作为开发人员, 我们有些时候想快速验证我们的解决方案是不是存在性能问题, 或者在并发情况下是否有意想不到的问题. 安装 LoadRunner 这样工具, 录制脚本很麻烦, 用起来就像在用大炮打蚊子.

wrk 是一个比较先进的 HTTP 压力测试工具。wrk负载测试时可以运行在一个或者多核CPU,wrk结合了可伸缩的事件通知系统epoll和kqueue等多线程设计思想。目前wrk可以安装在Linux系统和Mac系统。只有一个命令行, 就能做很多基本的 http 性能测试.

wrk 的开源的, 代码在 github 上. https://github.com/wg/wrk

首先要说的一点是: wrk 只能运行在 Unix 类的系统上. 比如 linux, mac, solaris 等. 也只能在这些系统上编译.

这里不得不说一下, 为什么很多人说 mac 是最好的开发环境. 不是因为使用 mac 逼格有多高. 而是你可以同时得到 windows 和 linux 的好处. 多数 linux 下的开发工具都可以在 mac 上使用. 很多都是预编译好的, 有些只需要编译一下就能用.

wrk 的一个很好的特性就是能用很少的线程压出很大的并发量. 原因是它使用了一些操作系统特定的高性能 io 机制, 比如 select, epoll, kqueue 等. 其实它是复用了 redis 的 ae 异步事件驱动框架. 确切的说 ae 事件驱动框架并不是 redis 发明的, 它来至于 Tcl的解释器 jim, 这个小巧高效的框架, 因为被 redis 采用而更多的被大家所熟知.

要用 wrk, 首先要编译 wrk.

你的机器上需要已经安装了 git 和基本的c编译环境. wrk 本身是用 c 写的. 代码很少. 并且没有使用很多第三方库. 所以编译基本不会遇到什么问题.

1. git clone https://github.com/wg/wrk.git

2. cd wrk

3. make

就 ok了.

make 成功以后在目录下有一个 wrk 文件. 就是它了. 你可以把这个文件复制到其他目录, 比如 bin 目录. 或者就这个目录下执行.

如果编译过程中出现:

1. src/wrk.h:11:25: fatal error: openssl/ssl.h: No such file or directory

2. #include

是因为系统中没有安装openssl的库.

sudo apt-get install libssl-dev

sudo yum install openssl-devel

我们先来做一个简单的性能测试:

1. wrk -t12 -c100 -d30s http://www.baidu.com

30秒钟结束以后可以看到如下输出:

1. Running 30s test @ http://www.baidu.com

2. 12 threads and 100 connections

3. Thread Stats Avg Stdev Max +/- Stdev

4. Latency 538.64ms 368.66ms 1.99s 77.33%

5. Req/Sec 15.62 10.28 80.00 75.35%

6. 5073 requests in 30.09s, 75.28MB read

7. Socket errors: connect 0, read 5, write 0, timeout 64

8. Requests/sec: 168.59

9. Transfer/sec: 2.50MB

先解释一下输出:

12 threads and 100 connections

这个能看懂英文的都知道啥意思: 用12个线程模拟100个连接.

对应的参数 -t 和 -c 可以控制这两个参数.

一般线程数不宜过多. 核数的2到4倍足够了. 多了反而因为线程切换过多造成效率降低. 因为 wrk 不是使用每个连接一个线程的模型, 而是通过异步网络 io 提升并发量. 所以网络通信不会阻塞线程执行. 这也是 wrk 可以用很少的线程模拟大量网路连接的原因. 而现在很多性能工具并没有采用这种方式, 而是采用提高线程数来实现高并发. 所以并发量一旦设的很高, 测试机自身压力就很大. 测试效果反而下降.

下面是线程统计:

1. Thread Stats Avg Stdev Max +/- Stdev

2. Latency 538.64ms 368.66ms 1.99s 77.33%

3. Req/Sec 15.62 10.28 80.00 75.35%

Latency: 可以理解为响应时间, 有平均值, 标准偏差, 最大值, 正负一个标准差占比.

Req/Sec: 每个线程每秒钟的完成的请求数, 同样有平均值, 标准偏差, 最大值, 正负一个标准差占比.

一般我们来说我们主要关注平均值和最大值. 标准差如果太大说明样本本身离散程度比较高. 有可能系统性能波动很大.

接下来:

1. 5073 requests in 30.09s, 75.28MB read

2. Socket errors: connect 0, read 5, write 0, timeout 64

3. Requests/sec: 168.59

4. Transfer/sec: 2.50MB

30秒钟总共完成请求数和读取数据量.

然后是错误统计, 上面的统计可以看到, 5个读错误, 64个超时.

然后是所以线程总共平均每秒钟完成168个请求. 每秒钟读取2.5兆数据量.

可以看到, 相对于专业性能测试工具. wrk 的统计信息是非常简单的. 但是这些信息基本上足够我们判断系统是否有问题了.

wrk 默认超时时间是1秒. 这个有点短. 我一般设置为30秒. 这个看上去合理一点.

如果这样执行命令:

1. /wrk -t12 -c100 -d30s -T30s http://www.baidu.com

可以看到超时数就**降低了, Socket errors 那行没有了:

1. Running 30s test @ http://www.baidu.com

2. 12 threads and 100 connections

3. Thread Stats Avg Stdev Max +/- Stdev

4. Latency 1.16s 1.61s 14.42s 86.52%

5. Req/Sec 22.59 19.31 108.00 70.98%

6. 4534 requests in 30.10s, 67.25MB read

7. Requests/sec: 150.61

8. Transfer/sec: 2.23MB

通过 -d 可以设置测试的持续时间. 一般只要不是太短都是可以的. 看你自己的忍耐程度了.

时间越长样本越准确. 如果想测试系统的持续抗压能力, 采用 loadrunner 这样的专业测试工具会更好一点.

想看看响应时间的分布情况可以加上--latency参数:

1. wrk -t12 -c100 -d30s -T30s --latency http://www.baidu.com

1. Running 30s test @ http://www.baidu.com

2. 12 threads and 100 connections

3. Thread Stats Avg Stdev Max +/- Stdev

4. Latency 1.22s 1.88s 17.59s 89.70%

5. Req/Sec 14.47 9.92 98.00 77.06%

6. Latency Distribution

7. 50% 522.18ms

8. 75% 1.17s

9. 90% 3.22s

10. 99% 8.87s

11. 3887 requests in 30.09s, 57.82MB read

12. Socket errors: connect 0, read 2, write 0, timeout 0

13. Requests/sec: 129.19

14. Transfer/sec: 1.92MB

可以看到50%在0.5秒以内, %75在1.2s 以内. 看上去还不错.

看到这里可能有人会说了, HTTP 请求不会总是这么简单的, 通常我们会有 POST,GET 等多个 method, 会有 Header, 会有 body 等.

在我第一次知道有 wrk 这个工具的时候他确实还不太完善, 要想测试一些复杂的请求还有点难度. 现在 wrk 支持 lua 脚本. 在这个脚本里你可以修改 method, header, body, 可以对 response 做一下自定义的分析. 因为是 lua 脚本, 其实这给了你无限的可能. 但是这样一个强大的功能如果不谨慎使用, 会降低测试端的性能, 测试结果也受到影响.

一般修改method, header, body不会影响测试端性能, 但是操作 request, response 就要格外谨慎了.

我们通过一些测试场景在看看怎么使用 lua 脚本.

POST + header + body.

首先创建一个 post.lua 的文件:

1. wrk.method = "POST"

2. wrk.body = "foo=bar&baz=quux"

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

就这三行就可以了, 当然 headers 可以加入任意多的内容.

然后执行:

1. wrk -t12 -c100 -d30s -T30s --script=post.lua --latency http://www.baidu.com

当然百度可能不接受这个 post 请求.

对 wrk 对象的修改全局只会执行一次.

通过 wrk 的源代码可以看到 wrk 对象的源代码有如下属性:

1. local wrk = {

2. scheme = "http",

3. host = "localhost",

4. port = nil,

5. method = "GET",

6. path = "/",

7. headers = {},

8. body = nil,

9. thread = nil,

10. }

schema, host, port, path 这些, 我们一般都是通过 wrk 命令行参数来指定.

wrk 提供的几个 lua 的 hook 函数:

setup 函数

这个函数在目标 IP 地址已经解析完, 并且所有 thread 已经生成, 但是还没有开始时被调用. 每个线程执行一次这个函数.

可以通过thread:get(name), thread:set(name, value)设置线程级别的变量.

init 函数

每次请求发送之前被调用.

可以接受 wrk 命令行的额外参数. 通过 -- 指定.

delay函数

这个函数返回一个数值, 在这次请求执行完以后延迟多长时间执行下一个请求. 可以对应 thinking time 的场景.

request函数

通过这个函数可以每次请求之前修改本次请求的属性. 返回一个字符串. 这个函数要慎用, 会影响测试端性能.

response函数

每次请求返回以后被调用. 可以根据响应内容做特殊处理, 比如遇到特殊响应停止执行测试, 或输出到控制台等等.

1. function response(status, headers, body)

2.ifstatus ~= 200 then

3. print(body)

4. wrk.thread:stop()

5. end

6. end

done函数

在所有请求执行完以后调用, 一般用于自定义统计结果.

1. done = function(summary, latency, requests)

2. io.write("------------------------------\n")

3.for_, p in pairs({ 50, 90, 99, 99.999 })do

4. n = latency:percentile(p)

5. io.write(string.format("%g%%,%d\n", p, n))

6. end

7. end

下面是 wrk 源代码中给出的完整例子:

1. local counter = 1

2. local threads = {}

3.

4. function setup(thread)

5. thread:set("id", counter)

6. table.insert(threads, thread)

7. counter = counter + 1

8. end

9.

10. function init(args)

11. requests = 0

12. responses = 0

13.

14. local msg = "thread %d created"

15. print(msg:format(id))

16. end

17.

18. function request()

19. requests = requests + 1

20.returnwrk.request()

21. end

22.

23. function response(status, headers, body)

24. responses = responses + 1

25. end

26.

27. function done(summary, latency, requests)

28.forindex, thread in ipairs(threads)do

29. local id = thread:get("id")

30. local requests = thread:get("requests")

31. local responses = thread:get("responses")

32. local msg = "thread %d made %d requests and got %d responses"

33. print(msg:format(id, requests, responses))

34. end

35. end

测试复合场景时, 也可以通过 lua 实现访问多个 url.

例如这个复杂的 lua 脚本, 随机读取 paths.txt 文件中的 url 列表, 然后访问.:

1. counter = 1

2.

3. math.randomseed(os.time())

4. math.random(); math.random(); math.random()

5.

6. function file_exists(file)

7. local f = io.open(file, "rb")

8.iff then f:close() end

9.returnf ~= nil

10. end

11.

12. function shuffle(paths)

13. local j, k

14. local n = #paths

15.fori = 1, ndo

16. j, k = math.random(n), math.random(n)

17. paths[j], paths[k] = paths[k], paths[j]

18. end

19.returnpaths

20. end

21.

22. function non_empty_lines_from(file)

23.ifnot file_exists(file) thenreturn{} end

24. lines = {}

25.forline in io.lines(file)do

26.ifnot (line == '') then

27. lines[#lines + 1] = line

28. end

29. end

30.returnshuffle(lines)

31. end

32.

33. paths = non_empty_lines_from("paths.txt")

34.

35.if#paths <= 0 then

36. print("multiplepaths: No paths found. You have to create a file paths.txt with one path per line")

37. os.exit()

38. end

39.

40. print("multiplepaths: Found " .. #paths .. " paths")

41.

42. request = function()

43. path = paths[counter]

44. counter = counter + 1

45.ifcounter > #paths then

46. counter = 1

47. end

48.returnwrk.format(nil, path)

49. end

关于 cookie

有些时候我们需要模拟一些通过 cookie 传递数据的场景. wrk 并没有特殊支持, 可以通过 wrk.headers["Cookie"]="xxxxx"实现.

下面是在网上找的一个例子, 取 Response的cookie作为后续请求的cookie

1. function getCookie(cookies, name)

2. local start = string.find(cookies, name .. "=")

3.

4.ifstart == nil then

5.returnnil

6. end

7.

8.returnstring.sub(cookies, start + #name + 1, string.find(cookies, ";", start) - 1)

9. end

10.

11. response = function(status, headers, body)

12. local token = getCookie(headers["Set-Cookie"], "token")

13.

14.iftoken ~= nil then

15. wrk.headers["Cookie"] = "token=" .. token

16. end

17. end

wrk 本身的定位不是用来替换 loadrunner 这样的专业性能测试工具的. 其实有这些功能已经完全能应付平时开发过程中的一些性能验证了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容