バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

プレ負荷テストの導入が、新規開発プロジェクトの負荷テストを円滑にした体験談

はじめに

こんにちは、開発2部の尾沼です。

私は現在、新規プロダクトである商品販売管理システムを開発するチームに所属しています。 リリースするにあたって、品質担保をするのは一般的なことであると思います。私たちのチームもその一環で負荷テストを実施しました。 しかし、まだユーザーに使われていないプロダクトにおいて負荷テストをする際、どうゴールを定めるか困ることもあるのではないでしょうか?

本記事では、私たちのチームが負荷テストを進めるうえでぶつかった問題と、プレ負荷テストを設けることでそれらを解決したことについて書きたいと思います。

対象読者

本記事は以下のような方々に読んでいただければ幸いです。

  • 新規開発プロジェクトにおいて、リリース前準備として負荷試験を控えているチームに所属するエンジニア

背景

私の所属しているチームではtoB向け商品販売管理システムを新規開発しています。そして、初のリリースを社内向けに9月に控えていました。

かねてより、私たちのチームには「リリース前に品質の担保をしておきたいよね」という共通認識はありました。しかし、その方法に関して具体的な部分は決まっておらず、「少なくともシナリオテストや負荷テストなどは必要そうなのでやりましょう」という状態でした。

そうした状況のもと、負荷テストの取り組みを開始しました。

当初の認識

一般的に負荷テストは、想定リクエスト数を段階的に増やしていき、各段階でのシステムの振る舞いを観測するものです。そして、各段階にて目標数値を突破できていればOKとするというものです。

私たちは以下のフローをイメージして、負荷テストの準備を開始しました。

当初の負荷テストのイメージ

負荷テストを進める上での問題

負荷テストを進めるにあたって、我々のチームでは問題が発生しました。

1. 負荷テストの目標が設定しにくい

新規開発では特に起こりうる話だと思います。

リリースしていないプロダクトであるため、目標とする数値をどこに設定するかに関して妥当な根拠となりうる情報が不足しており、設定することが難しい状況でした。

私たちのチームにおいては、負荷テストの計画をする上で信頼できる情報は以下の2点しかありませんでした。

  • リリース後の想定ユーザー数

  • 月当たりの想定流通商品数

また品質を担保する上で、負荷テストという手段でどこまで担保できるかに関しても曖昧な状態でした。

2. チームの負荷テストへの習熟度が高くない

私たちのチームは新規開発プロジェクトであり、これまでアクセス制限された環境で最低限のリソース構成で開発を進めてきました。

また、開発中は機能開発に優先してチームのリソースを割いていたため、 開発途中段階でログやメトリクスを観測してシステムのパフォーマンスを計測する、ということはあまり行ってきていませんでした。

また、負荷テストの経験がないメンバーもいたので、負荷テストで具体的にどのようなことをするかに関しての共通認識もない状態でした。

解決策

問題を解決するために、私たちのチームでは負荷テストを2回実施することにしました。本記事では以後、それぞれ「プレ負荷テスト」と「本負荷テスト」と表記します。

結果として、負荷テストの試みは以下を経ました。

負荷テストの流れ

プレ負荷テストと本負荷テストは目的と内容に違いがあります。

プレ負荷テストは、システムの現状把握や私たちのリハーサルの側面が強かったため、負荷のかけ方に幅を持たせたり、全チームメンバー総動員でテストを実施したりしました。それに対して本負荷テストでは、最低限必要なメンバーのみを動員し、負荷は品質担保の判断に足るレベルまでで実施しました。

特にプレ負荷テストが私たちの一連の取り組みを円滑にしたので、本記事ではそれについて詳しく紹介します。

プレ負荷テストの狙い

システムの健康診断

私たちのシステムの現状の状態を定量的に把握することを目的の1つとしました。

そもそも負荷テストの試み前にはっきりしていることは、開発の中で発生しうる少々のリクエストならば、観測できる範囲内では問題なく動くという程度でした。しかし、この状態では負荷テストで目標数値を設定することは困難です。

ユーザー数、同時リクエスト数、リクエストの間隔が開発環境下とは異なる状態になった場合、現状のシステムがそもそもどのような挙動を見せるのかを把握すること。そして、どこにボトルネックが発生するのかを知ることこそが、私たちにとっては必要であると考えました。

負荷テスト慣れ

負荷テストの準備〜当日の流れ〜パフォーマンスチューニングを一通り経験して慣れることは私たちにとって必要であると考えました。

先述したように、私たちはログやメトリクスを用いたパフォーマンス分析の経験が乏しかったです。また、負荷テストの段取りに関しても具体的な共通認識を持てていない状況でした。

そのため、プレ負荷テストをなるべく本番の流れを意識して行い、一連の負荷テストの流れをチームメンバー全員が体感することで段取りの共通認識を得ようと考えました。そして、ディスカッションしながら得られたデータを分析し、本負荷テストに備えたチューニングをすることを通じて、上記問題を解決しようと考えました。

障壁検知

