seansie's blog

autocannon API 端點壓力測試套件介紹

autocannon 是一款受 wrk 啟發、由 Node.js 撰寫的高效能 HTTP/1.1 基準測試(Benchmarking)工具。其支援 HTTP Pipelining ⚡,在 Node.js 環境中能榨出極高吞吐量,非常適合用於測試 REST API、GraphQL 或微服務的極限乘載力。

一、 📦 安裝與前置準備

你可以透過 pnpm 選擇「全域安裝」或「作為專案開發依賴 (Dev Dependency)」:

1. 🌐 全域安裝 (常用於個人電腦快速測試)

pnpm add -g autocannon

2. 📁 專案開發依賴 (推薦,便於團隊與 CI/CD 共享版本)

pnpm add -D autocannon

3. ⚡ 免安裝直接執行 (使用 pnpm dlx)

如果你不想預先安裝,可以直接呼叫:

pnpm dlx autocannon http://localhost:3000

二、 🛠️ CLI 快速上手與常用參數

CLI 是最快速驗證 API 效能的方式。

1. ⚙️ 常用選項說明

參數 長名稱 預設值 說明
-c --connections 10 並發連線數 (Concurrent Connections) 👥
-d --duration 10 壓測持續時間 (秒) ⏱️
-p --pipelining 1 每個連線同時送出的管線化請求數 (極限測試時建議調整為 10 或 20) 🚀
-a --amount 總請求次數 (若設定此項,將忽視 -d 時間設定) 🎯
-m --method GET HTTP 請求方法 (GET, POST, PUT, DELETE…) 📮
-H --headers 自訂 Header,可重複傳入多個 🔑
-b --body 請求主體 (Payload) 📦
-w --workers 1 開啟多執行緒 (Worker Threads) 發送請求,避免壓測端 CPU 成為瓶頸 🧠
-t --timeout 10 單一請求超時時間 (秒) ⏳
-l --latency false 輸出詳細的延遲統計表 (包含 p99.9 等百分位數) 📊
--json --json false 以 JSON 格式輸出結果 (適合系統整合) 📄

2. 💡 實用 CLI 範例

  • 🔹 基礎 GET 壓測 (50 連線,持續 15 秒)
pnpm dlx autocannon -c 50 -d 15 http://localhost:3000/api/health
  • 🔹 POST JSON Payload 測試
pnpm dlx autocannon -c 100 -d 10 \
  -m POST \
  -H "Content-Type=application/json" \
  -H "Authorization=Bearer YOUR_ACCESS_TOKEN" \
  -b '{"userId": 101, "action": "checkout"}' \
  http://localhost:3000/api/orders
  • 🔹 極限效能測試 (開啟 Pipelining 與 Worker Threads)
pnpm dlx autocannon -c 200 -d 10 -p 10 -w 4 http://localhost:3000/api/v1/products

三、 📊 測試結果與指標詳細判讀

執行後,autocannon 會輸出如下的表格與統計:

Running 10s test @ http://localhost:3000/api/v1/products
100 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max  │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────┤
│ Latency │ 1 ms │ 3 ms │ 18 ms │ 45 ms│ 4.12 ms │ 8.54 ms │ 98 ms│
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────┘
┌───────────┬────────┬────────┬────────┬────────┬───────────┬────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg       │ Stdev  │ Min    │
├───────────┼────────┼────────┼────────┼────────┼───────────┼────────┼────────┤
│ Req/Sec   │ 12000  │ 14500  │ 24000  │ 26000  │ 23410.5   │ 2100   │ 11200  │
├───────────┼────────┼────────┼────────┼────────┼───────────┼────────┼────────┤
│ Bytes/Sec │ 2.4 MB │ 2.9 MB │ 4.8 MB │ 5.2 MB │ 4.68 MB   │ 420 kB │ 2.2 MB │
└───────────┴────────┴────────┴────────┴────────┴───────────┴────────┴────────┘

Req/Bytes counts sampled once per second.
# 234k requests in 10.05s, 46.8 MB read
# 0 errors, 0 timeouts, 0 non-2xx responses

🔍 欄位詳細說明

