ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

既存APIサーバー移行のための負荷テスト設計と実施事例について

こんにちは、プロダクトエンジニアリング部の周です。

最近、社内の内部APIサーバーの一つを、AWSのEC2からECS Fargateへ移行しました。

既存のEC2 APIサーバーですでに一定のトラフィックを捌いているシステムだったため、移行後のECS環境でも同等のパフォーマンスが出せるか、適切なスペックやオートスケーリング設定はどうすべきかなどを検証する必要がありました。そこで、移行前に負荷テストを実施しました。

本記事では、その際の負荷テスト設計や実施内容についてシェアしたいと思います。

組織やプロダクト、ユースケースによって負荷テストの目的や設計は異なると思いますが、今回私たちが何を検討し、どのような工夫をしたのか、その一例として紹介します。これから既存APIサーバーの負荷テストを実施しようとしている方のヒントになれば嬉しいです。

負荷テストを実施する目的

一般的に負荷テストの目的は様々ですが、今回は以下の2点を達成するためにテストを設計しました。

  • 既存本番のEC2と同等のパフォーマンス維持
    • 既存本番のEC2環境と同じ負荷を新しいECSにかけた際、同等のパフォーマンスが出せるスペック構成やオートスケーリング設定を特定したい。
  • 将来的な負荷への耐久性検証
    • 今後の利用者増を見込み、現状の本番環境以上の負荷が発生した場合のECS環境の挙動を確認したいです。
    • ただし、将来的に想定される最大負荷以上の負荷をかけてもボトルネックが発生しない場合は、十分に安全圏であると判断し、それ以上の限界値までは追求しないことにしました。

利用ツール

負荷テストツールには k6 を採用しました。 詳細は割愛しますが、Go言語製で動作が軽量である点や、テストシナリオをJavaScriptで記述できるため扱いやすい点が採用の決め手です。

構成

移行後の本番ECS構成のイメージは以下の通りです。

負荷テストの環境は、なるべくECS本番と同じ構成を維持したいと考えました。そこで、Load Balancer以降の構成は本番と同等にしつつ、本番とは完全に切り離された環境を以下のように構築しました。

Load Generatorとしてのk6が、APIのクライアント(呼び出し元)をシミュレートし、ECSへ負荷をかける役割を担います。

テスト設計

いくつかのシナリオを設けてテストを行いました。

  1. 本番EC2で実際に発生しているピーク時の負荷をシミュレートし、ECS環境に同等の負荷をかける。

  2. 現行の数倍の負荷をかけ、システムの限界や挙動を確認する。

  3. リクエストが少ない時間帯(深夜など)の負荷をシミュレートし、スケールインの挙動などを確認する。

今回は最も重要である1.を中心に詳細を説明します。

検討項目

テスト実施にあたり、以下の項目を検討・設定しました。

  • 負荷を受ける側のスペック (Garden ECS)
    • CPU / メモリ
    • 初期タスク数
    • スレッド数
    • Auto Scaling Policies
  • 負荷をかける側のスペック (k6の実行環境)
    • CPU / メモリ
  • DBとRedisのデータ量/データ構成
    • 理想は本番データをそのままの実データ利用しますが、実データをテスト環境で使用するのはセキュリティ(個人情報など)の観点でリスクがあります。

ECS側の設定については、テストを何度も実施し、目標とする結果が出るまでパラメータチューニングを繰り返しました。

負荷の設計

多くの負荷テストの設計では、いくつかのユーザーシナリオ(例えば「ログイン→スレッド確認→写真アップロード」といった一連の流れ)を用意し、それを単位時間に何回実行するかを決めるのが主流だと思います。

しかし、うちのAPIサーバーを利用するプロダクトは2桁にのぼり、担当する開発チームもそれぞれ異なります。そのため、すべてのユースケースを洗い出してシナリオを設計するにはコストがかかります。

そこで今回はシナリオベースではなく、「実際にAPIに到達しているリクエスト」を分析し、それと同じ種類・割合・量でECSへ負荷をかけるというアプローチを採用しました。

リクエスト量の決定

本番環境の特定の日時におけるリクエスト数(RPS: Request Per Second)をトレースしてシミュレートすることにしました。 k6のramping-arrival-rateというexecutorを利用することで、上図のようにRPSを一定のレートで増減させ、実際のトラフィック波形を再現しました。

対象APIの選定

すべてのAPIをテストするのは難しいため、以下の基準で対象を選定しました。

リクエスト数累計上位90%のAPI