そもそもシステムがまともに負荷テストを実施できる状態なのかの判断もできない状態でした。そのため、実際に負荷をかけることで、本負荷テストをスムーズにする上での障壁となる部分や、リリースに影響が出る部分の早期検知を行うことにしました。

プレ負荷テストの時点で検知した不具合は、まずは後述する応急手当てで解消しました。対応しきれなかったものや仮対応の部分については、本負荷テストまでに改修を行い、スムーズな本負荷テストに繋げました。

進め方で工夫した点

適切なリクエストユーザー数を探る

負荷テストではリクエストユーザー数を段階的にわけます。しかし、現状のパフォーマンスがはっきりしていないため、どのくらいのリクエストユーザー数で負荷シナリオを実施すれば有意義なのか、どのくらい負荷の段階を刻むべきなのか判断しにくい状況でした。

そのため、あらかじめテストケースでは想定リクエストユーザー数を極小〜現実的な最大想定ユーザー数の3倍程度まで網羅しておいた上で、一旦現実的な最大想定ユーザー数で実施しました。その結果を踏まえて、次に実施する想定リクエストユーザー数を都度決めました。

例えば、現実的な最大想定ユーザー数が100人だとしたら、1人〜300人で10人刻みリクエストユーザー数を定義したシナリオを作成し、100人でまずテストしてみます。その結果を見て、次に実行する想定ユーザー数を決めるといった感じです。

応急手当てをしてでもとにかく動かす

いざテストを実行するとテストの進行に支障がでるレベルでシステムが動かないということが度々発生しました。私たちは、とにかく機能を保ったまま動くようにするために、応急の対応を施すことに注力しました。正常にシステムが動く状況下で、パフォーマンスを計測したかったからです。

プレ負荷テストの効果

プレ負荷テストの効果としては有意義なものがいくつか得られました。

理想的な段取りを再現性のある形で残すことができた

メンバー全員が負荷テスト時の動き方を具体的にイメージできるようになったので、議論の幅が広がりました。

その状態で、プレ負荷テスト後に負荷テストの進め方そのものの振り返りを行いました。

振り返りの過程では、例えば以下のような反省点があることがわかりました。

  • 関係各所への連絡タイミングやスケジュール調整が遅くなってしまった。
  • 具体的な進め方について、テスト当日に認識齟齬が発生した。
  • 当日の役割分担の周知の優先度が下がっており、当日若干の混乱が発生した。

そこで、どう進めるべきだったのかについて議論しました。そして、理想の進め方に再現性をもたせるために以下のような進め方手順書を作成し、本負荷テストは手順書に従って進めました。

その結果、本負荷テストはより少人数で段取りよく実施できました。

作成した手順書

本負荷テストをする上での必須改修箇所の検知と改修に繋がった

実際に負荷を加えてみた結果、以下のようなことがわかりました。

  • 負荷テストを進めることすらままならない事態がそれなりに起きること

  • テストシナリオの不備

上記に対して、改善案を考え、本番負荷テストまでに修正をしました。

データを分析して改修し、本負荷テストに備える

負荷テストを進めることすらままならない事態は極力応急手当てでしのぎました。しかし、解消しきれなかったものについてはプレ負荷テスト後に対応しました。

今回はその一例の「販売情報連携リクエストに対する処理でなぜかタイムアウトしてしまう」という事象に対して、私たちがどのように対応したかについて紹介したいと思います。

前提として、私たちのシステムは以下のような構成となっています。

システムの概観

クライアントからのリクエストをGraphQLサーバーで受け付け、処理を行います。必要に応じてCloud Tasksを経由してバッチサーバーに処理を繋げます。DBはCloud SQLを利用しています。

このうち、GraphQLサーバーに関しては他連携システムとの都合上、タイムアウトを30秒にしています。

販売情報連携リクエストでは、以下のようなイメージで販売先IDと商品のIDを送り、処理の中で販売先データの登録や非同期処理に必要なデータ準備を行い、最後にCloud TasksのキューにTaskを追加します。

{
  "store_ids": [1,2,3, ... ], // 販売先
  "item_ids": [1,2,3,4,5, .... , 1000]  // 商品
}

そして、負荷テストの過程で短いスパンでリクエストを送りつづけるとGraphQLサーバーの30秒のタイムアウト制限に引っかかってしまうという事象が発生しました。

私たちは以下に特に注目しながら、リクエスト数を変えたり販売情報連携先を変更しつつ、ボトルネックの特定を進めました。

  • Cloud SQLのQuery Insightsを活用しつつSQL実行時の状況を探索すること
    • 過度に重いSQLが実行されていないか
    • 秒単位のCPU待機やロック待機が発生していないか
    • インデックスが有効に使われているか
  • Cloud Loggingを用いた、タイムアウトの原因の特定
  • Cloud Runのインスタンス数の調整

インデックスの貼り漏れの解消、一度にInsertするデータ量の調整、Cloud Runの最小インスタンス数調整によるコールドスタート対策など様々な試行を試みました。

最終的には、1リクエストで送る販売先数を減らしつつ、複数のリクエストに分けることで仕様を満たす方針にしました。動くようになったので、改めて今回の対応が関わるシナリオだけ計測しなおし、本負荷テストに望むことにしました。