⏱️ Latency (回應延遲時間,毫秒 ms)

  • Avg (平均值):所有請求延遲的平均,容易受極端值干擾。
  • 50% (中位數 / p50):一半的使用者回應時間快於此值。
  • 97.5% / 99% (長尾延遲 / Tail Latency):🔥 最重要的穩定度指標。表示 $99%$ 的請求回應時間都在該數值以內。若 Avg 只有 4ms 但 p99 突增至數百毫秒,代表伺服器存在偶發性阻塞(例如:垃圾回收 GC 停頓、DB connection pool 耗盡或 Event Loop 被阻塞)。
  • Stdev (標準差):數值越低,說明系統回應越穩定均勻。

⚡ Req/Sec (每秒請求數 / QPS / TPS)

  • Avg:伺服器平均每秒能處理的請求數。數字越高代表吞吐量越好。

🌐 Bytes/Sec (每秒網路流量吞吐)

每秒傳送的資料量。若 QPS 無法提升但 Bytes/Sec 已接近網卡限制,代表網卡頻寬滿載。

🏥 健康度統計 (最底部的數值)

  • errors:TCP/Socket 連線失敗或重置數量。
  • timeouts:超過設定時間未獲得回應的請求數。
  • non-2xx responses:收到 HTTP 4xx 或 5xx 的數量。⚠️ 特別注意:當伺服器壞掉直接回傳 500 時,QPS 可能會異常飆高,必須比對 non-2xx 是否不為 0。

四、 ⚙️ 啟動機制、setupClient 與 client 物件詳解

在 Node.js 中呼叫 autocannon(opts, [cb]) 時,執行方式取決於是否有傳入完成 Callback 函式。

1. 🔬 深度解析:什麼是 client 物件與 setupClient?

在選項中,setupClient 是一個用來初始化連線實例的 Callback 函式:

setupClient: (client) => {
  // 💡 這個 client 代表其中一個「虛擬使用者 / TCP 持久連線」
}

(1) 🔌 什麼是 client 物件?

  • 觀念:若你設定 connections: 10,autocannon 會在內部建立 10 個長連線 (HTTP Keep-Alive / TCP Socket) 實例。每一個實例就是一個 client 物件(本質上是一個 EventEmitter)。
  • 生命週期:這 10 個 client 物件會貫穿整個壓測過程。在 10 秒的壓測期間內,這 10 個 client 會各自不斷重複以下循環: [發送請求] ➔ [收到回應] ➔ [發送下一個請求] ➔ [收到回應] ...
  • 觸發時機setupClient 函式會在每個 client 被建立時呼叫 1 次。若 connections: 10setupClient 就會被呼叫 10 次,傳入 10 個不同的 client 實例。

(2) 🔄 client vs req 的關鍵區別

在監聽 client.on('request', (req) => ...) 時,很多人會混淆 clientreq

物件 代表意義 存在時間 核心用途
client 🔌 連線通道 / 虛擬使用者 (EventEmitter) 整個壓測生命週期 (只建立一次) 用於監聽 requestresponseerror 事件
req ✉️ 當前這「一次」發送的 HTTP 請求 僅存在於該次請求發送前的瞬間 用於改寫 req.path (隨機 ID)、req.headersreq.body

(3) 🛠️ client 支援的常用事件與屬性:

事件 / 屬性 說明
client.on('request', (req) => ...) ⭐ 最常用。每次準備送出請求前觸發,傳入當前的 req 物件以修改請求細節
client.on('response', (statusCode, resBytes, responseTime) => ...) 當該連線收到伺服器回應時觸發,可用於檢查狀態碼或紀錄延遲
client.on('error', (err) => ...) 當該連線發生 TCP / Socket 底層錯誤時觸發
client.setHeaders(headers) 設定該 client 後續所有請求預設帶有的 Header 物件
client.setBody(body) 設定該 client 後續所有請求預設帶有的 Body 內容

2. 🤖 自動執行模式

只要呼叫 autocannon(opts, cb) 時傳入最後一個 Callback 函式,autocannon 在建立完實例後就會「自動開始壓測」。

配合 setupClient 回呼函式隨機產生 API ID 的完整範例:

import autocannon from 'autocannon';

