介紹如何使用 wrk 對 HTTP 網頁伺服器進行負載與效能測試,並搭配 LuaJIT 指令稿客製化各項功能。
wrk 是一個 HTTP 標竿測試工具,其核心主要以 C 語言撰寫而成,可以透過多執行緒的方式產生大量的測試流量,亦可搭配 LuaJIT 指令稿擴充各種功能,例如請求產生、回應處理以及產生自訂報表。
安裝 wrk
若在一般的 Linux 或 macOS 系統中,可以在下載原始碼之後,執行 make 編譯:
# 下載 wrk 原始碼 git clone https://github.com/wg/wrk.git # 編譯 wrk cd wrk/ make
若在 Ubuntu Linux 中,可用 apt 安裝:
# 安裝 wrk 套件
sudo apt install wrk
基本效能測試
使用 wrk 進行 HTTP 伺服器的效能測試時,可以指定一些測試的參數,以下是最常用的幾個參數:
-t:使用的執行緒(threads)數量。-c:維持連線的數量。-d:測試時間。-H:設定 HTTP 標頭。-s:指定 Lua 指令稿。--timeout:連線逾時時間。--latency:顯示連線延遲分佈。
wrk 的參數中,需要指定數值的部分都可以使用標準的 SI 單位,例如 1k、1M 與 1G,而時間部分也要加上單位,例如 2s、2m、2h。
例如使用 12 條執行緒,維持 400 條開啟的連線,持續測試 30 秒,則可執行:
# 基本效能測試 wrk -t12 -c400 -d30s http://127.0.0.1/
Running 30s test @ https://127.0.0.1/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.58s   304.06ms   2.00s    82.55%
    Req/Sec    40.75     54.17   303.00     85.30%
  6863 requests in 30.09s, 123.36MB read
  Socket errors: connect 0, read 0, write 0, timeout 753
Requests/sec:    228.07
Transfer/sec:      4.10MB
顯示延遲分佈
若要顯示連線延遲(latency)的分佈,可以加上 --latency 參數:
# 顯示連線延遲分佈 wrk -t12 -c400 -d30s --latency http://127.0.0.1/
Running 30s test @ https://127.0.0.1/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.61s   336.25ms   2.00s    83.32%
    Req/Sec    36.40     50.57   282.00     86.61%
  Latency Distribution
     50%    1.65s 
     75%    1.81s 
     90%    1.93s 
     99%    1.99s 
  6506 requests in 30.09s, 116.94MB read
  Socket errors: connect 0, read 0, write 0, timeout 1061
