BACK
Featured image of post 【K6】壓力測試工具介紹

【K6】壓力測試工具介紹

K6 核心以 Go 開發,不用擔心程式效能(依文件說明,K6 靠單一主機便可產生每秒 30 萬次請求);測試程式則採用 ES6 / JavaScript 語言,讓前端/全端工程師備感親切,進階應用需要對 Module、webpack 有點概念,但單純測試一個 .js 就能搞定,不難上手。官方文件整理得頗詳細,建議花點時間約略讀過再上路。

參考網站
參考網站
參考網站

與老牌工具 JMeter 相比,K6 比較年輕(2017),架構與設計現代化許多,採用 Go + JavaScript,聽起來就比 Java 年輕有活力。K6 沒有如 JMeter 的華麗友善 GUI,測試細節全靠指令參數與程式碼決定。

K6 核心以 Go 開發,不用擔心程式效能(依文件說明,K6 靠單一主機便可產生每秒 30 萬次請求);測試程式則採用 ES6 / JavaScript 語言,讓前端/全端工程師備感親切,進階應用需要對 Module、webpack 有點概念,但單純測試一個 .js 就能搞定,不難上手。官方文件整理得頗詳細,建議花點時間約略讀過再上路,許多東西文件都有寫到,不要瞎查資料胡亂嘗試浪費時間。

先釐清一點,雖然口語上我們很習慣說「壓力測試」或「壓測」,實際上這些相關測試可再細分成:煙霧測試、負載測試、壓力測試、尖峰測試、浸泡測試,下文會再詳細說明。

不過,「壓力測試」是比較通俗常用的說法,所以我會繼續統稱壓力測試,有特定目標的測試再特別區分。


介紹

K6 是用 Go 語言編寫的一種高效能的負載測試工具。具有下面幾個特點:

  • K6 嵌入了 JavaScript runtime,可以使用 JavaScript ES2015、ES6 來編寫指令碼。
  • 強大的 CLI 工具。
  • 使用 Checks 和 Thresholds 可以更加輕鬆的做面向目標的自動化的負載測試。

Checks: 類似斷言功能,但不會真的中斷執行,只是會返回 check 的數據百分比。
Thresholds: 極限值、門檻、閾值,指測試系統性能所預期的通過 or 失敗的標準。


為什麼要用 K6?

他是用 JavaScript 直接進行負載測試(JavaScript 腳本 ➡️ Go 底層運作)

這對專注於寫 JavaScript 的人是一大福音!

以自身公司而言,大多都是 JavaScript 為基底的測試,所以如果團隊中能夠統一一種語言,不管是在測試、開發上,能降地學習成本及提高協做效率。


如何安裝?

MacOS

記得先安裝 Homebrew

1
brew install k6

Windows

方法一:使用 chocolatey 安裝(需先安裝 Chocolatey package manager)

1
choco install k6

方法二:使用 winget 安裝

1
winget install k6

Linux

Debian/Ubuntu

1
2
3
4
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
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

Fedora/CentOS

1
2
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6

了解 K6 能做到哪些負載測試

  1. Smoke Testing 煙霧測試 - 驗證系統在最低硬體配備、正常負載下不會出錯
  2. Load Testing 負載測試(Performance Testing 效能測試) - 取得在一般及尖峰負載下的系統效能數字(用戶數、Throughput、吞吐量(RPS))
  3. Stress Testing 壓力測試 - 測試在高度負載或極端條件下系統的穩定性及可靠度,找出系統極限(請勿在 production 測試)
  4. Spike Testing 尖峰測試 - 故意製造瞬間流量驟升取得系統效能數字(請勿在 production 測試)
  5. Soak Testing 浸泡測試 - 測試系統在長期運作下的穩定性及可靠度

大致上的分類為(低中高流量每間公司定義都不一樣):

低流量中等流量高流量
Smoke Testing 煙霧測試Load Testing 負載測試(Performance Testing 效能測試)Stress Testing 壓力測試
Soak Testing 浸泡測試Spike Testing 尖峰測試

小試身手

在 Windows 安裝很容易,我是用 Chocolatey choco install -y k6 兩分鐘搞定。寫幾行程式存成 script.js,再執行 k6 run script.js 便做能完簡單測試,得到平均回應時間(http_req_duration)Throughput (http_reqs)

script.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import http from "k6/http";
import { sleep } from "k6";

export const options = {
  vus: 1, // user count
  duration: '1s'
};

export default function () {
  http.get("https://test.k6.io");
};

執行指令:

1
k6 run script.js


參數設定

K6 提供的參數很多,以下主要介紹幾個較常用的為主:

