Performance Testing 入門 — k6 vs JMeter vs Locust 該選哪個

「網站要扛得住 5000 人同時上線」是業務常常開的單。Performance test 不是壓爆它,是知道它什麼時候會爆。這篇給你 QA 角度的完整入門。

4 種效能測試一次看懂

flowchart TB
    A[Load Test<br>負載測試] --> A1[正常使用量<br>持續一段時間]
    B[Stress Test<br>壓力測試] --> B1[超過正常 2-5 倍<br>找崩潰臨界點]
    C[Spike Test<br>尖峰測試] --> C1[突然爆量<br>例如限時搶購]
    D[Soak Test<br>耐久測試] --> D1[正常量但跑 8+ 小時<br>找 memory leak]

    style A fill:#06b6d4,color:#fff
    style B fill:#ef4444,color:#fff
    style C fill:#f59e0b,color:#fff
    style D fill:#a855f7,color:#fff
類型 模擬什麼 抓什麼 bug
Load 正常流量 + 些許 buffer 平均回應時間、throughput
Stress 超量 2-5 倍 崩潰點、graceful degradation
Spike 突發爆量 autoscaling、queue 機制
Soak 正常量 8 小時+ memory leak、connection leak

多數團隊只做 Load,這是 80% 痛點來源。

為什麼選 k6

flowchart LR
    Choose{選效能<br>測試工具} --> JMeter[JMeter]
    Choose --> Gatling[Gatling]
    Choose --> Locust[Locust]
    Choose --> k6[k6]
    Choose --> Artillery[Artillery]

    JMeter --> JM["Java + GUI<br>老牌、強但重"]
    Gatling --> GA["Scala<br>強但學習曲線"]
    Locust --> LO["Python<br>容易但效能弱"]
    k6 --> K6["JavaScript<br>輕量、CI 友善"]
    Artillery --> AR["YAML<br>簡單但功能少"]

    style k6 fill:#10b981,color:#fff
    style K6 fill:#10b981,color:#fff
工具 語言 學習曲線 CI 友善 1 機可模擬人數
k6 JS ⭐⭐⭐ 30K+ VU
Locust Python ⭐⭐ 5K VU
JMeter XML / GUI 10K VU
Gatling Scala ⭐⭐ 50K+ VU
Artillery YAML ⭐⭐ 5K VU

新團隊建議 k6:JS 寫、CI 一行裝、效能最好。

30 分鐘從 0 到 1:k6 第一個 test

1. 安裝(macOS)

brew install k6

或 Docker:

docker pull grafana/k6

2. 寫第一支腳本

// test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 50,           // 50 個 virtual users
  duration: '30s',   // 跑 30 秒
};

export default function () {
  const res = http.get('https://staging.example.com/api/products');
  check(res, {
    'status is 200': r => r.status === 200,
    'response time < 500ms': r => r.timings.duration < 500,
    'has data': r => JSON.parse(r.body).length > 0,
  });
  sleep(1);
}

3. 跑

k6 run test.js

輸出長這樣:

✓ status is 200
✓ response time < 500ms
✓ has data

checks.........................: 100.00%
http_req_duration..............: avg=187.3ms p(95)=423ms p(99)=789ms
http_reqs......................: 1487   49.5/s
vus............................: 50
data_received..................: 2.3 MB

關鍵指標: - p(95) = 423ms — 95% 的 request 在 423ms 內回完 - p(99) = 789ms — 99% 在 789ms 內 - 49.5/s — 每秒處理 49.5 個 request

寫好 k6 腳本的 5 個原則

1. Stages — 階梯式加壓

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // 2 分鐘漸增到 100 VU
    { duration: '5m', target: 100 },  // 維持 5 分鐘
    { duration: '2m', target: 200 },  // 再增到 200
    { duration: '5m', target: 200 },
    { duration: '2m', target: 0 },    // 漸減回 0
  ],
};

突然開 1000 個 user 是 stress test 的玩法,load test 要漸進

2. Thresholds — 自動 fail

export const options = {
  vus: 50,
  duration: '5m',
  thresholds: {
    http_req_duration: ['p(95)<500'],     // p95 必須 < 500ms
    http_req_failed: ['rate<0.01'],       // 錯誤率 < 1%
    'checks{group:checkout}': ['rate>0.95'],
  },
};

CI 不過自動退 PR。

3. Scenarios — 真實場景

export const options = {
  scenarios: {
    browse_users: {
      executor: 'ramping-vus',
      stages: [{ duration: '5m', target: 80 }],
      exec: 'browse',
    },
    checkout_users: {
      executor: 'constant-vus',
      vus: 20,
      duration: '5m',
      exec: 'checkout',
    },
  },
};

export function browse() { /* 瀏覽 */ }
export function checkout() { /* 結帳 */ }

80% 在瀏覽、20% 在結帳,比所有人都做同件事真實

4. Setup / Teardown

export function setup() {
  // 跑測試前:建測試帳號、塞測試資料
  const res = http.post('/api/test-users');
  return { token: res.json().token };
}

export default function (data) {
  http.get('/api/profile', {
    headers: { Authorization: `Bearer ${data.token}` },
  });
}

export function teardown(data) {
  // 清理
  http.del(`/api/test-users/${data.userId}`);
}

5. Custom Metrics

import { Trend } from 'k6/metrics';

const checkoutTime = new Trend('checkout_time');