const instance = autocannon({
  url: '[https://your-api.com](https://your-api.com)',
  connections: 10, // 會建立 10 個虛擬 Client,因此 setupClient 會被呼叫 10 次
  duration: 10,
  
  // setupClient 是一個 Callback 函式,參數 client 為這 10 個連線實例之一
  setupClient: (client) => {
    // 🎲 為這個 client 監聽 request 事件:每次它要發送 HTTP 請求前都會觸發此 Callback
    client.on('request', (req) => {
      // 1. 動態隨機產生 1 ~ 1000 的 API ID
      const randomId = Math.floor(Math.random() * 1000) + 1;
      
      // 2. 直接改寫這次請求的 path
      req.path = `/api/users/${randomId}`;

      // 3. (選擇性) 動態調整 Header,例如加上時間戳記
      req.headers['x-request-timestamp'] = Date.now().toString();
    });

    // (選擇性) 監聽該 client 收到回應的事件
    client.on('response', (statusCode, resBytes, responseTime) => {
      if (statusCode >= 400) {
        console.warn(`⚠️ 收到錯誤狀態碼: ${statusCode}`);
      }
    });
  }
}, (err, result) => {
  // 🎯 傳入這個完成 Callback 後,建立完 instance 就會「自動執行」壓測
  if (err) return console.error('❌ 壓測出錯了:', err);
  
  console.log(`⚡ 平均 QPS: ${result.requests.average}`);
  console.log(`💥 錯誤的請求數量: ${result.errors}`);
});

3. 🎮 手動執行與事件監聽模式

如果不傳最後的完成 Callback,autocannon(opts) 會回傳一個 EventEmitter 實例 (instance)。你可以監聽多種事件(如 starttickdonereqError),並可在特定條件下呼叫 instance.stop() 主動結束壓測。

import autocannon from 'autocannon';

// 不傳最後的 Callback 函式,取得 instance 實例
const instance = autocannon({
  url: '[https://your-api.com](https://your-api.com)',
  connections: 20,
  duration: 10,
  setupClient: (client) => {
    client.on('request', (req) => {
      // 🎲 每次發送前動態產生亂數 ID
      const randomProduct = Math.floor(Math.random() * 500) + 1;
      req.path = `/api/products/${randomProduct}`;
    });
  }
});

// 🚀 監聽啟動事件
instance.on('start', () => {
  console.log('🚀 壓測已開始...');
});

// ⏱️ 監聽每秒進度 (tick)
instance.on('tick', (stats) => {
  // stats 包含該秒的單秒數據
});

// ❌ 監聽單一請求錯誤
instance.on('reqError', (err) => {
  console.error('💥 發生請求錯誤:', err.message);
});

// 🎉 監聽完成事件 (取得最終結果)
instance.on('done', (result) => {
  console.log('✅ 壓測完成!');
  console.log(`📊 總請求數: ${result.requests.total}`);
  console.log(`⏱️ p99 延遲: ${result.latency.p99} ms`);
});

// 🛑 手動控制:需要時呼叫 stop() 主動中止測試
setTimeout(() => {
  if (/* 觸發緊急條件 */ false) {
    console.log('⚠️ 手動中止壓測!');
    instance.stop(); // 手動結束並觸發 done 事件
  }
}, 3000);

4. 🎨 美觀輸出與進度條

不論是自動還是手動執行的 instance,都可以直接傳給 autocannon.track() 來渲染終端機進度條:

import autocannon from 'autocannon';

const instance = autocannon({
  url: '[https://your-api.com](https://your-api.com)',
  connections: 50,
  duration: 10
}, (err, result) => {
  if (err) console.error(err);
});

// 📊 直接把 instance 傳給 track 函數(自動或手動都可以處理)
autocannon.track(instance, { renderProgressBar: true });

5. ⚡ 現代化 Async / Await 封裝範例

在現代專案中,通常會以 Promise 將其封裝,配合 async/await 呼叫:

import autocannon from 'autocannon';

async function runBenchmark() {
  console.log('🚀 開始執行壓測...');

  const result = await new Promise((resolve, reject) => {
    const instance = autocannon(
      {
        url: '[https://your-api.com](https://your-api.com)',
        connections: 50,
        duration: 10,
        setupClient: (client) => {
          client.on('request', (req) => {
            // 🎲 動態隨機生成 API ID
            const randomId = Math.floor(Math.random() * 5000) + 1;
            req.path = `/api/products/${randomId}`;
          });
        }
      },
      (err, res) => (err ? reject(err) : resolve(res))
    );

    // 📊 掛載進度條
    autocannon.track(instance, { renderProgressBar: true });
  });

  console.log('\n✅ 測試完成!');
  console.log(`⚡ 平均 QPS: ${result.requests.average}`);
  console.log(`⏱️ p99 延遲: ${result.latency.p99} ms`);
}