テストシナリオの修正

テストシナリオに関しては、1シナリオあたりのスコープを調整と不要なシナリオの削減を実施しました。

私たちの開発しているシステムでは、ユーザーの利用時の流れは数パターンに大きく分けることができます。プレ負荷テストの段階のテストシナリオでは、その流れをいくつかに区切って部分部分で1つのシナリオとし、テストを実施しました。

しかし、私たちのシステムの意義は商品販売管理を効率化することです。その点を考慮し、一連のシステム利用の流れ全体にかかった時間を計測したほうが良いのではとチーム内で議論し、本負荷テストのシナリオのスコープを広くしました。以下はその例です。

シナリオのスコープを変更

また、そうしたスコープの調整を経て別シナリオに包含されたシナリオや、チューニングにて非同期化を施したことでユーザーからの大量アクセスが起きないようにした機能については、シナリオから省くことにしました。

本負荷テストでの適切な目標数値設定が可能となった

計測をした結果、多くのログやメトリクスデータを得ることができました。

私たちはシナリオ内のリクエストのレイテンシに対して、目標平均値、目標最大値、目標中央値を定めることにしたので、プレ負荷テストのシナリオ内で発生したリクエストのレイテンシデータを使うことにしました。

今回、負荷テストツールはk6を採用してテストを実施していたため、次のようなフォーマットでデータを得ることができました。

 http_req_duration: avg=230.51ms min=2.32ms med=82.05ms max=2.74s

得られたデータは、シナリオ内のリクエストのレイテンシの平均値、最小値、中央値、最大値です。

そのため、目標を立てる上で信頼できるデータは元々存在していたものと合わせて以下となりました。

  • リリース後の想定ユーザー数

  • 月当たりの想定流通商品数

  • 各シナリオ内で発生したリクエストにおけるレイテンシの平均値、最小値、中央値、最大値

上記情報をもとに本負荷テストでの目標を立てることにしました。

しかし、先述したようにプレ負荷テストと本負荷テストでシナリオを変更したため、そのままk6のデータを用いることはできません。そこで、以下のような手順で本負荷テストでの目標レイテンシを求めました。

1. 本負荷テストのシナリオに関連するプレ負荷テストのシナリオを選ぶ

私たちの事例では、本負荷テストのシナリオはプレ負荷テストのシナリオを合体させて一連の流れにしたものがほとんどでした。そのため、本負荷テストのうち、プレ負荷テストのどのシナリオが関わってくるかを判断することから始めました。

例えば以下のようなイメージです。

関連するシナリオを集める

2. 選定したプレ負荷テストのシナリオそれぞれにおいて、各シナリオ内のリクエストの合計レイテンシを求める

選定したシナリオを実現するk6のコードから、1シナリオ内でいくつリクエストが送られるかを数えました。そして、k6のレイテンシデータ(http_req_duration)のavgが、そのシナリオ内のリクエストの平均レイテンシなので、リクエスト数 × 平均レイテンシを行い、結果をそのシナリオのリクエストの合計レイテンシとしました。

プレ負荷テストのレイテンシを計算する

3. レイテンシの目標平均値を決める

本負荷テストの合計レイテンシ本負荷テストの合計リクエスト数で割ったものが、本負荷テストシナリオ内の1リクエストあたりの平均レイテンシです。

本負荷テストの合計レイテンシは、2で求めた各シナリオのレイテンシの和です。本負荷テストの合計リクエスト数k6のコードを見て数えました。

算出した平均レイテンシの数値に対して、微調整を加えた値を目標平均レイテンシとしました。

本負荷テストのレイテンシを計算する

4. レイテンシの目標最大値と目標中央値を決める

プレ負荷テストの結果を見て、レイテンシの最大値と中央値にも目標を設定しました。 最小レイテンシに関しては、その値の大小が品質の受け入れに影響を及ぼすことは少ないと考え、目標としては今回採用しませんでした。

5. プロダクトマネージャーが目標レイテンシが妥当かどうかの判断を行う

最後はプロダクトマネージャーが目標として妥当な数字であるかを判断します。問題なければそのシナリオの目標レイテンシが確定します。

結果として、本負荷テストシナリオにおいて、レイテンシの目標平均値、目標中央値、目標最大値が求まりました。

結果

プレ負荷テストを実施したことにより、本負荷テストをスムーズに実施することができました。

本負荷テストでは、目標レイテンシを満たすことができなかったシナリオもありましたが、それらに関しては原因をチームで議論し、受け入れ可能かの判断をしました。

そして、本負荷テストを踏まえて更なる改善をし、現実的にあり得る最大の負荷において品質の担保ができるようになりました。

おわりに

いかがでしたでしょうか? 私たちと同様に、負荷テストを行おうにも情報が不足していて困っているかたは、ぜひプレ負荷テストの実施を検討してみてください。

最後に、バイセルではエンジニアを随時募集しております。興味のある方はぜひ以下の採用サイトをご覧ください。

herp.careers

herp.careers