問題情境

Load test 跑完會產生大量指標,但 CI pipeline 需要的是 pass/fail 訊號。若沒有 threshold 把指標轉成判讀結論,效能退化只能靠人工看 dashboard 發現,等到看見時通常已經累積數個版本。

另一面,threshold 的判讀品質取決於 workload model 的真實度。用 --vus 10 --duration 30s 跑出來的結果跟 production 流量結構差距太大時,threshold 通過也無法證明 production 安全。

這篇處理兩個問題:怎麼設 threshold 讓 CI gate 可靠,怎麼設 scenario 讓 workload 接近真實。

Threshold 設計

Threshold 的責任是把 load test 指標轉成 CI 的 pass/fail 訊號。k6 在所有 threshold 都通過時回傳 exit code 0,任一 threshold 失敗就回傳非零 — CI pipeline 直接用 exit code 判斷。

多指標 threshold

單一指標 threshold 容易漏風險。latency 正常但 error rate 偏高代表系統在丟請求;throughput 正常但 latency 偏高代表排隊開始堆積。完整的 threshold 至少涵蓋三個面向:

1export const options = {
2  thresholds: {
3    http_req_duration: ['p(95)<500', 'p(99)<1000'],
4    http_req_failed:   ['rate<0.01'],
5    http_reqs:         ['rate>100'],
6  },
7};

latency threshold 用 percentile 而不是 average — average 會被長尾稀釋,p95/p99 更接近使用者感知的最差體驗。

門檻來源

Threshold 的門檻從 production baseline 出發。先從 observability 系統(Grafana / Datadog)取最近 7-30 天的 p95/p99 latency 與 error rate,加上可接受退化幅度(通常 10-20%)作為 threshold。門檻太緊會讓 CI 環境噪音觸發 false positive;門檻太寬會讓真退化滑過去。

校準節奏:每月或每次重大架構變更後重新對齊 production baseline,避免 threshold 跟真實系統漂移。

Path-level threshold

不同 API path 的效能特徵不同。checkout 路徑的 latency 容忍度可能比 listing 路徑低很多。k6 的 group + tag 機制讓 threshold 可以按 path 設定:

 1import { group } from 'k6';
 2
 3export default function () {
 4  group('checkout', function () {
 5    // checkout 請求
 6  });
 7  group('listing', function () {
 8    // listing 請求
 9  });
10}
11
12export const options = {
13  thresholds: {
14    'http_req_duration{group:::checkout}': ['p(95)<300'],
15    'http_req_duration{group:::listing}':  ['p(95)<800'],
16  },
17};

path-level threshold 讓 gate 的判讀粒度從「整體效能」細化到「關鍵路徑效能」。

Scenario 設計

Scenario 的責任是讓壓測的流量結構接近 production。k6 提供五種 scenario executor,選擇取決於要控制什麼變量。

Executor控制變量適用場景
constant-vus並發使用者數簡單 smoke test
ramping-vus並發使用者數階梯式升壓找 saturation
constant-arrival-rate固定 RPSCI regression(穩定輸入)
ramping-arrival-rate變化 RPS模擬 production peak/off-peak
externally-controlled外部 API結合 production 流量 replay

Executor 選擇判準

constant-vus 最簡單,但 throughput 會隨 response time 波動 — 伺服器變慢時 RPS 自動下降,掩蓋了真正的壓力。constant-arrival-rate 控制 RPS 穩定,能讓 threshold 的判讀基準一致,但需要設定足夠的 preAllocatedVUs 避免 k6 因為 VU 不足而主動降速。

CI regression 測試建議用 constant-arrival-rate:輸入固定、輸出可比較、版本間的差異才有意義。

Production traffic shape 對齊

用 ramping-arrival-rate 模擬 production 的流量形狀:

 1export const options = {
 2  scenarios: {
 3    peak_simulation: {
 4      executor: 'ramping-arrival-rate',
 5      startRate: 50,
 6      stages: [
 7        { target: 200, duration: '2m' },  // ramp up
 8        { target: 200, duration: '5m' },  // sustain peak
 9        { target: 50,  duration: '1m' },  // ramp down
10      ],
11      preAllocatedVUs: 300,
12    },
13  },
14};

流量形狀的參數(startRate / target / duration)從 production access log 的 peak 時段推算。Shopify 的 BFCM 準備流程把 game day 的 load test scenario 跟實際峰值形狀對齊 — 短時間爆量加高寫入比例需要特別設計 scenario 來覆蓋。

Cohort 模擬

Production 流量不是單一類型。用多 scenario 並行模擬不同 cohort:

 1export const options = {
 2  scenarios: {
 3    read_traffic: {
 4      executor: 'constant-arrival-rate',
 5      rate: 150, exec: 'readFlow',
 6      preAllocatedVUs: 200,
 7      duration: '5m',
 8    },
 9    write_traffic: {
10      executor: 'constant-arrival-rate',
11      rate: 30, exec: 'writeFlow',
12      preAllocatedVUs: 50,
13      duration: '5m',
14    },
15  },
16};
17
18export function readFlow() { /* GET 請求 */ }
19export function writeFlow() { /* POST 請求 */ }

讀寫比例從 production 的 access log 或 APM 資料推算。比例偏差會讓瓶頸位置失真 — 讀為主的模型抓不到寫入引起的 lock contention。

資料驅動

測試資料用 SharedArray 載入,避免每個 VU 各自載入造成記憶體浪費:

1import { SharedArray } from 'k6/data';
2
3const users = new SharedArray('users', function () {
4  return JSON.parse(open('./users.json'));
5});

資料來源可以是 production sample(脫敏後)或 synthetic generation。資料分佈需要接近 production — ID 範圍、key 分佈、payload 大小都會影響 query plan 與 cache 行為。

CI 整合實務

Fast path(每次 push)

固定 scenario + 短 duration(30s-2min),用 constant-arrival-rate 做 regression 偵測。threshold 設在 production baseline + 10%。這一層的目的是快速攔住明顯退化,不需要模擬完整峰值。

Slow path(merge gate)

完整 scenario + 較長 duration(5-15min),包含多 cohort 與 ramping 模擬。threshold 涵蓋 path-level 指標。這一層的目的是深層驗證變更在接近真實壓力下的行為。

結果留存

k6 結果預設輸出到 stdout。CI 整合時用 --out flag 把結果送到時序資料庫(InfluxDB / Prometheus Remote Write / Grafana Cloud k6),讓歷史趨勢可查詢。趨勢比較能偵測 threshold 內但持續惡化的 slow drift。

LinkedIn 的自動化壓測實踐把 load test 結果跟容量預測接在一起 — saturation point 隨時間的變化趨勢直接驅動擴容決策。

邊界與陷阱

Threshold variance:CI runner 的硬體差異(shared runner 的鄰居效應、network jitter、GC pause)會讓同一份 code 在不同 run 產生不同結果。控制方式:dedicated runner 消除鄰居效應、warmup iteration 丟棄前幾輪結果、多次 run 取中位數。若 variance 超過 threshold 的退化幅度,gate 判讀就不可信。

門檻過寬或過緊:threshold 永遠通過代表 gate 形同虛設;threshold 頻繁 false positive 會讓團隊忽略 CI 結果。兩者都會讓 gate 失去判讀價值。校準的判準是:過去 30 天的 threshold 結果中,真正需要關注的退化是否都被攔住,同時 false positive 率低於 5%。

Scenario 跟 production drift:production 的流量結構會隨產品演進改變。定期(每月或每次重大功能上線)用 access log 校準 scenario 的 RPS、cohort 比例與資料分佈,避免模型越跑越偏。

整合路由