如何給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閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容