vus

虛擬用戶的數量,最少必須要 1 個,與 duration 搭配使用。

duration

指定測試運行的總持續時間,與 vus 一起使用。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
  vus: 1,
  duration: '1s',
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}
CLI:
1
k6 run --vus 1 --duration 1s script.js

iterations

腳本中的函數被執行的次數。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
  vus: 10,
  duration: '5s',
  iterations: 50
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}
CLI:
1
k6 run --vus 10 --duration 5s --iterations 50 script.js

表示「模擬 10 個用戶,此次腳本最多就是執行 50 次,如果 50 次腳本沒執行完,時間卻到了,那此次測試會顯示 default ✗」。

Stages

可以指定在特定時間內增加或減少用戶數量的執行方式,也就是說可以模仿更精準的測試情境。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
  stages: [
    { duration: '30s', target: 10 },
    { duration: '1m30s', target: 30 },
    { duration: '20s', target: 0 },
  ],
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}
CLI:
1
k6 run --stage 30s:10, -s 1m30s:30, -s 20s:0 .\script.js
  • 第一階段就是在 30 秒間逐步把用戶加到 10 個
  • 第二階段就是在一分半內逐步從 10 個用戶追加到 30 個用戶
  • 第三階段是在 20 秒內逐步從 30 個用戶 降到 0 個用戶個用戶

rps

用戶每秒可以發送最大的 request 數量。

其實就是為了更好模擬用戶流量,再測試前須確認好測試範疇,多數平台應該都是在某些時間點流量會增大,但這個流量增大並非是無上限的增大,一定會落在某個數值,只要將數值取出來後並換算。

e.g:
查看數據後,發現這個時段會有 100000 個 request 數量
那我們要測試的是 60 秒能要達到 100000
那就是 100000/60 = 1666
1666 出來後,我們都會多估 10~20% 以防萬一,所以總計大約 2000 即是 rps 的數量

若要更準確的符合真實數據,也可以以下這樣計算:

1
2
3
4
5
用戶量 100 vus
執行時間 60s
request 條件是 100000
那就是一位 user rps 就是 100000/100/60 = 16
通常 rps 的範疇會多估 10-20% 以防萬一,所以湊個整數 20 就是較符合的數值
sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
  vus: 100,
  duration: '60s',
  rps: 20
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}

group

主要就是將按照功能進行組裝在同一個測試腳本。

可以針對測試方法做調整,通常在測試 API 時可能是一個行為,但是是會多個 API 連續調用的情況。

這時候可以使用 group 方法來實現,以下是官網的例子。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { group } from 'k6';

export default function () {
  group('user flow: returning user', function () {
    group('visit homepage', function () {
      // load homepage resources
    });
    group('login', function () {
      // perform login
    });
  });
}

如上方例子,可以將整個登入流程加入一個群組,測試結果會多一個指標 group_duration,就可方便得知這一個 group 平均執行時間。

另外,官方也寫到:
Discouraged: one group per request
Wrapping each request within a group might add unnecessary boilerplate.

意味著這個 group 功能不建議僅拿來組裝單一 API,它不是一個像單回傳 api module 般的使用。

這樣就失去 group 功能真正的涵義了。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { group, check } from 'k6';
import http from 'k6/http';

const id = 5;

// reconsider this type of code
group('get post', function () {
  http.get(`http://example.com/posts/${id}`);
});
group('list posts', function () {
  const res = http.get(`http://example.com/posts`);
  check(res, {
    'is status 200': (r) => r.status === 200,
  });
});

scenarios

顧名思義,就是測試的情境的參數。

在做效能測試前,通常都會先擬定好要測試的情境,蒐集真實數據進行評估,再進一步評估預期達到的目標再哪。

所以可以先將一些常用的測試寫好。只要引用一下,設定一下立刻可以達到我們想要測試的結果。

只要針對每個情境實作好了一些 Executors,最後只要呼叫它即可。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import http from 'k6/http';

export let options = {
  discardResponseBodies: true,
  scenarios: {
    contacts: {
      executor: 'constant-arrival-rate',
      rate: 200, // 200 RPS, since timeUnit is the default 1s
      duration: '1m',
      preAllocatedVUs: 50,
      maxVUs: 100,
    },
  },
};

export default function () {
  http.get('https://test.k6.io/contacts.php');
}

使用這個範例,K6 會盡所能在 duration 內去達到 200 RPS。

所以只要使用這個方法,可以很快找到你 API 的極限瓶頸在哪。