Requests/sec:    216.24
Transfer/sec:      3.89MB
指定 HTTP 標頭
若要自行加入指定的 HTTP 標頭,可以使用 -H 參數:
# 指定 HTTP 標頭 wrk -t12 -c400 -d30s -H"User-Agent: wrk" http://127.0.0.1/
POST 請求
wrk 預設是發送 GET 請求,若要發送 POST 請求,可以搭配 Lua 指令稿來設定,以下是一個用來發送 POST 請求的 Lua 指令稿:
-- 請求類型 wrk.method = "POST" -- 請求內容 wrk.body = "foo=bar&baz=quux" -- HTTP 標頭 wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
將上面這段指令稿儲存為 post.lua,並以下列指令執行測試:
# 使用 post.lua 指令稿進行測試 wrk -t12 -c400 -d30s -s counter.lua https://127.0.0.1/
傳送 JSON 資料
若要傳送 JSON 格式的資料,只要將 Content-Type 改為 JSON 類型,然後將 JSON 資料內容放進 wrk.body 中即可:
-- 請求類型 wrk.method = "POST" -- JSON 資料內容 wrk.body = '{"test":"abcd"}' -- 以 HTTP 標頭將 Content-Type 設定為 JSON wrk.headers["Content-Type"] = "application/json"
將這段指令稿儲存為 json_test.lua,以 HTTPBin 測試:
# 傳送 JSON 資料 wrk -t12 -c400 -d30s -s json_test.lua https://httpbin.org/post
動態產生 HTTP 請求
wrk 可以搭配 Lua 指令稿自訂各種處理程序,例如以下指令稿可以依序送出不同編號的網址,同時將編號設定在 HTTP 標頭中:
-- 設定計數器初始值 counter = 0 -- 定義 HTTP 請求建立函數 request = function() path = "/" .. counter -- 以計數器產生網址的路徑 wrk.headers["X-Counter"] = counter -- 設定 HTTP 標頭 counter = counter + 1 -- 遞增計數器 return wrk.format(nil, path) -- 產生並傳回 HTTP 請求 end
將上面這段指令稿儲存為 counter.lua,並以下列指令執行測試:
# 使用 counter.lua 指令稿進行測試 wrk -t12 -c400 -d30s -s counter.lua https://127.0.0.1/
當 wrk 以多執行緒的方式執行時,每一條執行緒中會有獨立的 counter 變數,所以這裡使用 12 條執行緒,程式就會同時發送 12 個 /0 路徑的網址、接著同時發送 12 個 /1 路徑的網址,以此類推。
解析 JSON 格式的 HTTP 回應資料
如果需要檢查 JSON 格式的 HTTP 回應資料,以確認執行有沒有成功,可以使用 json.lua 這個函式庫來處理 JSON 資料的解析,其使用方式很簡單,只要下載 json.lua 這個指令稿之後,將 json.lua 跟我們自己的指令稿放在一起,即可直接引入使用:
-- 引入 json.lua json = require "json" -- 請求類型 wrk.method = "POST" -- JSON 資料內容 wrk.body = '{"test":"abcd"}' -- 以 HTTP 標頭將 Content-Type 設定為 JSON wrk.headers["Content-Type"] = "application/json" -- 定義處理 HTTP 回應的 response 函數 function response(status, headers, body) result = json.decode(body) -- 解析 JSON 格式的 body 資料 print(result["data"]) -- 輸出 JSON 資料中的 data 欄位 end
將這段指令稿儲存為 json_parse.lua,以 HTTPBin 測試:
# 解析 JSON 格式之 HTTP 回應資料 wrk -t12 -c400 -d30s -s json_parse.lua https://httpbin.org/post
分析與統計 HTTP 回應資料
若要分析與統計 HTTP 回應的資料,可以參考以下指令稿結構。在這段指令稿中,我們在每一個 HTTP 請求中放入一個 randNum 隨機變數,再從回應訊息中讀出這個變數,統計大於以及小於 0.5 的個數,這種測試程式碼架構可以很容易修改成各種應用情境,統計測試成功與失敗的次數。
-- 引入 json.lua json = require "json" -- 請求類型 wrk.method = "POST" -- 以 HTTP 標頭將 Content-Type 設定為 JSON wrk.headers["Content-Type"] = "application/json" -- 儲存執行緒的變數 local threads = {} -- 各執行緒設置函數 function setup(thread) table.insert(threads, thread) end -- 各執行緒初始化函數 function init(args) large = 0 small = 0 end -- 產生包含隨機資料的 HTTP 請求 function request() randNum = math.random() -- 產生隨機亂數 wrk.body = json.encode({randNum = randNum}) -- 產生 JSON 格式請求資料 return wrk.format(nil, nil) -- 產生並傳回 HTTP 請求 end -- 定義處理 HTTP 回應的 response 函數 function response(status, headers, body) result = json.decode(body) -- 解析 JSON 格式的 body 資料 if result["json"]["randNum"] > 0.5 then large = large + 1 else small = small + 1 end end -- 結束處理函數 function done(summary, latency, requests) large_sum = 0 small_sum = 0 print("--------------- Result ---------------") for index, thread in ipairs(threads) do local large = thread:get("large") local small = thread:get("small") large_sum = large_sum + large small_sum = small_sum + small end print("large:", large_sum) print("small:", small_sum) end
其中 setup() 函數會在每條執行緒執行前被呼叫一次,在這個函數中我們將各執行緒的 thread 變數放入 threads 中儲存起來,以利後續統計時使用。init() 會在每一條執行序開始執行時進行變數的初始化。request() 函數會在每一次發送 HTTP 請求時,產生一個 randNum 隨機亂數,並以 JSON 的格式放入 HTTP 請求中。response() 則是會對收到的每一條 HTTP 回應進行分析處理。最後統一由 done() 函數取得每一條執行緒的資料,統計出整體的測試報告。
將這段指令稿儲存為 random.lua,以 HTTPBin 測試:
# 分析與統計 HTTP 回應資料 wrk -s random.lua https://httpbin.org/post
Running 10s test @ https://httpbin.org/post
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   256.94ms  120.11ms   1.08s    87.86%
    Req/Sec    19.91      8.51    40.00     48.12%
  370 requests in 10.02s, 226.21KB read
Requests/sec:     36.93
Transfer/sec:     22.58KB
--------------- Result ---------------
large:	200
small:	170

