介紹如何使用 wrk
對 HTTP 網頁伺服器進行負載與效能測試,並搭配 LuaJIT 指令稿客製化各項功能。
wrk 是一個 HTTP 標竿測試工具,其核心主要以 C 語言撰寫而成,可以透過多執行緒的方式產生大量的測試流量,亦可搭配 LuaJIT 指令稿擴充各種功能,例如請求產生、回應處理以及產生自訂報表。
若在一般的 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 標頭,可以使用 -H
參數:
# 指定 HTTP 標頭 wrk -t12 -c400 -d30s -H"User-Agent: wrk" http://127.0.0.1/
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 格式的資料,只要將 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
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.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 請求中放入一個 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