こんにちは。みなさんお元気ですか。
わたしは最近ELDEN RING NIGHTREIGNをやっており体調が悪いです。
さて、皆さんはロードテスト(負荷試験)を実施していますか?
私は業務でロードテストを行うことになったので、その経験をまとめてみます。
1. ロードテストの意義
ロードテストを行うことで、主に次のようなことが分かります。
- 対象サービスがどこまで耐えられるか
- サービスのボトルネックの特定
- オートスケーリング環境での挙動確認
例えばECサイトでは、広告出稿やテレビでの紹介、限定商品の発売などにより突発的にアクセスが増えることがあります。
お客さまから受注している場合は、障害が発生すると賠償問題にもつながるため「サービスがどこまで耐えられるか」「負荷増加時にどう振る舞うか」を事前に把握することは非常に重要です。
2. ロードテストに使えるフレームワーク
今回のロードテストでは Artillery を採用しました。
有名どころとしては k6 や Locust、Apache JMeter などがありますが、Artilleryを選んだ理由は以下の2点です。
- Playwrightと互換性がある
- AWS Fargate上での実行をサポートしている
2.1 Playwrightとの互換性
ArtilleryはPlaywrightを実行エンジンとして利用できるため、Playwrightで作ったE2Eシナリオを元にロードテストに転用できます。
シナリオはPlaywrightのTest Generatorで簡単に作成できる点も便利です。(この記事ではTest Generatorを使っていません。)
2.2 AWS Fargateでの実行
ArtilleryはFargate上での実行を標準サポートしているため、自前のAWSアカウントから大規模なロードテストを実施できます。
ローカル環境では端末スペックに依存しがちですが、Fargateを利用することでより高負荷なテストが可能です。課金もテスト実行時間分だけなので比較的リーズナブルです。
3. Artillery(+Playwright)によるロードテスト実施手順
ロードテストは大きく次のステップで進めます。
- Artilleryのインストール
- Playwrightでシナリオを作成
- PlaywrightシナリオをArtillery用に調整
- ロードテスト設定ファイルを追加
- ローカル環境からロードテスト実行
- AWS Fargateからロードテスト実行
以下、詳細なコマンドやコード例を順番に解説します。
3.1 Artilleryのインストール
まずはPlaywrightをインストールします。対話形式の設定は好みに応じて調整してください。
# ロードテスト用のディレクトリへ移動
$ mkdir loadtest
$ cd loadtest
$ npm init playwright@latest
次に、Artillery本体をインストールします。
$ npm install artillery@latest
Artilleryを実行できるか確認します。
$ node_modules/artillery/bin/run dino
------------
< Artillery! >
------------
\
\
.@
@.+
@,
@'
@'
@;
`@;
@+;
.@#;'
#@###@;'.
:#@@@@@;.
@@@+;'@@:
`@@@';;;@@
@;:@@;;;;+#
`@;` ,@@,, @@`
@`@ @`+
@ , @ @
@ @ @ @
これでArtilleryを使える状態になりました。
3.2 Playwrightでシナリオを作成
PlaywrightでE2Eテスト用シナリオを作成します。
今回は以下のような流れを想定します。
- トップページへアクセス
- 記事一覧のリンクからランダムに選択
- 記事詳細画面へアクセス
今回のテストシナリオは `tests/main-playwright.spec.ts` に保存します。大まかには以下のようになります。
対象のURLはローカル環境(localhostの8080番ポート)で実行しているものとします。Fargateから実行する際は、対象のサービスのURLに変更してください。
注意: サンプルです。実際に動作するか試していません。
import { test, expect } from '@playwright/test';
const targetUrl = "http://localhost:8080/";
test.describe('mainScenario', () => {
test('mainScenario', async ({ page }) => {
// 1. トップページへアクセス
await test.step('Go to landing_page', async () => {
const requestPromise = page.waitForRequest(targetUrl);
await page.goto(targetUrl);
await requestPromise;
});
// 2. ランダムにリンクを選択し、詳細画面へアクセス
await test.step('Go to post_page', async () => {
const postSelector = 'ul.post-item > li > a[href]';
const posts = document.querySelectorAll(postSelector);
const targetPost = Math.floor(Math.random() * posts.length);
const targetPostPath = posts[targetPost].getAttribute('href');
// 3. 詳細画面へアクセス
const postURL = targetUrl + targetPostPath;
const requestPromise = page.waitForRequest(postURL);
await page.goto(postURL);
await requestPromise;
});
});
});
3.3 PlaywrightシナリオをArtillery用に調整
Playwrightでのテストが動作したら、Artillery用にシナリオを調整します。
Artillery用のシナリオは `tests/main-artillery.spec.ts` として保存し、以下のように非同期関数として定義します。
注意: サンプルです。実際に動作するか試していません。
import { test, expect } from '@playwright/test';
import { Page } from 'playwright';
/**
* Artillery用のシナリオ
* 1. トップページにアクセス
* 2. 記事一覧からランダムにリンクを選択
* 3. 選択した記事の詳細ページにアクセス
*/
const targetUrl = "http://localhost:8080/";
export async function artilleryLoadTest(page: Page, context: any, events: any) {
try {
// 1. トップページへアクセス
await page.goto(targetUrl);
await page.waitForLoadState('networkidle');
// 2. 記事一覧からランダムにリンクを選択
const postSelector = 'ul.post-item > li > a[href]';
const posts = document.querySelectorAll(postSelector);
const targetPost = Math.floor(Math.random() * posts.length);
const targetPostPath = posts[targetPost].getAttribute('href');
// 3. 詳細画面へアクセス
await page.goto(targetUrl + targetPostPath);
await page.waitForLoadState('networkidle');
} catch (error) {
console.error('Error in artilleryLoadTest:', error);
throw error;
}
}
3.4 ロードテスト設定ファイルの追加
ロードテスト実行用の設定ファイル `artillery-config.yml` を作成します。
config:
target: "http://localhost:8080/"
phases:
- duration: 3
arrivalRate: 1
name: "Sample phase for local env"
engines:
playwright:
trace: true
processor: "./tests/main-artillery.spec.ts"
scenarios:
- name: "artilleryLoadTest"
weight: 100
engine: "playwright"
testFunction: "artilleryLoadTest"
負荷を上げたい場合は `phases` の `duration` や `arrivalRate` を調整します。
phases:
- duration: 200
arrivalRate: 10
name: "Ramp up" # 200秒の間、秒間10ユーザがシナリオを開始
- duration: 180
arrivalRate: 30
name: "Sustained load" # 180秒の間、秒間30ユーザがシナリオを開始
- duration: 100
arrivalRate: 10
name: "Peak out" # 100秒の間、秒間10ユーザがシナリオを開始
3.5 ローカル環境でロードテスト実行
シナリオと設定ファイルを用意したら、まずは小さい負荷で実行して動作を確認します。
$ node_modules/artillery/bin/run --output report.json artillery-config.yml
3.6 AWS Fargateからロードテスト実行
AWS Fargateから実行するにはAWSアクセスキーと必要な権限が必要です。
必要な権限は以下を参照してください。
https://www.artillery.io/docs/load-testing-at-scale/aws-fargate#alternative-create-the-iam-permissions-manually
$ export AWS_ACCESS_KEY_ID=AKI******
$ export AWS_SECRET_ACCESS_KEY=********
Artilleryのrun-fargateコマンドを使って実行します。
$ node_modules/artillery/bin/run run-fargate --region=ap-northeast-1 ./artillery-config.yml
初回実行時はCloudFormationにより各種リソースを作成するため少し時間がかかります。
実行結果はS3バケットに出力されます。ライフサイクル設定があるため、後で確認する場合は必要に応じてダウンロードやS3バケットの設定変更をしてください。
4. さいごに
サービスの安定稼働には限界値を知ることが不可欠です。Artilleryなら比較的低コストで高負荷テストができるのが魅力です。
別の記事でPlaywrightのTest GeneratorによるE2Eテストの作成を説明したいです。
E2Eテストを用意したなら、ログインや会員登録など重要な処理だけでもCIで定期実行するのがおすすめです。そのあたりも記事にしてみたいと思います。