是一款開源的高性能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的具體使用吧。
wrk的一切内容都在github
https://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