アプリケーションは開発や運用を続けていく中で、徐々にパフォーマンスが低下します。機能が追加されていったり、データが増える中で、ユーザーに快適な体験を提供し続けるためには、定期的に負荷テストを実施し、パフォーマンスの劣化を検知することが重要です。
そうしたパフォーマンスの測定に使えるツールとして、k6があります。k6はオープンソースの負荷テストツールで、HTTP/HTTPSだけでなく、gRPCやWebSocketなどもサポートしています。また、ブラウザーベースの負荷テストも可能です。
この記事では、k6の基本的な使い方を紹介します。
k6のインストール
k6は、OSによってインストール方法が異なります。
Windowsの場合
Windowsの場合、公式のインストーラーが提供されています。この他、Chocolateyや Windows Package Managerを使ってインストールすることも可能です。
# Chocolateyの場合
choco install k6
# Windows Package Managerの場合
winget install k6 --source winget
macOSの場合
macOSの場合、Homebrewを使ってインストールするのが簡単です。
brew install k6
Linuxの場合
Linuxの場合、公式のAPTリポジトリやYUMリポジトリを使ってインストールできます。例えば、Ubuntuの場合は以下のようにします。
sudo gpg -k
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の場合は以下のようにします。
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
Dockerの場合
k6はDockerイメージも提供されています。Dockerを使っている場合は、以下のようにしてk6を実行できます。
docker pull grafana/k6
もしブラウザも利用する場合には、以下のイメージを利用します。
docker pull grafana/k6:master-with-browser
k6の基本的な使い方
k6はJavaScriptでテストスクリプトを記述します。まず、以下のようなコード script.js を記述します。
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
iterations: 10,
};
export default function () {
http.get('https://quickpizza.grafana.com');
sleep(1);
}
このコードは、 https://quickpizza.grafana.com に対して10回のHTTP GETリクエストを送信し、各リクエストの後に1秒間のスリープを行います。
実行する際には、 k6 run コマンドを使います。結果は以下のように表示されます。
% k6 run script.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
█ TOTAL RESULTS
HTTP
http_req_duration..............: avg=250.75ms min=171.87ms med=187.06ms max=629.81ms p(90)=476.27ms p(95)=527.11ms
{ expected_response:true }...: avg=250.75ms min=171.87ms med=187.06ms max=629.81ms p(90)=476.27ms p(95)=527.11ms
http_req_failed................: 0.00% 0 out of 230
http_reqs......................: 230 7.48289/s
EXECUTION
iteration_duration.............: avg=1.31s min=1.17s med=1.18s max=2.62s p(90)=1.52s p(95)=1.63s
iterations.....................: 230 7.48289/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
NETWORK
data_received..................: 786 kB 26 kB/s
data_sent......................: 17 kB 566 B/s
running (0m30.7s), 00/10 VUs, 230 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 30s
ブラウザを使った負荷テスト
ブラウザを使ったテストも書けます。k6では、Chromiumベースのブラウザを使って、実際のユーザーが行うような操作をシミュレートできます。以下は、ブラウザを使ったテストスクリプトの例です。
まずテンプレートを使って、新しいスクリプトを作成します。
k6 new --template browser browser-script.js
内容は以下のようになっています。
import http from "k6/http";
import exec from 'k6/execution';
import { browser } from "k6/browser";
import { sleep, fail } from 'k6';
import { expect } from "https://jslib.k6.io/k6-testing/0.5.0/index.js";
const BASE_URL = __ENV.BASE_URL || "https://quickpizza.grafana.com";
export const options = {
scenarios: {
ui: {
executor: "shared-iterations",
vus: 1,
iterations: 1,
options: {
browser: {
type: "chromium",
},
},
},
},
};
export function setup() {
let res = http.get(BASE_URL);
expect(res.status, Got unexpected status code ${res.status} when trying to setup. Exiting.).toBe(200);
}
export default async function() {
let checkData;
const page = await browser.newPage();
try {
await page.goto(BASE_URL);
await expect.soft(page.locator("h1")).toHaveText("Looking to break out of your pizza routine?");
await page.locator('//button[. = "Pizza, Please!"]').click();
await page.waitForTimeout(500);
await page.screenshot({ path: "screenshot.png" });
await expect.soft(page.locator("div#recommendations")).not.toHaveText("");
} catch (error) {
fail(Browser iteration failed: ${error.message});
} finally {
await page.close();
}
sleep(1);
}
処理内容としては、 https://quickpizza.grafana.com にアクセスし、ページ内の特定の要素を検証し、ボタンをクリックしてからスクリーンショットを保存します。
実行すると、以下のような結果が得られます。
% k6 run browser-script.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: browser-script.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
█ TOTAL RESULTS
HTTP
http_req_duration..............: avg=450.95ms min=450.95ms med=450.95ms max=450.95ms p(90)=450.95ms p(95)=450.95ms
{ expected_response:true }...: avg=450.95ms min=450.95ms med=450.95ms max=450.95ms p(90)=450.95ms p(95)=450.95ms
http_req_failed................: 0.00% 0 out of 1
http_reqs......................: 1 0.081764/s
EXECUTION
iteration_duration.............: avg=5.82s min=5.82s med=5.82s max=5.82s p(90)=5.82s p(95)=5.82s
iterations.....................: 1 0.081764/s
vus............................: 1 min=0 max=1
vus_max........................: 1 min=1 max=1
NETWORK
data_received..................: 7.2 kB 590 B/s
data_sent......................: 563 B 46 B/s
BROWSER
browser_data_received..........: 339 kB 28 kB/s
browser_data_sent..............: 7.1 kB 583 B/s
browser_http_req_duration......: avg=996.01ms min=180.12ms med=916.58ms max=1.63s p(90)=1.62s p(95)=1.62s
browser_http_req_failed........: 0.00% 0 out of 25
WEB_VITALS
browser_web_vital_cls..........: avg=0.031895 min=0.031895 med=0.031895 max=0.031895 p(90)=0.031895 p(95)=0.031895
browser_web_vital_fcp..........: avg=3.49s min=3.49s med=3.49s max=3.49s p(90)=3.49s p(95)=3.49s
browser_web_vital_fid..........: avg=199.99µs min=199.99µs med=199.99µs max=199.99µs p(90)=199.99µs p(95)=199.99µs
browser_web_vital_inp..........: avg=16ms min=16ms med=16ms max=16ms p(90)=16ms p(95)=16ms
browser_web_vital_ttfb.........: avg=988ms min=988ms med=988ms max=988ms p(90)=988ms p(95)=988ms
running (00m12.2s), 0/1 VUs, 1 complete and 0 interrupted iterations
ui ✓ [======================================] 1 VUs 00m10.6s/10m0s 1/1 shared iters
画像も保存されています。
APIテストの例
k6では、APIテストも簡単に記述できます。以下は、JSON/gRPC/WebSocketの各種APIテストの例です。
JSON APIテストの例
JSONリクエストを実行するようなAPIテストも可能です。
import http from 'k6/http';
export default function () {
const payload = JSON.stringify({
name: 'lorem',
surname: 'ipsum',
});
const headers = { 'Content-Type': 'application/json' };
http.post('https://quickpizza.grafana.com/api/post', payload, { headers });
}
これを実行した結果です。
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: api-script.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
█ TOTAL RESULTS
HTTP
http_req_duration..............: avg=180.2ms min=180.2ms med=180.2ms max=180.2ms p(90)=180.2ms p(95)=180.2ms
{ expected_response:true }...: avg=180.2ms min=180.2ms med=180.2ms max=180.2ms p(90)=180.2ms p(95)=180.2ms
http_req_failed................: 0.00% 0 out of 1
http_reqs......................: 1 0.650015/s
EXECUTION
iteration_duration.............: avg=1.53s min=1.53s med=1.53s max=1.53s p(90)=1.53s p(95)=1.53s
iterations.....................: 1 0.650015/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
NETWORK
data_received..................: 4.5 kB 2.9 kB/s
data_sent......................: 653 B 425 B/s
running (00m01.5s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m01.5s/10m0s 1/1 iters, 1 per VU
gRPCテストの例
以下はgRPCの例です。
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';
// Download quickpizza.proto for grpc-quickpizza.grafana.com, located at:
// https://raw.githubusercontent.com/grafana/quickpizza/refs/heads/main/proto/quickpizza.proto
// and put it in the same folder as this script.
const client = new grpc.Client();
client.load(null, 'quickpizza.proto');
export default () => {
client.connect('grpc-quickpizza.grafana.com:443', {
// plaintext: false
});
const data = { ingredients: ['Cheese'], dough: 'Thick' };
const response = client.invoke('quickpizza.GRPC/RatePizza', data);
check(response, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
});
console.log(JSON.stringify(response.message));
client.close();
sleep(1);
};
実行結果です。
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: grpc-script.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
INFO[0001] {"starsRating":2} source=console
█ TOTAL RESULTS
checks_total.......: 1 0.398957/s
checks_succeeded...: 100.00% 1 out of 1
checks_failed......: 0.00% 0 out of 1
✓ status is OK
EXECUTION
iteration_duration...: avg=2.5s min=2.5s med=2.5s max=2.5s p(90)=2.5s p(95)=2.5s
iterations...........: 1 0.398957/s
vus..................: 1 min=1 max=1
vus_max..............: 1 min=1 max=1
NETWORK
data_received........: 4.1 kB 1.6 kB/s
data_sent............: 814 B 325 B/s
GRPC
grpc_req_duration....: avg=180.74ms min=180.74ms med=180.74ms max=180.74ms p(90)=180.74ms p(95)=180.74ms
running (00m02.5s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m02.5s/10m0s 1/1 iters, 1 per VU
WebSocketテストの例
WebSocketの例です。
import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import ws from 'k6/ws';
import { check, sleep } from 'k6';
const sessionDuration = randomIntBetween(3000, 6000); // user session between 3s and 6s
export const options = {
vus: 10,
iterations: 10,
};
export default function () {
const url = wss://quickpizza.grafana.com/ws;
const params = { tags: { my_tag: 'my ws session' } };
const user = user_${__VU};
const res = ws.connect(url, params, function (socket) {
socket.on('open', function open() {
console.log(VU ${__VU}: connected);
socket.send(JSON.stringify({ msg: 'Hello!', user: user }));
socket.setInterval(function timeout() {
socket.send(
JSON.stringify({
user: user,
msg: I'm saying ${randomString(5)},
foo: 'bar',
})
);
}, randomIntBetween(1000, 2000)); // say something every 1-2 seconds
});
socket.on('ping', function () {
console.log('PING!');
});
socket.on('pong', function () {
console.log('PONG!');
});
socket.on('close', function () {
console.log(VU ${__VU}: disconnected);
});
socket.on('message', function (message) {
const data = JSON.parse(message);
console.log(VU ${__VU} received message: ${data.msg});
});
socket.setTimeout(function () {
console.log(VU ${__VU}: ${sessionDuration}ms passed, leaving the website);
socket.send(JSON.stringify({ msg: 'Goodbye!', user: user }));
}, sessionDuration);
socket.setTimeout(function () {
console.log(Closing the socket forcefully 3s after graceful LEAVE);
socket.close();
}, sessionDuration + 3000);
});
check(res, { 'Connected successfully': (r) => r && r.status === 101 });
sleep(1);
}
実行結果です。WebSocket接続が確立され、メッセージの送受信が行われていることがわかります。メッセージは大量なので、一部を省略しています。
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: ws-script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 10m30s max duration (incl. graceful stop):
* default: 10 iterations shared among 10 VUs (maxDuration: 10m0s, gracefulStop: 30s)
INFO[0002] VU 3: connected source=console
:
INFO[0010] VU 3: disconnected source=console
█ TOTAL RESULTS
checks_total.......: 10 0.86777/s
checks_succeeded...: 100.00% 10 out of 10
checks_failed......: 0.00% 0 out of 10
✓ Connected successfully
EXECUTION
iteration_duration....: avg=10.37s min=8.99s med=10.26s max=11.52s p(90)=11.31s p(95)=11.41s
iterations............: 10 0.86777/s
vus...................: 3 min=3 max=10
vus_max...............: 10 min=10 max=10
NETWORK
data_received.........: 57 kB 4.9 kB/s
data_sent.............: 11 kB 989 B/s
WEBSOCKET
ws_connecting.........: avg=1.67s min=1.67s med=1.67s max=1.67s p(90)=1.67s p(95)=1.67s
ws_msgs_received......: 156 13.537209/s
ws_msgs_sent..........: 66 5.727281/s
ws_session_duration...: avg=9.37s min=7.99s med=9.26s max=10.52s p(90)=10.3s p(95)=10.41s
ws_sessions...........: 10 0.86777/s
running (00m11.5s), 00/10 VUs, 10 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 00m11.5s/10m0s 10/10 shared iters
HTMLパースの例
HTMLをパースして、テストも可能です。
import { parseHTML } from 'k6/html';
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');
}
Azure Active Directoryを使ったOAuth認証の例
さらに複雑な例として、Azure Active Directoryを使ったOAuth認証の例を示します。
import http from 'k6/http';
/**
* Authenticate using OAuth against Azure Active Directory
* @function
* @param {string} tenantId - Directory ID in Azure
* @param {string} clientId - Application ID in Azure
* @param {string} clientSecret - Can be obtained from https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app#create-a-client-secret
* @param {string} scope - Space-separated list of scopes (permissions) that are already given consent to by admin
* @param {string} resource - Either a resource ID (as string) or an object containing username and password
*/
export function authenticateUsingAzure(tenantId, clientId, clientSecret, scope, resource) {
let url;
const requestBody = {
client_id: clientId,
client_secret: clientSecret,
scope: scope,
};
if (typeof resource == 'string') {
url = https://login.microsoftonline.com/${tenantId}/oauth2/token;
requestBody['grant_type'] = 'client_credentials';
requestBody['resource'] = resource;
} else if (
typeof resource == 'object' &&
resource.hasOwnProperty('username') &&
resource.hasOwnProperty('password')
) {
url = https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token;
requestBody['grant_type'] = 'password';
requestBody['username'] = resource.username;
requestBody['password'] = resource.password;
} else {
throw 'resource should be either a string or an object containing username and password';
}
const response = http.post(url, requestBody);
return response.json();
}
まとめ
k6は、シンプルで使いやすい負荷テストツールであり、様々なプロトコルやシナリオに対応しています。実行を繰り返したり、要素を中酒したテストなども可能です。単純なネットワークアクセスはもちろん、ヘッドレスブラウザを使ったアクセスも可能なので、実際のユーザー行動に近い形での負荷テストも実現できます。
ぜひk6を活用して、効果的な負荷テストを行ってください。