group vs scenarios

  • group: 通常一套業務邏輯且會被重複使用
  • scenarios: 純粹一個情境一個故事,為了要讓單一邏輯或 group 能夠被相互組合應用,需要多種不同的參數,用戶數量(vus)、持續時間(duration)等等,以便達到該情境的預期結果。

常看的指標

Http 系列

  • http_req_receiving: API 從 server 回應 所花費的時間
  • http_req_sending: API 發送 到 server 所花費的時間
  • http_req_waiting: API 從 server 等待回應 所花費的時間
  • http_req_duration: API 請求的總時間(http_req_sending + http_req_waiting + http_req_receiving)
  • http_req_failed: API 失敗的機率(預設 status code 是 200,若有特別判斷可使用 expectedStatuses)
  • iteration_duration: 該腳本執行一次所花費的時間
  • http_reqs: 會計算測試總共發了多少 request

上述的參數,主要可以分析整個測試的過程,同時也可以了解該 API 的詳細資料針對響應速度、等待回應時間等等,好讓相關團隊可以進行優化分析。

http_req_receiving、http_req_sending、http_req_waiting 三者關係圖

Checks

其實就是類似斷言。僅會返回通過(pass)失敗(fail)

但這邊的斷言是,不論成功或失敗,腳本皆仍會繼續執行。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export default function () {
  const res = http.get('http://test.k6.io/');
  check(res, {
    'is status 200': (r) => r.status === 200,
  });
}

// or

export default function () {
  const res = http.get('http://test.k6.io/');
  check(res, {
    'is status 200': (r) => r.status === 200,
    'body size is 11,105 bytes': (r) => r.body.length == 11105,
  });
}

但如果要需要特別處理錯誤情境時,可以使用 Thresholds

Thresholds

強烈建議多參考官方文檔,寫得非常清楚。

主要是可以為測試的結果自訂一個標準限制,中文稱為門檻值

可以針對各個指標限制於某個條件,相對來說會更彈性,測試會更為準確。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import http from 'k6/http';

export const options = {
  thresholds: {
    http_req_failed: ['rate<0.01'], // http request 失敗的總比例要低於 1% 才能算通過
    http_req_duration: ['p(95)<200'], // 95% 的 requests 數回傳時間都要低於 200ms 以內才算通過
  },
};

export default function () {
  http.get('https://test-api.k6.io/public/crocodiles/1/');
}

其中 http_req_durationp(95) 高於 200ms 了,所以它就會顯示 some thresholds have failed

Threshold 其實與 Check 有點類似,但差別在於:

  • Check 設定比較單一 且 執行失敗的話,腳本仍會繼續執行,直到腳本結束,才會返回 Check 的數據值。
  • Threshold 可以彈性設定條件 且 執行失敗的話,它還可以自行設置中斷點,讓執行中腳本達到某條件的話,就直接中斷並且返回錯誤資訊。
aborting sample code(可自行設定中斷點 sample code):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export const options = {
  vus: 30,
  duration: '2m',
  thresholds: {
    http_req_duration: [{ threshold: 'p(90) < 400', abortOnFail: true }] //90% 的 requests 數回傳時間都要低於 400ms 以內才算通過,只要一旦高於 400ms 就會直接中斷測試
  }
};

export default function () {
  http.get('https://test-api.k6.io/public/crocodiles/1/');
}

其中 http_req_durationp(95) 高於 200ms 了,所以它就會顯示 some thresholds have failed


常見的套件(module)

K6 套件(module) 基本上我覺得很萬用,是足夠支撐所有測試情境的,也主要介紹幾個較常用的 K6 本身支援的 module (JavaScript API)

http

主要是能發送 API,常使用為 get、post、put、delete…等。

GET sample code:
1
2
3
4
5
6
7
8
9
import http from 'k6/http';
export const options = {
  vus: 1,
  duration: '1s',
};

export default function () {
  http.get('https://test.k6.io');
}
POST sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import http from 'k6/http';

const url = 'https://httpbin.test.k6.io/post';
const logoBin = open('./logo.png', 'b');
export default function () {
  let data = { name: 'Bert' };

  // Using a JSON string as body
  let res = http.post(url, JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' },
  });
  console.log(res.json().json.name); // Bert

  // Using an object as body, the headers will automatically include
  // 'Content-Type: application/x-www-form-urlencoded'.
  res = http.post(url, data);
  console.log(res.json().form.name); // Bert

  // Using a binary array as body. Make sure to open() the file as binary
  // (with the 'b' argument).
  http.post(url, logoBin, { headers: { 'Content-Type': 'image/png' } });

  // Using an ArrayBuffer as body. Make sure to pass the underlying ArrayBuffer
  // instance to http.post(), and not the TypedArray view.
  data = new Uint8Array([104, 101, 108, 108, 111]);
  http.post(url, data.buffer, { headers: { 'Content-Type': 'image/png' } });
}

