大型リリースの心理的安全を担保する負荷試験
背景
私たちのチームでは先日、これまでのユーザーが突然倍以上になることが予想されるような超大型リリースを経験しました。
私たちのチームはSaaS型のプロダクトを運営していたため定量的な非機能要件は設けられていませんでしたが、チームとしては「リリースしても大丈夫」と言える状況にしたいと考えていました。
このブログでは、そういったシチュエーションで実施した負荷試験のノウハウを記していきます。
実施手順
3週間ほどかけてシステム負荷の予想~チューニングというプロセスで実施しました。
テスト方針の決定
負荷テストについて調査を行った結果、限界値テストやパフォーマンステストなど、複数の種類があることがわかりました。
今回のテストの目的は「システムが問題なく動作すること」を確認することでした。そのため、定常的な負荷を一定期間与えるAverage Load Testを採用することにしました。
テスト環境については、事前に破壊的なテストが可能な環境を用意していたため、そこで実施することにしました。
負荷試験ではPDCAを何度か回す必要があるため、メンバーとの調整コストが最小限で済む環境を選択することが重要です。
システム負荷の予想
私たちのサービスは人力オペレーションからWEBベースのオペレーションへの移行することをサポートするためサービスです。
そのため、現在の人力オペレーションの量からシステム負荷の増加を予測することができました。
負荷試験の各シナリオでは現システム負荷×予想増加率
を計算して負荷試験を実施しました。
シナリオごとの負荷は機能によっては利用者数に比例して増加しないような例外ケースもありましたが安全性を担保するための負荷試験として、オーバー負荷となることは許容することにしました。
シナリオ選定
GCPのCloudMonitoringでAPIメトリクスを確認し、リクエスト数の多いAPIを中心にシナリオを選定しました。
ログイン機能は外部システムとの連携が多く、当初は試験実施予定でしたが、計画段階で有益なベンチマークが取得できないことが判明したため対象外としました。
プッシュ通知システムについては、通知サービスを提供するベンダーのドキュメントとサポート情報を参考に、実施可能な範囲で試験を行いました。
テスト実施準備_ダミーデータ・シナリオの用意
シナリオの詳細はPlanning Docに記載し、テストの種類、実施時間、必要な環境を定義しました。
実施ツールはk6を選定しました。k8sを採用していたためk6-operatorによる実行も検討しましたが、今回はローカルからの実行で十分だったため、ローカルから実施することにしました。
ただし、IPがバンされるリスクや負荷試験実施中にローカルマシンが停止する可能性があるため、より大きな負荷をかける際はローカルからの実行は推奨しません。
RDBMSに投入するダミーデータは、簡単なダミーデータジェネレーターを実装して作成しました。RDBMSへのインサート句にはNバイト以下という制約があったため、実行クエリは制約を考慮した工夫が必要でした。
実施
負荷試験の実施前に、Slackでチームメンバーに実施予定を連絡しました。
実施過程で、一部のメンバーしか把握していない重いバッチ処理の存在が判明しました。そのため、実施前にはワーカーやバッチ処理の全体像を把握しておくことが重要だとわかりました。
また、SESによる不要なメール送信が発生するケースがありました。外部サービスについては、モック化するか該当コードをコメントアウトしたソースコードをデプロイしておく方が望ましかったです。
結果チェック・チューニング
CloudMonitoringとCloudSQLのダッシュボードを使用して、関連するコンポーネントのメトリクスを確認しました。
複数のシナリオの検証結果に基づき、以下の調整を実施しました:
- サーバーリソースの調整
- クエリインデックスの再設定
- 同時接続数と実行数の調整
CloudSQLのQueryInsights機能を活用することで、負荷の高いクエリを効率的に特定することができました。
また、MySQLでは以下の方法で実行中のクエリを調査しました:
-
information_schema.innodb_trx
テーブルへのクエリ -
SHOW PROCESSLIST
コマンドの使用
これらを用いてクエリのボトルネックを特定し、対応を行いました。
反省・振り返り
手軽に・何度でも実行できる環境を用意する
負荷試験の目的は、対象サーバーに負荷をかけて問題を発見し、それを改善することです。
そのため、負荷試験を繰り返し実施できる環境を整えることが重要です。また、サーバーやデータストアのリソースを容易に調整できる環境が望ましいです。
私たちのチームでは以下の構成を採用していたため、環境の準備や調整が容易でした:
- インフラ:terraform
- サーバー:k8s(yamlファイルで管理)
- デプロイ:makefileで管理
実際の運用では、同じ負荷をかけた場合でも:
- 問題の発生有無に差がある
- パフォーマンスにブレが生じる
といった現象が確認されました。
新規開発機能ごとにカジュアルに負荷試験を行えるとよかった
今回のリリースでは、リリースの1ヶ月前から負荷試験を開始したため、全ての機能で負荷試験を実施することはできませんでした。
負荷試験を実施してみると、レコード数や負荷の増加に応じてスロークエリなどのパフォーマンス劣化が発生するケースが、想定以上に多く見られました。
そのため、以下のようなベンチマークを機能開発時から把握できていれば、より心理的安全性の高いリリースが可能だったと考えられます:
- APIのRPSとリソース使用量の関係
- CPU使用率の推移
- メモリ使用量の推移
単一障害点(Single Point Of Failure)に絞ったテストも行えるとよかった
MG-DXでは、マイクロサービスアーキテクチャを採用しており、ドメインごとに個別のサーバーを起動して運用しています。
負荷テストの実施中、特定のサーバーへのコネクション数の集中が原因でパフォーマンス劣化が発生するケースを偶然発見しました。
パフォーマンス劣化の主な要因としてDBのIOがボトルネックとなるケースを想定していましたが、システム負荷が集中するサーバーの接続数など、単一障害点となりうるポイントを意識した負荷テストの実施が必要だとわかりました。
感想
普段からシステムパフォーマンスや負荷を意識したい
負荷試験を通じて、サーバーリソースが一定である以上、システムには必ず限界点が存在することを学びました。
多くの現場では、現在の構成でどの程度のシステム負荷まで耐えられるかについて、具体的な指標を持たずに運用されているのではないかと考えます。少なくとも私自身はそうでした。
システム設計において、以下のような指標を意識した上で設計を選択できることが、エンジニアとしての実力だと感じました:
- APIあたりのCPUリソース使用量
- メモリ使用量
- 外部サーバーコネクション数
- 設計パターンごとのシステム負荷の違い
データベースというものを深く深く理解したい
こちらの記事では、「多くのソフトウェア・アプリケーションのパフォーマンスは、本質的にディスクI/Oによって制限されます。CPUタイムの大部分をI/Oアクティビティが完了するまでの待機に使用するアプリケーションはI/Oバウンドと呼ばれます。」と述べられています。
今回の負荷試験でも、APIサーバー起因のボトルネックは一部あったものの、大部分はSQLがボトルネックとなっていました。
今後パフォーマンスをより深く理解していくにあたり、InnoDBのバッファプールについての理解を深めることが重要だと感じました。
Discussion