export default function () {
  const start = Date.now();
  // 跑 checkout 流程
  checkoutTime.add(Date.now() - start);
}

業務指標自定義,不只看 HTTP timing。

完整實戰範例:模擬電商高峰

import http from 'k6/http';
import { check, group, sleep } from 'k6';

export const options = {
  scenarios: {
    normal_browsing: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 200 },
        { duration: '5m', target: 200 },
        { duration: '2m', target: 0 },
      ],
      exec: 'browse',
    },
    checkout_flow: {
      executor: 'constant-vus',
      vus: 50,
      duration: '7m',
      exec: 'checkout',
    },
  },
  thresholds: {
    'http_req_duration{name:product_list}': ['p(95)<300'],
    'http_req_duration{name:add_to_cart}': ['p(95)<500'],
    'http_req_duration{name:checkout}': ['p(95)<1000'],
    'http_req_failed': ['rate<0.01'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com';

export function browse() {
  group('Browse', () => {
    http.get(`${BASE_URL}/api/products`, { tags: { name: 'product_list' } });
    sleep(2);
    http.get(`${BASE_URL}/api/products/123`, { tags: { name: 'product_detail' } });
    sleep(3);
  });
}

export function checkout() {
  group('Checkout', () => {
    const r1 = http.post(`${BASE_URL}/api/cart`,
      JSON.stringify({ sku: '123', qty: 1 }),
      { tags: { name: 'add_to_cart' }, headers: { 'Content-Type': 'application/json' } }
    );
    check(r1, { 'cart created': r => r.status === 201 });
    sleep(1);

    const r2 = http.post(`${BASE_URL}/api/orders`,
      JSON.stringify({ cart_id: r1.json().id }),
      { tags: { name: 'checkout' }, headers: { 'Content-Type': 'application/json' } }
    );
    check(r2, { 'order placed': r => r.status === 201 });
    sleep(2);
  });
}

跑:

BASE_URL=https://staging.example.com k6 run --out json=results.json scripts/ecommerce.js

CI 整合(GitHub Actions)

name: Performance Test
on:
  schedule:
    - cron: '0 2 * * *'   # 每天凌晨 2 點跑
  workflow_dispatch:

jobs:
  perf:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: grafana/[email protected]
        with:
          filename: perf/ecommerce.js
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
      - if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: perf-results
          path: results.json

不要每個 PR 都跑 — 太慢、太貴。跑 nightly 或 weekly 即可。

結果該怎麼讀(指標解讀)

flowchart TD
    Result[k6 結果] --> Q1{p95 達標?}
    Q1 -->|否| F1[效能不夠<br>找 bottleneck]
    Q1 -->|是| Q2{錯誤率 < 1%?}
    Q2 -->|否| F2[系統不穩<br>看 server log]
    Q2 -->|是| Q3{throughput<br>能持續?}
    Q3 -->|否| F3[降速、queue 滿]
    Q3 -->|是| OK[通過]

    style OK fill:#10b981,color:#fff
    style F1 fill:#ef4444,color:#fff
    style F2 fill:#ef4444,color:#fff
    style F3 fill:#f59e0b,color:#fff

我看效能報告的順序

  1. Error rate — 有錯誤就先看這個
  2. p99 latency — 看最慘的 1%(這才是使用者罵的)
  3. p95 — 主要 SLA 指標
  4. Throughput trend — 看是不是穩定(崩盤前會先降速)
  5. Server-side metrics(搭配 Datadog / Grafana)— CPU / RAM / DB query

只看 average 是新手。Average 200ms 可能包含 p99 = 5 秒。

反模式

  1. 本機跑大流量 — 1 機網路打不出 1000 RPS,要用雲端
  2. 沒 ramp-up — 突然 1000 VU 是 stress test 不是 load
  3. 打 prod — 除非你想被 fire
  4. 不清理 test data — 一個月後 staging DB 變垃圾場
  5. 沒設 threshold — 結果出來大家看一下、沒人 own
  6. 效能規格未談 — 沒 SLA 不知道過或不過
  7. 只跑一次 — 應該 nightly 跑、看趨勢

跟其他工具的比較

k6 vs JMeter

  • JMeter 強在 GUI 設計複雜流程、適合非工程師
  • k6 強在 code 易維護、CI 整合、效能好
  • 新案首選 k6

k6 vs Locust

  • Locust Python 寫、容易上手
  • k6 JS 寫、效能強 5-10 倍
  • 重視效能選 k6、重視團隊 Python 熟悉度選 Locust

k6 vs Gatling

  • Gatling Scala 寫、效能最強
  • k6 JS 寫、夠用且容易
  • 電商 / 金融大規模選 Gatling、中小團隊 k6 即可

進階主題(之後可以深入)

  • Distributed k6 — 雲端跑、模擬全球流量
  • Browser-based perf — k6/browser 加 Chromium 跑(真實前端 perf)
  • gRPC / WebSocket 測試 — k6 原生支援
  • 整合 APM — Datadog / New Relic 看 server-side
  • Continuous performance testing — 整合 SLO 監控

給 QA 的關鍵 takeaway

效能測試不是「測一次過了就算」。它是

  1. 建立 baseline(系統正常時的 p95)
  2. 每次大改動跑、看有沒有 regression
  3. 收集 trend、跟 PM 約 SLO
  4. 上線後配合 APM 監控

從一支 k6 腳本開始,3 個月後你會發現自己變不可或缺