html

解析 HTML 各元素,可能會需要取得 HTML 上的一些元素值做一些處理或換算。

sample code:
1
2
3
4
5
6
7
8
import http from 'k6/http';

export default function () {
  const res = http.get('https://k6.io');
  const doc = parseHTML(res.body); // equivalent to res.html()
  const pageTitle = doc.find('head title').text();
  const langAttr = doc.find('html').attr('lang');
}

sleep

主要就是等待時間,有些時候使用 sleep 是為了更好模擬使用者情境。

因為使用者真實操作 web 可能都會遇到有 loading 渲染的時間,所以我們將 sleep 算進來的話就當作是再重現這部分。

sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
  vus: 1,
  duration: '1s',
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}

crypto

這一定也不陌生,非常強大的加密 module ,支援多種不同的加密方式如下:

  • hmac
  • md4
  • md5
  • sha256
  • sha384
  • sha512

通常是為了確保用戶資料安全而特別處理的。e.g. 密碼、API header 等。

sample code:
1
2
3
4
5
6
7
8
9
import crypto from 'k6/crypto';

export default function () {
  console.log(crypto.sha256('hello world!', 'hex'));
  const hasher = crypto.createHash('sha256');
  hasher.update('hello ');
  hasher.update('world!');
  console.log(hasher.digest('hex'));
}
respone:
1
INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9

encoding

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { check } from 'k6';
import encoding from 'k6/encoding';

export default function () {
  const str = 'hello world';
  const enc = 'aGVsbG8gd29ybGQ=';
  const buf = new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]).buffer;
  check(null, {
    'is encoding string correct': () => encoding.b64encode(str) === enc,
    'is encoding ArrayBuffer correct': () => encoding.b64encode(buf) === enc,
  });
}

JSlib

K6 本身支援蠻多受用的套件,但除此之外,他們還有支援一個是 JSlib 顧名思義就是 JS 庫。

我覺得可以把它當作是外掛包因為其中還支援的很多額外的 module 可用。

  • httpx: 是將 k6/http 原本套件加以簡化處理,可以當成是優化版的 http
  • k6chaijs: 適用於 BDD 和 TDD 斷言風格
k6chaijs sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.1/index.js';
import http from 'k6/http';

export const options = {
  thresholds: {
    checks: [{ threshold: 'rate == 1.00' }], // fail test on any expect() failure
  },
};

export default function testSuite() {
  describe('Basic API test', () => {
    const response = http.get('https://test-api.k6.io/public/crocodiles');
    expect(response.status, 'API status code').to.equal(200);
  });
}
  • utils: K6 的小工具包
utils sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { sleep } from 'k6';
import http from 'k6/http';

import {
  randomIntBetween,
  randomString,
  randomItem,
  uuidv4,
  findBetween,
} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export default function () {
  const res = http.post(`https://test-api.k6.io/user/register/`, {
    first_name: randomItem(['Joe', 'Jane']), // random name
    last_name: `Jon${randomString(1, 'aeiou')}s`, //random character from given list
    username: `user_${randomString(10)}@example.com`, // random email address,
    password: uuidv4(), // random password in form of uuid
  });

  // find a string between two strings to grab the username:
  const username = findBetween(res.body, '"username":"', '"');
  console.log('username from response: ' + username);

  sleep(randomIntBetween(1, 5)); // sleep between 1 and 5 seconds.
}
  • aws: 主要是可以直接串接 AWS 上的 API 服務。

總結

k6 官方提供的文檔其實都非常好懂,甚至都會附上很多程式碼範本讓你直接使用。

基本上只要會 javascript,你就可能無痛起手了。

另外 k6 這框架大多使用的情境是以下:

適用人員描述
開發人員、SDET方便我們透過 K6 提供的 api 以及 CLI 工具來使用開發且開發人員同常對 javascript 也不太陌生,他們也能快速使用 javascript 來開發模擬真實場景的負載測試
DevOps、SRE這兩個職位都是偏運維方面的,我們能把上面 SDET 開發的 script 拿來進行自動化的壓力測試,確保我們的基礎建設與應用服務都還是保持著高性能的表現。在 K6 開發的 script 內,設定 SLO 來測試服務的運行狀況是否達標
QA更方便的寫測試案例與腳本,跑起來也很快,還能跟 Postman、Swagger 等整合,對 QA 來說是很方便的

comments powered by Disqus