負荷テストとは何か
- 測定対象のリソースに対して、機械的にHttpリクエストやWebSocket通信などを行い負荷をかけ、パフォーマンスを計測するテスト
- 仮想ユーザー数やリクエスト数、実行時間などの要素を増減させ、その時のシステムの応答性と安定性を計測する(システムの限界点と許容限度がわかる)
負荷テストのポイント
目的を明確化する
同時接続〇〇万人で95パーセンタイルが300ms未満をクリアできるかどうかや、どれくらいの負荷をかけるとサーバーリソースがエラーを返す(限界点に達する)のか等
パフォーマンスに関する重要な指標
レイテンシ
リクエストを送ってからレスポンスが帰ってくるまでの時間。ネットワーク経由でデータが送信されるのにかかる時間。一般的には数百ミリ秒未満であることが好ましい
スループット
特定の時間にネットワークを実際に通過できるデータの平均量=リクエスト量。ビット/秒(bps)、バイト/秒(Bps)で表現される
重要なリソース指標
- コンピューティングリソースの使用率(CPU、メモリ)
- データベースへのクエリ数、実行時間、ロック数、待機時間
- ネットワークI/O
- ディスクI/O(ハードディスクなどの外部記憶装置に対するデータの読み書き操作)
- エラーが発生した場合、その発生頻度、エラーの割合
計測値
平均値、最大値、中央値などよりもパーセンタイルで計測することが好ましい(計測値の上限/下限の数%には異常値が含まれる場合があるが、これを無視できるメリットがある)
負荷テストの分類と役割
負荷テストは一般的にそのシュチュエーションにより以下のように分類できる
- 予備テスト
- 再小規模で十分なパフォーマンスが得られるか
- パフォーマンステスト
- 通常時に十分なパフォーマンスが得られるか
- ロードテスト
- ピーク時に十分なパフォーマンスが得られるか
- 限界値テスト
- システム限界点を把握する
- 限界を超えた場合にどのような現象が発生するか
シナリオ作成時のポイント
ユーザーの現実的な利用シーン、操作に則したシナリオを作成することが重要。ユースケースに沿ってシナリオを作成し、その中でデータの受信と送信、次のページに移るまでのユーザーの思考遅延などをsleepなどで指定することにでより意味のあるテストに近づく
主要な負荷テストツール
- k6(GO製、JavaScriptでシナリオを書ける
- locust(Pythonでシナリオを書ける
- Apache JMeter(Javaでシナリオを書ける
k6
読み方はケーシックス。HTTP,WebSocketに対応。
インストール
mac
brew install k6
mac以外はこちらを参照
GETリクエスト
- テストファイルにエントリーポイントを表すデフォルト関数を少なくとも1つ含める必要がある
- VU(仮想ユーザー)は、すべての条件が満たされるまで、デフォルト関数内のコードを最初から最後まで順次実行し、最後に到達すると、最初から同じ処理を繰り返す
http/index.js
// 指定したulrにリクエストを送信する
import http from "k6/http";
import { check } from "k6";
export default function () {
const res = http.get("https://httpbin.test.k6.io/");
check(res, { "status was 200": (r) => r.status == 200 });
}
POSTリクエスト
postメソッドの第二引数にペイロードを、第三引数にリクエストヘッダーを指定できる
import http from 'k6/http';
export default function () {
const url = 'http://example.com/login';
const payload = JSON.stringify({
email: 'hoge@gmail.com',
password: 'xfeeajepjege',
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
http.post(url, payload, params);
}
テスト実行
k6 run http/index.js
出力結果
k6 run http/index.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: http/index.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)
✓ status was 200
checks.........................: 100.00% ✓ 1 ✗ 0
data_received..................: 15 kB 27 kB/s
data_sent......................: 458 B 817 B/s
http_req_blocked...............: avg=377ms min=377ms med=377ms max=377ms p(90)=377ms p(95)=377ms
http_req_connecting............: avg=184.85ms min=184.85ms med=184.85ms max=184.85ms p(90)=184.85ms p(95)=184.85ms
http_req_duration..............: avg=182.52ms min=182.52ms med=182.52ms max=182.52ms p(90)=182.52ms p(95)=182.52ms
{ expected_response:true }...: avg=182.52ms min=182.52ms med=182.52ms max=182.52ms p(90)=182.52ms p(95)=182.52ms
http_req_failed................: 0.00% ✓ 0 ✗ 1
http_req_receiving.............: avg=331µs min=331µs med=331µs max=331µs p(90)=331µs p(95)=331µs
http_req_sending...............: avg=71µs min=71µs med=71µs max=71µs p(90)=71µs p(95)=71µs
http_req_tls_handshaking.......: avg=190.18ms min=190.18ms med=190.18ms max=190.18ms p(90)=190.18ms p(95)=190.18ms
http_req_waiting...............: avg=182.12ms min=182.12ms med=182.12ms max=182.12ms p(90)=182.12ms p(95)=182.12ms
http_reqs......................: 1 1.7848/s
iteration_duration.............: avg=560.14ms min=560.14ms med=560.14ms max=560.14ms p(90)=560.14ms p(95)=560.14ms
iterations.....................: 1 1.7848/s
running (00m00.6s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m00.6s/10m0s 1/1 iters, 1 per V
特に重要な指標
テスト実行後、コンソールに各実行結果が表示される。特に重要な指標としては以下。その他の指標についてはこちらを参考されたし
指標名 | 内容 |
---|---|
checks | 宣言した check() アサーションのうち、実際に完了した数 |
http_reqs | 送信された HTTP リクエストの総数 (スループット) |
http_req_duration | リクエストを送信してからレスポンスが返ってくるまでの合計時間(レイテンシ) |
iteration_duration | デフォルト関数(シナリオ)の実行にかかった合計時間 |
iterations | デフォルト関数(シナリオ)が呼び出された回数の集計 |
vus | アクティブ仮想ユーザー数 |
iteration_durationはsleep分などで待機時間があると、その分増加する
コンソールに出力される数字
テストの実行中に何かしらの変数なりをconsole.logでログを出力すると以下のように、INFO[000X]のような内容が表示される。
INFO[0003] Connected source=console
INFO[0003] Disconnected source=console
INFO[0003] Response: source=console
INFO[0003] Response: source=console
INFO[0004] Connected source=console
INFO[0004] Disconnected source=console
INFO[0005] Connected source=console
INFO[0005] Disconnected source=console
INFO[0005] Response: source=console
この数字は、シナリオとして作成したdurationとtargetの設定によって決定される
例えば、以下のようなステージ設定だと、最初の2秒経過後に同時実行数が1、その後次の2秒間経過までで2にする
stages: [
{ duration: "2s", target: 1 },
{ duration: "2s", target: 2 },
{ duration: "2s", target: 0 },
この場合、[0003]は2秒経過以降4秒経過までに計1人の仮想ユーザーが実行し、[0005]は4秒経過以降6秒経過までに計2人の仮想ユーザーが実行する。(0秒経過から2秒経過までは仮想ユーザーが0人なので、実行されていない)
INFO[0003] responseBody: 200 OK source=console
INFO[0005] responseBody: 200 OK source=console
INFO[0005] responseBody: 200 OK source=console
オプション
以下のように、実行時にvus(vitual users)の数などをパラメータとして指定できるし、ソースコード上でも定義できる
k6 run --vus 2 http/index.js
options
という名称で変数を定義し、vusやdurationなどのパラメータを指定できる(要export)
並列度(vus)
optionsにvus(vitual users)の数を指定できる
import http from 'k6/http';
export let options = {
vus: 2
};
export default function () {
http.get('http://example.com/foo');
}
startVUs
テスト開始時点の仮想ユーザー数を指定できる
export const options = {
scenarios: {
sample_scenario: {
startVUs: 0,
duration
テストの実行時間を指定できる
import http from 'k6/http';
export let options = {
duration: '10s'
};
export default function () {
http.get('http://example.com/foo');
}
しきい値(threshold)
メトリクスに基づいて、どのような条件下でテストが成功または失敗とみなされるかを設定できる。
export let options = {
stages: [
{ duration: "10s", target: 20 },
{ duration: "10s", target: 10 },
{ duration: "10s", target: 0 },
],
thresholds: {
http_req_duration: ["p(95)<200"], // 全てのリクエストにおいて95パーセンタイルが200msec以下であるか
},
};
Stages
- 仮想ユーザー数をピーク値まで徐々に増やし(ランプアップ) その後、仮想ユーザー数を徐々に減らしていき、テストの実行を終了する(ランプダウン)やり方が一般的
stages
を使用することで段階的にパラメータを変動させることが可能- 挙動としてはステージごとに指定した実行時間、アクティブな仮想ユーザーの数を変えながら可能な限り多くのリクエストを実行する
- 詳細はこちらを参照
// 最初の20秒間は1VUから20VUへ。次の60秒間でゆっくりと40VUに移行。最後の10秒間で40VUから0VUに移行
export let options = {
stages: [
{ duration: '20s', target: 20 },
{ duration: '1m', target: 40 },
{ duration: '10s', target: 0 },
],
};
Scenarios
scenariosを使用することで、上記で紹介したような設定値の内容を1つのシナリオとしてまとめることができる。
https://k6.io/docs/using-k6/k6-options/reference/#scenarios
Executor
k6 Executorが指定することで比較的簡単にはシナリオを定義することが可能。例えばk6 Executorの1つであるramping-vusは、可変数の仮想ユーザーが、指定された時間内にできるだけ多くの反復を実行するというシナリオであり、他にもシナリオ作成をサポートするExecutorが複数存在する
https://k6.io/docs/using-k6/scenarios/executors/
env
Scenariosに環境変数を指定できる
export const options = {
scenarios: {
sample_scenario: {
executor: "ramping-vus",
stages: [
{ duration: "3s", target: 1 },
{ duration: "3s", target: 2 },
{ duration: "3s", target: 0 },
],
env: {
HOSTNAME: "https://sample.com",
},
},
},
summaryTrendStats,
};
定義した環境変数は以下のようにテスト内で使用できる
export default function () {
const endpoint = `${__ENV.HOSTNAME}/foo/bar?hoge=yaa`
...
}
noVUConnectionReuse
テスト実行時にVUの反復間でTCPコネクションを再利用するかどうかを指定することが可能。trueの場合再利用しない
export const options = {
noVUConnectionReuse: true
summaryTrendStats
テスト結果として表示するメトリクスを指定できる
export const options = {
summaryTrendStats: [
"avg",
"min",
"med",
"max",
"p(95)",
"p(99)",
"p(99.99)",
"count",
],
アサート
check
メソッドにより、テスト実行結果のアサートが可能- Httpレスポンスには、bodyやstatusなどが含まれているので、これに対して期待する結果が返却されているかをアサートする
- 一般的なアサートとは異なり、失敗しても負荷テストの実行を中断することはなく、そのまま継続する
export default function () {
const res = http.get("https://hoge.com/foo");
check(res, {
"status was 200": (r) => r.status == 200,
});
}
WebSocket
k6/wsモジュールにより、可能。また、connectしてからdisconnectするまでの一連の処理を実行するようにしてあげないと、no iterationsということで、1つもテストが完了していないと判定されてしまうので注意
export default function () {
const url = `${__ENV.WS_HOSTNAME}/ws/foo/bar/`;
const res = ws.connect(url, {}, function (socket) {
socket.on("open", () => {
console.log("Connected");
socket.send(
JSON.stringify({
token: `${__ENV.TOKEN}`,
action: "something_action",
})
);
});
socket.on("close", () => console.log("Disconnected"));
socket.on("message", (data) => {
console.log("Message received", data);
});
socket.on("error", function (e) {
console.error("An unexpected error occured: ", e.error());
});
});
console.log(`Response: ${res.body}`);
check(res, { "Connected successfully": (r) => r && r.status === 101 });
socket.setTimeout(function () {
console.log("Closing the socket");
socket.close();
}, 5000);
}
分散テスト
k6-operatorにより、Kubernetesを利用した分散テストが実現できる
手順
dockerデスクトップでEnable Kubernetesにチェックをつけ、Kubernetesを有効化する
ソースコードを取得、ルートディレクトリに移動
git clone https://github.com/grafana/k6-operator && cd k6-operator
カスタムリソース定義をクラスターに適用
make install
利用可能なカスタムリソース定義にk6s.k6.ioが追加されているか確認
kubectl get crd
NAME CREATED AT
k6s.k6.io 2023-11-04T11:22:01Z
以下でデプロイすると、クラスター内にコントローラーのサービスアカウントやロールなどに関するいくつかのリソースが作成される
make deploy
configmapの作成。(作成しなくても、config/samples/k6_v1alpha1_configmap.yamlを使用できるが、自前で用意したい場合は以下で作成できる)
kubectl create configmap --from-file
configmapのデプロイ
kubectl apply -f config/samples/k6_v1alpha1_configmap.yaml
利用できるconfigmapを確認し、config/samples/k6_v1alpha1_configmap.yamlのmetadata.nameで指定した名称の項目が追加されていればOK
kubectl get configmap
NAME DATA AGE
k6-test 1 13h
k6-testのconfigmapの詳細を確認。config/samples/k6_v1alpha1_configmap.yamlで定義した内容が表示されていればOK
kubectl describe configmap k6-test
Name: k6-test
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
test.js:
----
import http from 'k6/http';
import { Rate } from 'k6/metrics';
テストの実行
kubectl apply -f config/samples/k6_v1alpha1_k6.yaml
ポッドの一覧で処理の進捗を確認。テストが完了したら、statusがcompletedとなる
kubectl get pods
NAME READY STATUS RESTARTS AGE
k6-sample-1-6qvb7 0/1 Running 0 17s
k6-sample-10-smqcd 0/1 Running 0 16s
k6-sample-2-mwsrf 0/1 Running 0 17s
k6-sample-3-fzm5p 0/1 Running 0 17s
↓
NAME READY STATUS RESTARTS AGE
k6-sample-1-trrjr 0/1 Completed 0 60s
k6-sample-10-tc7dl 0/1 Completed 0 60s
k6-sample-2-szqkg 0/1 Completed 0 60s
k6-sample-3-tc2j6 0/1 Completed 0 60s
・・・
特定のポッドでのテスト実行結果を確認
kubectl logs k6-sample-2-szqkg
✓ http response status code is 200
checks.........................: 100.00% ✓ 520 ✗ 0
data_received..................: 543 kB 9.0 kB/s
data_sent......................: 69 kB 1.1 kB/s
✓ failed_requests................: 0.00% ✓ 0 ✗ 520
http_req_blocked...............: avg=14.55ms min=8.91µs med=30.66µs max=407.39ms p(90)=92.05µs p(95)=186.69µs
・・・
podのクリーンアップ。テストを再実行したい場合はこのコマンドで一度削除してから再度kubectl apply
する
kubectl delete -f config/samples/k6_v1alpha1_k6.yaml
configmapの削除
kubectl delete configmap <namespace-name>
podの一覧を表示
kubectl get pods
オペレータ関連のリソースを全て削除
make delete
まとめ
いかがでしたでしょうか。本記事では負荷テストの概要とk6を用いて負荷テストを実装するための方法やポイントについて紹介しました。大規模なアプリケーションや高パフォーマンスが求められるシステムでは負荷テストは必須のテストになりますのでぜひ参考にしてみてください