こんにちは。よっしーです(^^)
今日は、APIの負荷テストについてご紹介します。
背景
Dockerで構築したWebアプリの開発環境において、k6を利用した負荷テストについて調査したときの内容を備忘として残しました。
開発環境のソースは下記のリポジトリにあります。
検討事項
以前にテストスクリプトを記述したことがある場合、k6スクリプトの実装は馴染みがあるはずです。k6テストはJavaScriptで記述され、k6 APIの設計には他のテストフレームワークと類似点があります。
しかし、他のテストとは異なり、負荷テストはそのスクリプトを何百、何千、何百万回も実行します。負荷をかけることにはいくつかの特定の懸念事項があります。k6を使用してAPIの負荷テストを行う際に、スクリプト設計の以下の側面を考慮してください。
データのパラメータ化
データのパラメータ化は、ハードコードされたテストデータを動的な値で置き換えるときに発生します。パラメータ化により、さまざまなユーザーとAPI呼び出しを持つ負荷テストを管理しやすくなります。パラメータ化の一般的なケースは、各仮想ユーザーやイテレーションごとに異なるユーザーIDやパスワードの値を使用したい場合です。
例えば、ユーザー情報のリストを含むJSONファイルを考えてみてください:
{
"users": [
{ "username": "lorem", "surname": "ipsum" },
{ "username": "dolorem", "surname": "ipsum" },
]
}
SharedArrayオブジェクトを使って、以下のようにユーザーをパラメータ化することができます:
import { check } from 'k6';
import http from 'k6/http';
import { SharedArray } from 'k6/data';
const users = new SharedArray('users.json', function () {
return JSON.parse(open('./users.json')).users;
});
export const options = {};
export default function () {
// now, user data is not the same for all the iterations
const user = users[Math.floor(Math.random() * users.length)];
const payload = JSON.stringify({
name: user.username,
surname: user.surname,
});
const headers = { 'Content-Type': 'application/json' };
const res = http.post('https://httpbin.test.k6.io/post', payload, {
headers,
});
check(res, {
'Post status is 200': (r) => res.status === 200,
'Post Content-Type header': (r) => res.headers['Content-Type'] === 'application/json',
'Post response name': (r) => res.status === 200 && res.json().json.name === user.username,
});
}
エラーハンドリングと失敗の受け入れ
テストロジックにエラーハンドリングを実装することを忘れないでください。十分な負荷下では、システムは失敗し、エラーで応答し始める可能性があります。テストが失敗を誘発するように設計されている場合でも、時には最良のケースシナリオに焦点を当て、エラーを考慮する重要性を忘れがちです。
テストスクリプトはAPIエラーを処理し、実行時の例外を回避し、テスト目標に従ってSUTが過負荷時にどのように振る舞うかをテストすることを確実にする必要があります。例えば、前のリクエストの結果に依存する操作を行うためにスクリプトを拡張することができます:
import { check } from 'k6';
import http from 'k6/http';
import { SharedArray } from 'k6/data';
const users = new SharedArray('users.json', function () {
return JSON.parse(open('./users.json')).users;
});
export const options = {};
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
const payload = JSON.stringify({
name: user.username,
surname: user.surname,
});
const headers = { 'Content-Type': 'application/json' };
const res = http.post('https://httpbin.test.k6.io/post', payload, {
headers,
});
check(res, {
'Post status is 200': (r) => res.status === 200,
'Post Content-Type header': (r) => res.headers['Content-Type'] === 'application/json',
'Post response name': (r) => res.status === 200 && res.json().json.name === user.username,
});
if (res.status === 200) {
// enters only successful responses
// otherwise, it triggers an exception
const delPayload = JSON.stringify({ name: res.json().json.name });
http.patch('https://httpbin.test.k6.io/patch', delPayload, { headers });
}
}
テストの再利用とモジュール化
負荷テストは範囲が広いことがあり、さまざまな種類のテストが必要です。一般的に、チームはシンプルなテストやクリティカルな負荷テストから始め、新しいユースケース、ユーザーフロー、トラフィックパターン、機能、システムなどのテストを追加していくことがあります。
このプロセスでは、負荷テストスイートは時間とともに成長します。繰り返しの作業を最小限に抑えるために、テストスクリプトを早期に再利用し、テストの関数とロジックをモジュール化しましょう。一般的なシナリオを再利用可能なモジュールでスクリプト化すると、さまざまな種類の負荷テストを作成しやすくなります。新しい負荷テストを作成するプロセスは次のようになります。
- 新しいテストファイルを作成します。
- 特定の負荷とその他のオプションを設定します。
- シナリオをインポートします。
テストが成熟するにつれて、複数のシナリオを組み合わせてより多様なトラフィックをシミュレートするテストを作成することも検討してください。
1つのエンドポイント用のダイナミックURL
デフォルトでは、異なるURLで同じAPIエンドポイントにアクセスする場合(例:http://example.com/posts/${id})、k6はエンドポイントの結果を別々に報告します。これは不必要なメトリクスの量を作成する可能性があります。
エンドポイントの結果をグループ化するために、URLのグループ化を使用します。
おわりに
今日は、APIの負荷テストについてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント