天天看點

Web服務壓測神器wrk

wrk

是一款開源的高性能http壓測工具(也支援https),非常小巧,可以執行檔案隻有3M(其中主要是luajit和openssl占用絕大多數空間),别看核心代碼3-5年沒更新了,但依舊非常好用。雖然很早之前我就知道有這麼個工具了,當時學習這個工具的時候我還拿它壓測了我們的個人網站

xindoo.me

,發現mysql性能不行後加了wp-cache,通過cache把我網站的承載能力提升了10多倍。但當時之前簡單使用它的初級功能,最近工作中恰好有個http服務需要壓測,然後就拿wrk做了。這次使用了wrk lua進階功能實作了壓測,我們找到了我們服務的瓶頸,同時也被wrk的超高性能所震驚。

Web服務壓測神器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           

繼續閱讀