K6 壓力測試

Posted by Adam on August 24, 2022
## 📌 核心知識 ### 什麼是 k6? * **開發者友善**:使用 **JavaScript (ES6)** 撰寫測試腳本。 * **高效能**:底層使用 **Go 語言** 打造,輕量且極省記憶體,單機就能模擬上萬個虛擬使用者 (VU)。 * **自動化整合**:非常適合直接嵌入 CI/CD Pipeline(如 GitHub Actions、GitLab CI)。 ### k6 壓測的核心概念 * **VU (Virtual User)**:虛擬使用者。每一個 VU 都會獨立、重複執行腳本中的 `default` 函式。 * **Duration**:測試持續的時間。 * **Stages**:階段式壓測。用來模擬流量「逐步爬升 (Ramp-up) $\to$ 高峰維持 $\to$ 逐漸下降 (Ramp-down)」的真實情境。 * **Thresholds**:效能合格門檻。用來定義 API 的 SLO/SLA(例如:`95% 的請求必須在 200ms 內完成`),若未達標,k6 會判定測試失敗。 --- ## 📝 範例說明 ### 安裝 k6 你可以透過簡單的指令進行安裝: * **Mac (Homebrew)**: `brew install k6` * **Windows (Chocolatey)**: `choco install k6` * **Linux (Debian/Ubuntu)**: ```bash sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D661C44D echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install k6 ``` ### k6 REST API 通用模組範本 ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; // ========================================== // 利用環境變數彈性調整 Options // ========================================== // 若外部沒有傳入變數,則使用 || 後面的「預設值」 const TARGET_VUS = parseInt(__ENV.VUS) || 25; // 預設 5 個 VU const RAMP_TIME = __ENV.RAMP_TIME || '5m'; // 預設 爬升 10 秒 const RUN_TIME = __ENV.RUN_TIME || '30s'; // 預設 持續 30 秒 const MAX_LATENCY = parseInt(__ENV.MAX_LATENCY) || 500; // 預設 門檻 500ms // ========================================== // 定義兩種不同的 Options 配置 // ========================================== // 方案 A: 使用 traditional stages (傳統階段式:控制人數 VU) const stagesOptions = { stages: [ { duration: '10s', target: 10 }, // 10秒內爬升至 10 個 VU { duration: '30s', target: 10 }, // 維持 10 個 VU 測試 30 秒 { duration: RAMP_TIME, target: TARGET_VUS }, // 動態設定持續壓測時間 { duration: RAMP_TIME, target: TARGET_VUS }, // 動態設定持續壓測時間 ], thresholds: { // 門檻數值也能從環境變數帶入 http_req_duration: [`p(95)<${MAX_LATENCY}`], http_req_failed: ['rate<0.01'], // 期望失敗率低於 1% }, }; // 方案 B: 使用 scenarios (固定 TPS:控制每秒請求數) const scenariosOptions = { scenarios: { constant_tps_test: { executor: 'constant-arrival-rate', // 固定到達率 rate: 15, // 目標每秒 15 次請求 (TPS = 15) timeUnit: '1s', // 時間單位:1 秒 duration: '1m', // 測試持續 1 分鐘 preAllocatedVUs: 20, // 預先分配 20 個 VU maxVUs: 100, // 應付遲鈍 API 的最高擴展上限 }, }, thresholds: { http_req_duration: ['p(95)<500'], }, }; // ========================================== // 根據環境變數 TEST_MODE 動態切換 Options // ========================================== // 預設使用 'stages' 模式,除非外部傳入 TEST_MODE=tps const mode = __ENV.TEST_MODE || 'stages'; export const options = (mode === 'tps') ? scenariosOptions : stagesOptions; // ========================================== // HTTP 工具類別(彈性 Header、Auth、Session) // ========================================== class HttpClient { /** * 建構子:初始化基礎設定 * @param {string} baseUrl - API 的基礎路徑 (例如: https://api.example.com) * @param {string} authToken - 選擇性傳入 Bearer Auth Token */ constructor(baseUrl, authToken = null) { this.baseUrl = baseUrl; this.authToken = authToken; this.cookieJar = http.cookieJar(); // 自動管理 Session (Cookies) } /** * 內部方法:動態建立 Request Headers * @param {object} customHeaders - 使用者自訂的額外 Headers * @returns {object} 完整組合後的 Headers 物件 */ _buildHeaders(customHeaders = {}) { // 預設強制使用 JSON 格式 const defaultHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; // 如果建構子有帶入 Auth Token,自動加入 Authorization Header if (this.authToken) { defaultHeaders['Authorization'] = `Bearer ${this.authToken}`; } // 將自訂 Headers 與預設值合併(自訂值會覆蓋預設值) return Object.assign({}, defaultHeaders, customHeaders); } /** * 通用 GET 請求 * @param {string} endpoint - API 端點路徑 * @param {object} customHeaders - 彈性傳入的額外 Headers */ get(endpoint, customHeaders = {}) { const url = `${this.baseUrl}${endpoint}`; const params = { headers: this._buildHeaders(customHeaders), jar: this.cookieJar, // 帶入 Session Cookie }; return http.get(url, params); } /** * 通用 POST 請求 * @param {string} endpoint - API 端點路徑 * @param {object} payload - 要傳送的 JS 物件(內部會自動轉 JSON) * @param {object} customHeaders - 彈性傳入的額外 Headers */ post(endpoint, payload = {}, customHeaders = {}) { const url = `${this.baseUrl}${endpoint}`; const params = { headers: this._buildHeaders(customHeaders), jar: this.cookieJar, }; // 將 JS 物件轉為 JSON 字串傳送 return http.post(url, JSON.stringify(payload), params); } /** * 通用 PUT 請求 * @param {string} endpoint - API 端點路徑 * @param {object} payload - 要更新的 JS 物件 * @param {object} customHeaders - 彈性傳入的額外 Headers */ put(endpoint, payload = {}, customHeaders = {}) { const url = `${this.baseUrl}${endpoint}`; const params = { headers: this._buildHeaders(customHeaders), jar: this.cookieJar, }; return http.put(url, JSON.stringify(payload), params); } /** * 通用 DELETE 請求 * @param {string} endpoint - API 端點路徑 * @param {object} customHeaders - 彈性傳入的額外 Headers */ delete(endpoint, customHeaders = {}) { const url = `${this.baseUrl}${endpoint}`; const params = { headers: this._buildHeaders(customHeaders), jar: this.cookieJar, }; return http.del(url, null, params); } } // ========================================== // 測試情境主程式 (Default Function) // ========================================== export default function () { // 示範使用免費的測試 API (JSONPlaceholder) const baseUrl = __ENV.BASE_URL || 'https://jsonplaceholder.typicode.com'; // 假設已取得的 Bearer Token(範例) const mockToken = __ENV.API_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // 1. 初始化 Client(已內建 Token 與 Session 管理) const client = new HttpClient(baseUrl, mockToken); // ------------------------------------------ // 範例 A: GET 請求(帶入額外的彈性 Header) // ------------------------------------------ const getRes = client.get('/posts/1', { 'X-Custom-Trace-Id': 'trace-123456', // 彈性擴充自訂 Header }); // 驗證 GET 結果 check(getRes, { 'GET Status is 200': (r) => r.status === 200, 'GET Body has id': (r) => r.json().id === 1, }); // ------------------------------------------ // 範例 B: POST 請求 (新增資料) // ------------------------------------------ const newPostData = { title: 'k6 Performance Testing', body: 'Automated test using custom client class.', userId: 1, }; const postRes = client.post('/posts', newPostData); // 驗證 POST 結果 check(postRes, { 'POST Status is 201': (r) => r.status === 201, 'POST Body contains title': (r) => r.json().title === newPostData.title, }); // ------------------------------------------ // 範例 C: PUT 請求 (更新資料) // ------------------------------------------ const updatePostData = { id: 1, title: 'Updated Title', body: 'Updated Body Content', userId: 1, }; const putRes = client.put('/posts/1', updatePostData); // 驗證 PUT 結果 check(putRes, { 'PUT Status is 200': (r) => r.status === 200, }); // ------------------------------------------ // 範例 D: DELETE 請求 (刪除資料) // ------------------------------------------ const deleteRes = client.delete('/posts/1'); // 驗證 DELETE 結果 check(deleteRes, { 'DELETE Status is 200': (r) => r.status === 200, }); // 每個 VU 執行完一輪後暫停 1 秒,模擬真實使用者停頓 sleep(1); } ``` ### 執行指令範例 #### 🔹 情況 A:完全不給參數(直接使用預設值) ```bash k6 run script.js ``` #### 🔹 情況 B:只修改人數與目標網址(針對 QA 環境) ```bash k6 run -e BASE_URL=https://qa-api.example.com -e VUS=20 script.js ``` #### 🔹 情況 C:高強度壓測與調整檢驗標準(針對 Prod 環境) ```bash k6 run -e BASE_URL=https://api.example.com \ -e VUS=100 \ -e RAMP_TIME=30s \ -e RUN_TIME=5m \ -e MAX_LATENCY=200 \ -e API_KEY=prod-secret-token \ script.js ``` ### 看懂報告 執行結束後,k6 會印出強大的統計報告,重點欄位如下: ```text ✓ Status is 200 ✓ Response time < 200ms ✓ http_req_duration..............: avg=120.5ms min=90ms med=110ms max=350ms p(95)=210ms ✓ http_req_failed................: 0.00% ✓ 0 ✗ 450 http_reqs......................: 450 11.25/s vus............................: 1 min=1 max=20 ``` #### 📊 關鍵指標說明: * **`http_req_duration`**:API 響應時間(包含 `avg` 平均、`p(95)` 前 95% 人的回應時間)。 * **`http_req_failed`**:請求失敗率。 * **`http_reqs`**:總共發送了多少次 HTTP 請求以及 **RPS (每秒請求數 count/s)**。 * **`checks`**:斷言檢查點的成功率。 --- ## 🔸 補充說明 * **k6 內建 CLI 旗標重寫 (CLI Flags Override)**:除了使用 `__ENV` 寫在 JavaScript 裡面外,k6 本身也支援直接透過 CLI 覆蓋 `options`。 例如:`k6 run --vus 50 --duration 2m script.js`。**CLI 旗標的優先權會高於腳本內部的 `options` 物件**。 * **讀取系統原生環境變數**:如果你的作業系統已經有 `export API_KEY=123`,在執行 k6 時只需要加上 `--export-ENV-vars` 或依據作業系統直接執行,k6 也能存取到該變數。 * **`http.CookieJar()`**:相當於瀏覽器的 Session 記憶體,會在該 VU 的請求生命週期中自動儲存並帶上伺服器回傳的 `Set-Cookie`。 * **`Object.assign()`**:用於動態合併物件。在此處是用來將「預設 Header」與「使用者自訂 Header」組合在一起,讓每一個請求都能彈性覆蓋或新增特定的 Header(如 `X-Request-ID` 等)。 # **VU(虛擬使用者數量)** 與 **TPS(每秒請求數/交易數)** 在 k6 中,`stages` 預設控制的是 **VU(虛擬使用者數量)**。 如果你想在不同的 stage 設定 **TPS(每秒請求數/交易數)**,就不能用預設的簡單模型,而是需要使用 k6 的 **Executor(執行器)**。 以下分別介紹 **「設定 VU Stages」** 與 **「設定 TPS Stages」** 的兩種寫法: --- ### 方法一:設定不同 Stage 的 VU(預設 Ramp-up / Ramp-down) 這是最常見的模式,適合用來測試「線上人數漸進式增加」時系統的承載力。 ```javascript import http from 'k6/http'; import { sleep } from 'k6'; export const options = { stages: [ { duration: '1m', target: 20 }, // 階段 1:1 分鐘內,VU 從 0 增加到 20 人 (Ramp-up) { duration: '3m', target: 20 }, // 階段 2:維持 20 人壓測 3 分鐘 (Plateau) { duration: '1m', target: 50 }, // 階段 3:1 分鐘內,VU 從 20 人拉高到 50 人 { duration: '3m', target: 50 }, // 階段 4:維持 50 人壓測 3 分鐘 { duration: '1m', target: 0 }, // 階段 5:1 分鐘內,VU 慢慢歸零 (Ramp-down) ], }; export default function () { http.get('https://test.k6.io'); sleep(1); } ``` > **注意**:這種模式下,**TPS 是無法精確說明的**,因為 TPS 會隨著 API 的回應速度變化(API 回應越快,TPS 越高)。 --- ### 方法二:設定不同 Stage 的 TPS(精確控制每秒吞吐量) 若你的情境是「我要模擬第 1 分鐘每秒 100 筆請求,第 2 分鐘拉高到每秒 500 筆請求」,你必須使用 `ramping-arrival-rate` 這個 Executor。 此模式下,**k6 會自動增減所需的 VU 數量**來強行達到你設定的 RPS/TPS 目標! ```javascript import http from 'k6/http'; export const options = { scenarios: { my_tps_scenario: { executor: 'ramping-arrival-rate', // 使用漸進式到達率執行器 startRate: 10, // 測試一開始的 TPS (10 req/s) timeUnit: '1s', // 時間單位:秒 (即 TPS) preAllocatedVUs: 50, // 預先分配的 VU 數 (節省資源) maxVUs: 200, // 若 API 變慢,k6 最多可自動補充到 200 個 VU 來維持 TPS stages: [ { duration: '1m', target: 100 }, // 1 分鐘內,TPS 從 10 爬升到 100 TPS { duration: '3m', target: 100 }, // 維持 100 TPS 壓測 3 分鐘 { duration: '1m', target: 500 }, // 1 分鐘內,TPS 從 100 暴增到 500 TPS { duration: '3m', target: 500 }, // 維持 500 TPS 壓測 3 分鐘 { duration: '30s', target: 0 }, // 30 秒內降至 0 TPS ], }, }, }; export default function () { http.get('https://test.k6.io'); // 注意:在 constant/ramping-arrival-rate 模式下,預設函數內部不需要寫 sleep() } ``` --- ### 兩種模式怎麼選? | 比較項目 | VU-based Stages (`stages`) | TPS-based Stages (`ramping-arrival-rate`) | | --- | --- | --- | | **控制核心** | **固定發起壓力的併發人數** | **固定每秒發送的請求總數** | | **適用情境** | 評估系統能「同時服務多少在線使用者」 | 根據業務指標壓測(如:活動預計 1 秒進來 500 筆訂單) | | **當系統變慢時** | TPS 會**自動下滑**(因為每個人都在卡住等待) | k6 會**自動啟動更多 VU** 強行發送請求,維持設定的 TPS | | **潛在風險** | 無法保證精確的 TPS 量 | 若 `maxVUs` 設定太小或目標 TPS 太高,k6 會跳出 Warning 警告 VU 不夠用 |