本番でのAPIコール数が多い順にソートし、全体の90%を占めるAPIを特定しました。 下のような感じです。

API 呼び出し数の全体割合(%) 累計(%) 選定対象
GET /api/v1/a 40% 40% O
GET /api/v1/b 30% 70% O
GET /api/v1/c 10% 80% O
GET /api/v1/d 10% 90% O
POST /api/v1/e 3% 93% X
DELETE /api/v1/f 3% 96% X

選定されたAPI(例:/api/v1/a ~ /api/v1/d)に対して、その割合に応じた負荷を配分しました。 例えば、テスト全体の総RPSに対して、/api/v1/a は 40% / 0.9 = 約44%の比率でリクエストを生成するように設定します。

CRUD(Create/Read/Update/Delete) APIの網羅

今回のシステムはGET系(参照系)のリクエストが殆どでしたが、更新系(DB/Redisへの書き込み)のパフォーマンスも確認しておく必要があります。そのため、上位には入らなくても、CUD(Create/Update/Delete)系のAPIの中で利用頻度が高いものも選定に加えました。

負荷が高い単一API

全体負荷が高い状況下で、元々処理が重いAPIが正常に動作するかを確認するため、レスポンスタイムが長いAPIや、データ転送量が多いAPIも個別にピックアップしました。

その他

  • APIは渡されるパラメータによって処理負荷が大きく変わります。そのため、EC2本番ログを調査し、平均的によく利用されているパラメータを特定してテストシナリオに組み込みました。
  • 同じパラメータを使い続けることによるDBキャッシュの影響も考慮しましたが、今回のボトルネックはDBではなくAPIサーバー側の処理にあると判断したため、テストへの影響は軽微として許容しました。

本番負荷の数倍の試験について

前述の通り、本番リクエスト数の倍率をかけたストレステストも実施しました。 例えばn倍試験の場合、以下の図のように、リクエストの増減パターン(波形)は維持したまま、各時点のリクエスト数をn倍に底上げして実施しました。

試験中・試験後の監視項目

  • ECSのCPU / メモリ
  • k6 / LB / APIサーバに各レイヤのresponse time
    • 移行前のEC2と劣らぬパフォーマンスであるかを測るメインの指標です。EC2より大きく劣ってる場合はチューニングします。
  • リクエスト種類ごとのレスポンスタイム
  • Auto Scalingの具合
    • リクエスト上昇時に、Scale Out(タスク追加)の速度が追いつくか。
    • リクエスト減少時に、適切に Scale In(タスク削減)されるか。
  • k6のCPU / メモリ
    • 負荷をかける側(k6)のリソースが枯渇すると正しい負荷がかけられないため、こちらも監視必須です。
  • DB / Redisのmetricsに異常がないか
    • 今回は主対象ではありませんが、異常値がないかを確認しました。
  • エラー率
  • スループット
  • AWSコスト試算
    • パフォーマンスが良くてもコストがかかりすぎては意味がないため、想定コスト内に収まるかも確認します。

チューニングについて

テストの結果、パフォーマンスが目標に達しなかったり、コスト過多だったりした場合は設定を見直しました。具体的には以下の項目を調整しました。

  • ECSのタスク定義(CPU / メモリ)
  • Auto Scaling Policies
  • APIサーバのプロセス数とスレッド数

試験後

チューニングの結果を本番に適用したら、ECSのパフォーマンスは予想と大きくずれなく本番稼働ができてます!

改善できること

今回の負荷テストの設計や実施について改善できる点も見つかりました。

  • スパイクテスト(短時間の急激なアクセス)の考慮
    • 移行後、実際に短時間に大量のリクエストが集中することがあり、ECSのCPUが100%に張り付いてパフォーマンスが低下する事象が発生しました。事前にスパイク的な負荷テストを実施し、それに基づいたチューニングができていれば防げた可能性があります。
  • スレッド数の監視
    • 今回監視項目にスレッド数が監視できなかったです。APIサーバーからカスタムメトリクスとしてモニタリングサービス(CloudWatch)へ情報を送る実装が必要です。
    • もしスレッド数まで可視化できていれば、より精度の高い最適化が可能だったはずです。

最後に

負荷テストは環境構築やシナリオ作成に工数がかかりますが、サービスの安定稼働を守り、自信を持ってリリースするためには非常に重要なプロセスだと改めて実感しました。

この記事が、これから負荷テストを検討している方のヒントになれば嬉しいです。 最後まで読んでいただき、ありがとうございました!

jobs.unifa-e.com