Temporal 效能測試筆記(一):k6 基本觀念與送壓設定

Temporal 效能測試筆記(一):k6 基本觀念與送壓設定
Photo by Susan Holt Simpson / Unsplash

一般認知上,要在系統中導入一套新的工具,總有很多事項需要考量,盡量避免破壞原有的運作,其中包含效能好不好,是否會導致系統卡住,而流程引擎作為核心機制更需要這方面的考量。即使 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=warmupworkflow_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: 9timeUnit: '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 最後真的完成幾筆 iterationsRequest 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-smokemake k6-docker-load 這類 k6 target 會再啟動 starter 並執行測試。

背後跑的仍然是 k6,只是 Docker network、報告路徑、scenario 設定都先處理好了。前面講的 scenariosthresholds 也沒有寫死在 JS 裡,而是放在 test-plans/*.json,結構跟 k6 options 對齊。

完整的啟動、驗證、報告路徑,見 README.md

接到第二篇

第一篇先把 k6 script、options、scenarios、VU、thresholds 串起來。

第二篇開始看跑完後的 metrics 和 report:checkshttp_req_failediterationsdropped_iterations、avg、p95、p99、max。