インフラの死角
エンジニアリングチームがユニットテストの磨き上げに数週間を費やしたものの、マーケティングキャンペーンによる2,000人のユーザー流入でインフラが崩壊するのを何度も見てきました。これは繰り返される悪夢です。機能ロジックは完璧でも、500件の同時リクエストでデータベースのコネクションプールがパンクしてしまうのです。従来、負荷テストは四半期ごとに隔離されたQAチームが行う手動のイベントでした。ボトルネックが表面化する頃には、コードベースが進歩しすぎており、修正には100時間ものコストがかかるリファクタリングが必要になることも珍しくありません。
パフォーマンスを後回しにすることは、巨大な技術的負債を築くことと同じです。新しいコミットがチェックアウトフローに300msの遅延を加えたなら、数週間後ではなく、数分以内にそれを知る必要があります。これが「シフトレフト」パフォーマンステストの核心です。CI/CDパイプラインに負荷テストを組み込むことで、パフォーマンスはセキュリティやビルドチェックと同様、譲れない条件になります。
なぜモダンエンジニアリングにはk6なのか
ツールの選択は、多くの場合「摩擦」の少なさで決まります。JMeterやGatlingは業界の定番ですが、JMeterのXML多用のUIは2005年の遺物のように感じられ、Gatlingは開発者にScalaエコシステムへの習入を強います。k6はこの障壁を下げます。スクリプトには標準のJavaScriptを使用し、Goベースのエンジンを活用することで、ローカルCPUを浪費することなく高並列な実行を可能にします。
CI/CDにおける真の力は、k6の「しきい値(Thresholds)」にあります。手動テストでは、Grafanaのダッシュボードを眺めて「大丈夫そうだ」と願うだけかもしれません。しかし、パイプラインにはバイナリ(成功か失敗か)の結果が必要です。k6では、「95パーセンタイルのレイテンシが400ms未満であること」といった厳格な基準を定義できます。パフォーマンスが低下した場合、ゼロ以外の終了コードを返し、ビルドを停止させることができます。
監視すべき主要メトリクス
- VUs (Virtual Users): 仮想ユーザー。ユーザー의行動をシミュレートする同時実行スレッド数。
- Iteration: 1つのVUがテストスクリプトを完全に実行した回数。
- http_req_duration: サーバーの応答にかかった合計時間。最も重要な健全性指標です。
- http_req_failed: 4xxまたは5xxエラーを返したリクエストの割合。
初めてのk6スクリプト作成
小さく始めましょう。まずはステージング環境やローカルコンテナを対象にします。予告なしに本番サイトに負荷テストを仕掛けることは、午前3時のオンコール呼び出しへの近道です。まず、マシンにk6バイナリをインストールします。
# macOSの場合
brew install k6
# Linux (Debian/Ubuntu)の場合
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
次に、load-test.jsを作成しましょう。このスクリプトは現実的なユーザージャーニーをモデル化しています。30秒かけて仮想ユーザーを0から50人に増やし、そのピークを2分間維持し、その後ゼロまで減らします。
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 }, // 50ユーザーまでランプアップ
{ duration: '2m', target: 50 }, // 負荷を維持
{ duration: '20s', target: 0 }, // スケールダウン
],
thresholds: {
'http_req_duration': ['p(95)<400'], // リクエストの95%が400ms未満であること
'http_req_failed': ['rate<0.01'], // エラー率を1%未満に抑える
},
};
export default function () {
const res = http.get('https://staging-api.example.com/v1/products');
check(res, {
'ステータスが200であること': (r) => r.status === 200,
});
sleep(1);
}
thresholdsブロックは自動化されたゲートキーパーとして機能します。この設定により、10件以上の同時書き込みがある場合にのみ発生するデータベースロックの問題を特定できた事例もあります。これは、最適化されていないコードがユーザーのブラウザに届くのを防ぐセーフティネットです。
GitHub Actionsへの組み込み
自動化は、一度限りのチェックをプロジェクトの標準に変えます。ほとんどのCI/CDプラットフォームがk6をサポートしていますが、GitHub Actionsは特に簡単です。プルリクエストごとに実行されるワークフローを.github/workflows/performance.ymlに作成します。
name: パフォーマンス回帰テスト
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
k6_load_test:
name: k6負荷テストの実行
runs-on: ubuntu-latest
steps:
- name: コードのチェックアウト
uses: actions/checkout@v3
- name: k6テストの実行
uses: grafana/[email protected]
with:
filename: load-test.js
flags: --tag testid=github-actions-run
このYAMLを配置すると、すべてのPRがパフォーマンス監査の対象になります。新しい依存関係や複雑なSQLクエリによってp95の応答時間が400msを超えると、GitHub Actionsは失敗します。これにより、開発者はマージ前にコードを最適化することを強制されます。
階層型テスト:スモーク、ロード、ストレス
すべてのコミットで大規模なストレステストを実行するのはコストがかかり、速度も低下します。開発ループを高速に保つために、戦略を階層化しましょう。
1. スモークテスト
すべてのコミットで実行します。2人の仮想ユーザーで60秒間実行し、/healthや/api/v1/statusエンドポイントが500エラーを返さないことを確認します。これは素早い生存確認です。
2. ロードテスト(負荷テスト)
mainへのすべてのPRで実行します。典型的な1日のピーク(例えば200人の同時ユーザー)をシミュレートします。これにより、時間の経過とともに蓄積されるわずかなパフォーマンスの低下を捉えます。
3. ストレステスト
プロダクトのローンチなど、主要なイベントの前に実行します。システムの限界を見つけるために、通常の1,000%のキャパシティまで負荷をかけます。ロードバランサーが先に壊れるのか、データベースのCPUが100%に達するのか. これらの限界を知ることで、実際のスパイク時にパニックに陥るのを防げます。
データの解釈
パイプラインが失敗してもパニックにならないでください。k6はログに明確なサマリーを表示します。まずhttp_req_durationメトリクスを確認しましょう。p(99)が高い場合、負荷がかかったときにのみ発生する特定の修正ケースや最適化されていないループを示していることが多いです。
長期的な追跡のために、k6の結果をPrometheusやGrafana Cloudに転送します。今日のビルドを3ヶ月前のものと比較することで、アプリケーションが高速化しているのか、それとも徐々に肥大化しているのかが明らかになります。
終わりに
パフォーマンステストの自動化により、デプロイにおける推測が排除されます。もう、ロールアウト中に指をくわえてCPU使用率を眺める必要はありません。明確なしきい値を設定することで、速度がエンジニアリングチーム全体の共通の責任であるという文化を育むことができます。
今日、最もコストの高いAPIエンドポイントに対してスモークテストを書くために10分だけ時間をとってみてください。チームがPRでそれらのメトリクスを目にすれば、パフォーマンスは「いつかやる」タスクではなく、ワークフローの不可欠な一部になるはずです。

