Temporal 效能測試筆記(一):k6 基本觀念與送壓設定
一般認知上,要在系統中導入一套新的工具,總有很多事項需要考量,盡量避免破壞原有的運作,其中包含效能好不好,是否會導致系統卡住,而流程引擎作為核心機制更需要這方面的考量。即使 Temporal 已有官方的壓測數據,以及各大公司的使用經驗,或許自己動手去體驗可以帶來更深的理解。
本專案是透過 k6 壓測,分析 Temporal Workflow 在加壓過程中運行的狀況,藉以了解 Temporal 的承載能力。
壓測種類
「壓測」是一個泛用的詞,實際上有著不同的類型,回答不同問題。
| 類型 | 主要問題 | 常見做法 |
|---|---|---|
| Smoke test | 路徑有沒有接通? | 小流量短時間,只確認能跑通 |
| Load test | 預期流量下是否穩定? | 用目標流量跑一段固定時間 |
| Stress test | 超過預期後怎麼退化? | 逐步加壓,觀察錯誤率、latency、掉壓 |
| Spike test | 瞬間尖峰撐不撐得住? | 短時間快速拉高 request rate |
| Soak test | 長時間會不會出問題? | 穩定流量跑數小時到數天 |
| Capacity benchmark | 目前環境大約能到哪個量級? | 固定 workload 和環境,記錄 throughput、latency、錯誤率 |
本專案內容主要做三件事:
| 目的 | 對應指令 | 說明 |
|---|---|---|
| Smoke test | make k6-smoke |
簡單確認 k6 能打到 TypeScript starter,starter 能啟動 Temporal workflow |
| Local load test | make k6-docker-load |
在本機 Docker Compose 用固定 arrival rate 送壓 |
| Capacity benchmark | sample run 與第五篇換算 | 用固定 workload 建立量級感,例如 9 WF/s 約等於 52 萬件/日 |
k6 在做什麼
一般 HTTP 壓測可以拆成四個角色:
| 角色 | 一般 HTTP 壓測裡是誰 | 負責什麼 |
|---|---|---|
| 送壓端 | k6 | 安排並送出 request |
| 被測入口 | HTTP API | 接住 request,回傳 response |
| 被測系統 | API 背後的服務、資料庫、佇列 | 真正處理業務工作 |
| 報告 | report.html / summary.json |
呈現成功率、流量、latency |
在本專案中:
- k6 負責按照 script 和 options 對被測系統送 request,記錄成功率耗時、錯誤率等 metrics。
- HTTP API 是 TypeScript starter;starter 背後才是 Temporal Server、worker 和 PostgreSQL。
一次 request 會啟動 workflow、等待 workflow 完成,再回傳結果。
k6
-> TypeScript starter HTTP API
-> Temporal Server / Worker / PostgreSQL
k6 run:把 script 跑起來
本機已經安裝 k6 時,可以先確認版本:
k6 version
執行 k6 script 的基本指令是:
k6 run script.js
k6 會讀這支 script,重複執行裡面的測試動作,最後印出 summary。
最小 script:只送一個 request
一支最小的 k6 HTTP script 如下:
import http from 'k6/http';
export default function () {
http.get('http://localhost:8080/health');
}
這個 function 裡寫的是「每一輪測試要做什麼」。k6 跑壓測時會一輪一輪重複執行;報告裡會把每一輪叫做 iteration。
一般 API 壓測裡,一次 iteration 可能只打一支 API。本專案會把一次 iteration 設計成:
呼叫 TypeScript starter HTTP API
-> starter 啟動 Temporal workflow
-> starter 等 workflow 完成
-> 回傳 workflow result
也就是說,一次 request 的等待時間會包含 workflow 實際跑完的時間。後面看報告時,這個設計會影響我們怎麼解讀 latency。
名詞解釋
看 k6 report 時,會有不少的名詞,得要先知道他們各自的意思。
| 名詞 | 白話理解 | 例子 |
|---|---|---|
| iteration | k6 執行一次測試動作 | 打一次 starter request,等 workflow 完成 |
| check | 每一筆 request 的檢查 | HTTP 200、workflow completed successfully |
| metric | 被量測的數字 | latency、failure rate、iterations |
| tag | metric 的上下文標籤 | scenario=warmup、workflow_type=OrderFulfillmentWorkflow |
| threshold | 整輪測試的驗收門檻 | checks 100%、p99 小於指定毫秒 |
把它們串起來,大概是這樣:
k6 一輪一輪執行 iteration
每一輪送出 request
check 判斷單筆是否成功
metric 記錄過程中的數字
tag 幫 metric 補上下文
threshold 判斷整輪測試是否達標
check:單筆 request 有沒有成功
不僅要把 request 打出去,還要確認這筆算不算成功。
k6 用 check() 做單筆驗證:
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const response = http.get('http://localhost:8080/health');
check(response, {
'starter returned HTTP 200': (r) => r.status === 200,
});
}
checks 就是每一筆 request 跑完後,k6 幫它打勾或打叉。
本專案範例會檢查兩件事:
check(response, {
'starter returned HTTP 200': (r) => r.status === 200,
'workflow completed successfully': () => body.ok === true,
});
第一個檢查 HTTP 層,第二個檢查 workflow 結果。HTTP 200 只代表 starter 有回應,不代表 workflow 業務結果一定成功。
options:決定跑多久、送多少
最小 script 只能把 request 送出去。真的要做壓測,k6 還需要知道:
- 同時用幾個 VU?
- 要跑多久?
- 每秒送幾筆?
- 什麼門檻算這次測試通過?
這些可以寫成 k6 的 options ,最簡單的寫法是 vus + duration:
export const options = {
vus: 10,
duration: '30s',
};
這段可以讀成:
用 10 個 VU 持續跑 30 秒。
這種簡單的寫法適合先把測試跑起來。當測試需要分階段 warmup、pressure、cooldown,或要控制固定 arrival rate,就會改用 scenarios。
scenarios:完整送壓劇本
scenario 是送壓劇本。讓 k6 按照劇本執行:什麼時候開始、每秒送幾筆、送多久、最多準備多少 VU。
export const options = {
scenarios: {
pressure: {
executor: 'constant-arrival-rate',
rate: 9,
timeUnit: '1s',
duration: '4m',
preAllocatedVUs: 180,
maxVUs: 2700,
},
},
};
欄位可以這樣讀:
| 欄位 | 意義 |
|---|---|
executor |
k6 使用哪種送壓方式 |
rate |
每個 timeUnit 安排幾次 iteration |
timeUnit |
rate 的時間單位,本例是每秒 |
duration |
這個階段持續多久 |
preAllocatedVUs |
預先準備多少 virtual users |
maxVUs |
k6 最多可以擴到多少 virtual users |
rate: 9、timeUnit: '1s' 就是:
每秒安排 9 次 iteration
本專案把一次 iteration 設計成「啟動並等待一筆 workflow」,所以它可以近似讀成 9 WF/s。
k6 的 executor 可以簡單分成三類:
| 類型 | executor | 固定的是 | 適合 |
|---|---|---|---|
| 按 iteration 數 | shared-iterations / per-vu-iterations |
跑幾次 | 功能測試、冒煙測試 |
| 按 VU 數 | constant-vus / ramping-vus |
同時有幾個 VU | 模擬固定併發人數 |
| 按到達率 | constant-arrival-rate / ramping-arrival-rate |
每秒幾筆 | 容量測試、SLA 測試 |
容量壓測通常會選 arrival rate 模型,因為它固定送壓節奏。系統變慢時,k6 仍會照目標速率安排 request;如果排不出去,就會出現 dropped_iterations。本專案主要使用 constant-arrival-rate。
目標流量和實際流量
scenario 寫 rate: 9,只代表目標送壓速率。真正有沒有送到,要看報告。
| 名詞 | 白話意思 | 報告看哪裡 |
|---|---|---|
| 目標流量 | 期望 k6 每秒安排幾筆 | scenario 裡的 rate |
| 實際流量 | k6 最後真的完成幾筆 | iterations、Request Rate |
| 掉壓 | k6 原本要送,但沒有排出去 | dropped_iterations |
dropped_iterations > 0 時,代表 k6 沒有完整送出要求的流量。這時候不能直接說系統扛住了,因為測試可能根本少送了。
VU:為什麼 request 慢就需要更多
VU 是 virtual user,也就是 k6 手上的虛擬執行者。一個 VU 送出 request 後,要等 response 回來,才算完成這次 iteration。
response 快,少量 VU 就能輪流處理很多 request;response 慢,前面送出的還在等,新的又要照節奏送,就需要更多 VU 同時等待。
可以用這個粗估:
需要的 VU ≈ arrival rate × e2e latency
本專案一次 iteration 要等 workflow 跑完,e2e 大約 20 秒,所以:
9 WF/s × 20s ≈ 180 VUs
也就是說,9 WF/s 不代表 k6 只要 9 個 VU。它需要約 180 個 VU 來承接還沒完成的 request。
VU 是 k6 送壓端的概念,跟被測端的 worker 不同層:
| 名詞 | 在哪一邊 | 負責什麼 |
|---|---|---|
| VU | k6 送壓端 | 送 request、等 response |
| Temporal worker | 被測端 | 執行 workflow/activity |
thresholds:整輪測試的驗收門檻
check 看的是單筆 request,threshold 看的是整輪測試。
check:這一筆有沒有成功
threshold:這整輪測試有沒有達到門檻
常見 thresholds 長這樣:
export const options = {
thresholds: {
http_req_failed: ['rate<=0.01'],
checks: ['rate==1'],
dropped_iterations: ['count==0'],
http_req_duration: ['p(99)<=180000'],
},
};
拆開看:
| Threshold | 驗收門檻 | 為什麼要看 |
|---|---|---|
http_req_failed |
HTTP failure rate 要低於指定比例 | 確認 HTTP 層沒有大量錯誤 |
checks |
checks 必須 100% 通過 | 確認每筆 workflow 結果符合預期 |
dropped_iterations |
k6 必須完整送出排程 | 確認 k6 端沒有少送流量 |
http_req_duration |
p99 latency 要低於目標 | 確認大多數 request 沒有慢到超出預期 |
本範例還會加 workflow 層 thresholds:
workflow_success_rate: ['rate==1']
workflow_e2e_duration_ms: ['p(99)<=180000']
這樣 HTTP 層和 workflow 層可以分開看。
實際怎麼跑
附上專案 Repo,前面用的是 k6 原生指令 k6 run script.js。本專案 repo 要同時管理 Temporal、PostgreSQL、worker、starter 和 k6 container,所以用 Makefile 把常用指令包起來。
最小路徑是:
make start # 啟動 Temporal、PostgreSQL、worker
make k6-smoke # 驗證 k6 能打到 workflow
make k6-docker-load # 跑本機 Docker 壓測
make start 先把 Temporal 和 worker 準備好;make k6-smoke、make k6-docker-load 這類 k6 target 會再啟動 starter 並執行測試。
背後跑的仍然是 k6,只是 Docker network、報告路徑、scenario 設定都先處理好了。前面講的 scenarios、thresholds 也沒有寫死在 JS 裡,而是放在 test-plans/*.json,結構跟 k6 options 對齊。
完整的啟動、驗證、報告路徑,見 README.md。
接到第二篇
第一篇先把 k6 script、options、scenarios、VU、thresholds 串起來。
第二篇開始看跑完後的 metrics 和 report:checks、http_req_failed、iterations、dropped_iterations、avg、p95、p99、max。