runBenchmark().catch(console.error);

五、 🎯 進階應用情境

1. ✉️ POST / 自訂 Header

import autocannon from 'autocannon';

const instance = autocannon({
  url: '[https://your-api.com](https://your-api.com)',
  connections: 50,
  duration: 10,
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'authorization': 'Bearer your-dynamic-token'
  },
  body: JSON.stringify({
    name: 'Test User',
    email: 'test@example.com'
  })
}, (err, result) => {
  if (err) console.error(err);
  else console.log('✅ 測試完成!');
});

2. 🔀 多重 API 端點管道測試 (requests 陣列)

按順序輪流發送不同的 API 請求:

import autocannon from 'autocannon';

const instance = autocannon({
  url: '[https://your-api.com](https://your-api.com)',
  connections: 10,
  duration: 5,
  requests: [
    {
      method: 'GET',
      path: '/'
    },
    {
      method: 'GET',
      path: '/api/products',
      headers: { 'x-custom-header': 'abc' }
    },
    {
      method: 'POST',
      path: '/api/checkout',
      body: JSON.stringify({ id: 123 })
    }
  ]
});

六、 🤖 CI/CD 自動化與效能門檻檢測 (Quality Gate)

將效能壓測納入 CI/CD 流程中,當 API 效能退化(如 p99 延遲過高或出現錯誤)時自動讓 Build 失敗。

建立一個驗證腳本 scripts/perf-test.js

// scripts/perf-test.js
import autocannon from 'autocannon';

const THRESHOLDS = {
  maxErrorRate: 0,       // ❌ 錯誤數必須為 0
  maxNon2xx: 0,          // ⚠️ 非 2xx 狀態碼必須為 0
  maxLatencyP99: 200,    // ⏱️ p99 延遲不可超過 200ms
  minAvgQps: 1000        // ⚡ 平均 QPS 必須大於等於 1000
};

async function runCiTest() {
  console.log('🔍 正在執行 CI 效能門檻檢測...');

  const result = await new Promise((resolve, reject) => {
    const instance = autocannon(
      {
        url: process.env.TEST_TARGET_URL || 'http://localhost:3000/api/health',
        connections: 50,
        duration: 10
      },
      (err, res) => (err ? reject(err) : resolve(res))
    );
    
    autocannon.track(instance, { renderProgressBar: true });
  });

  console.log('\n--- 📊 壓測數據門檻驗證 ---');
  let passed = true;

  if (result.errors > THRESHOLDS.maxErrorRate) {
    console.error(`❌ 失敗: 連線錯誤數 (${result.errors}) 超過門檻 (${THRESHOLDS.maxErrorRate})`);
    passed = false;
  }

  if (result.non2xx > THRESHOLDS.maxNon2xx) {
    console.error(`❌ 失敗: 非 2xx 回應數 (${result.non2xx}) 超過門檻 (${THRESHOLDS.maxNon2xx})`);
    passed = false;
  }

  if (result.latency.p99 > THRESHOLDS.maxLatencyP99) {
    console.error(`❌ 失敗: p99 延遲 (${result.latency.p99} ms) 超過門檻 (${THRESHOLDS.maxLatencyP99} ms)`);
    passed = false;
  }

  if (result.requests.average < THRESHOLDS.minAvgQps) {
    console.error(`❌ 失敗: 平均 QPS (${result.requests.average}) 低於門檻 (${THRESHOLDS.minAvgQps})`);
    passed = false;
  }

  if (!passed) {
    console.error('\n💥 效能測試未達標!');
    process.exit(1);
  }

  console.log('\n🎉 所有效能門檻皆已順利通過!');
  process.exit(0);
}

runCiTest().catch((err) => {
  console.error('💥 執行壓測時發生意外錯誤:', err);
  process.exit(1);
});

package.json 設定指令並執行:

{
  "scripts": {
    "test:perf": "node scripts/perf-test.js"
  }
}
pnpm test:perf

七、 📈 報表輸出

除了終端機列印,也可以將結果存成報告:

1. 🌐 產生 HTML 視覺化報表 (CLI)

pnpm dlx autocannon [https://your-api.com](https://your-api.com) --report > report.html

2. 📄 輸出 JSON 格式供歷史紀錄分析

pnpm dlx autocannon [https://your-api.com](https://your-api.com) --json > result.json