モノリシックサービスから高負荷なエンドポイントを切り出して段階的に運用改善した話

Progateの小笠原です。普段はSREチームで開発効率化やサービスの安定化に取り組んでいます。

本稿では弊社SREチームで取り組んだ事例の一つである「モノリシックなサービスから高負荷なエンドポイントを切り出して段階的に運用改善した話」について紹介させていただきます。

はじめに

ProgateのフロントエンドはReactを利用してリッチなUI/UXを実現しています。特にサービスの中心である演習画面ではユーザの書いたコードに対してエラーを出したり、サーバとWebSocketで接続してRubyやNodeなど様々な言語のコードを実行したりと複雑な構成となっています。

演習画面
演習画面

開発ではリリース前にQAを行い機能的な不備やデグレーションがないかを確認しますが、クライアント環境によっては意図したとおりにJavaScript(以下JS)が動かないケースが多々あります。例えばネットワーク環境の違いによりWebSocketが利用できずサーバと接続できなかったり、ブラウザの種類や拡張機能の存在によって判定ボタンが正しく動作しない、画面崩れが起きる、などが挙げられます。

クライアント環境の差異によって生じるこれらの事象を開発時に全て検出することは現実的には難しく、対症療法的に対応することも多いです。その際、起こった事象をなるべく正確に把握・分析するためにJSでデバッグログやエラーログを出力し、これをサーバに送信して分析基盤上で検索できるようにしています。

具体的には以下のような構成の分析基盤を構築して、クライアントのJS関連ログに関してRe:dashからクエリを叩いて分析・調査できるようにしています。

分析基盤
分析基盤

課題

ユーザ数増加とサービスの成長に伴いJS関連ログのリクエスト数も以前に比べて増えており、気づけばログリクエストの処理にサーバのCPUが常時10%から20%占有される状況でした。

ログのリクエスト数は通常はあまり多くないのですが突発的にリクエスト数が急増する場合があり、これが原因でサーバのCPU利用率が100%に張り付いて正常にリクエストを処理できなくなる障害が起きてしまいました。アクセス障害はサービスの信頼性を大きく損ねるため、この課題に対してSREチームとして早急に対策を講じることが求められました。

対応方針

リクエスト急増の原因を見つけて都度パッチを当てる対応も考えられますが、原因究明から対応完了までの間はダウンタイムが発生することを避けられません。

そこで、突発的な負荷増大に対してそもそも本体のサービス(以下、本体)に影響が出ないようにすることを大枠の方針に定めました。具体的にはログ関連のエンドポイントと処理をRailsから別サービスに切り出して負荷を分散することで本体の可用性向上を狙います。

ただ、ログ処理用のサービスを正しく作るにはプロビジョニングやデプロイフローなどの構成検討と実装にどうしても時間がかかりサービス安定化の観点では遅い対応となってしまいます。そこでフェーズを大きく二つに分けて段階的に対応することにしました。

まず第一フェーズでは最小手数で負荷分散を行い、緊急度の高い課題であるサービス安定化を図ります。次に第二フェーズでサービスを構成から正しく作り直して運用改善に取り組みます。以降では各フェーズでの具体的な実施内容を順に解説していきます。

第一フェーズ: 負荷を分散してサービス安定化を図る

このフェーズのゴールは以下2点です。

  1. ログ関連のリクエストに関してサーバ/DBの負荷分散を行い、本体を安定化させる
  2. なるべく少ない工数で完了させる

構成

この時点でProgateのサービス構成は(細かい部分は省略しますが)以下の通りです。

当初の構成
当初の構成

サービス概要とログ処理の流れ

基盤はALBとEC2(AutoScalingGroup)、Auroraの一般的なWebサービスの構成です。Webサーバはnginx、APサーバはRails(unicorn)、そしてログ収集はfluentdで処理します。

クライアントはJS関連のログを特定のエンドポイント(便宜的に /path/to/logs とする)にJSON形式で送信します。Railsは受け取ったJSONにログに情報を付与した後ファイルに書き出し、これをfluentdがKinesis Data Firehose(以下Kinesis)経由でS3バケットにアップロードします。なお、fluentdはこの他にもRailsのアプリケーションログやnginxのアクセスログなどもアップロードします。

実装方針

サービスの構成とゴールを踏まえて以下の手順にしたがって第一フェーズを進めることになりました。

  1. 本体と同じ構成でサービスを一組追加する(以下progate-logsと呼ぶ)
  2. ALBのL7ルーティングを利用してJS関連のリクエストをprogate-logsに流してサーバ負荷を分散させる
  3. progate-logsのDB接続先をAuroraのReadOnlyなエンドポイントに変更してDB負荷を分散させる

第一フェーズ完了後の構成
第一フェーズ完了後の構成

達成されたこと

第一フェーズ完了により、ログ処理に関わる負荷がprogate-logsに分散されることで本体への負荷が減り、狙い通りログリクエストが増加しても本体への影響が非常に小さい状態にすることができました。

運用面ではログリクエストの急増でアラートが鳴っても本体への影響が小さいため以前より落ち着いて対応できるようになりました。また本体とprogate-logsを別でインフラ設計できるようになったため、特に本体に関してはインスタンス数を8台体制から3台体制に減らすことができ運用コストを削減することができました。

第二フェーズ: 構成を最適化して運用改善する

このフェーズのゴールはprogate-logsの構成最適化と運用改善です。

第一フェーズでは最小手数で作業を進めるため本体と同じ構成を採用しましたが、ログ処理には不要なミドルウェアが多く入っておりリソース消費が必要以上に大きい状態でした。また、詳細は省きましたが本体と同じデプロイフローを採用したため、本体をデプロイする度にprogate-logsも毎回デプロイされていました。これはデプロイパイプラインの運用コストを増大させるだけでなく、本体のデプロイ時間を長くする一因となっていました。

これらの課題を解決するためprogate-logsの構成を本来必要な最小セットに置き換え、合わせてデプロイパイプラインも適切なものを新たに構築します。

サービス要件

チームで相談して新しく作るログ処理サービス(便宜上、新progate-logsと呼びます) の要件を以下のように定めました。

  1. ALB以下に配置でき、 /path/to/logs/* のエンドポイントへのリクエストをこれまで通りに処理できる
  2. ログ形式はRails構成の場合と同等のものを生成し、Kinesis経由でS3へログをアップロードできる
  3. クラウドサービスのコストとメンテナンスの手間の両方の観点で運用コストを削減する

つまり機能を維持しながら運用コストを削減することが要件です。

実装方針

要件を踏まえて事前の技術検証を行い、実装方針を以下のように定めました。

  1. 新progate-logsはnginx+fluentdのコンテナ構成(ECS Fargate)を採用する
  2. ログをフロントエンドで組み立てることでDB接続不要にする
  3. デプロイパイプラインはFargateのデプロイに適した構成で作成する

構成

最終的にはEC2構成のprogate-logsをFargate構成の新progate-logsに置き換えます。Railsで行っていたログの加工をフロントに移すことでDBへの接続が不要となり、nginxとfluentdだけでこれまでと同等の処理を実現できるようになりました。

第二フェーズ完了後の構成
第二フェーズ完了後の構成

デプロイパイプライン

デプロイパイプラインはECSと相性の良いCode系サービスを利用して以下のような構成で作成しました。GitHubにコミットをプッシュすると自動でイメージ更新とデプロイが走るようになっています。

デプロイパイプライン
デプロイパイプライン

考慮したこと

技術選定で考慮したことは大きくは二つありました。

一つ目は、コンテナ構成にするかEC2構成にするかです。Progateでは多くのサービスがEC2(AutoScalingGroup)で運用されており、プロビジョニング(chef)やデプロイに関してある程度流用が効くためサービスをEC2構成で作るメリットがあります。ただ、開発面でコンテナはchefよりデバッグサイクルが短くなること、運用面でコンテナはEC2構成よりもデプロイがシンプルでメンテナンスしやすいことが大きなメリットです。今回の要件ではnginxとfluentdのconfigurationをデバッグすることがメインになることと今後のメンテナンスコストを考えコンテナ運用を採用しました。

二つ目は、ECSサービスのうちEC2タイプを使うかFargateタイプを使うかです。FargateタイプはEC2タイプよりもスケール戦略が直感的に組めて運用しやすいのが大きなメリットですが、sshデバッグができないため必要なログをCloudWatch Logsなどに出力する必要があります。今回は構成がシンプルなため見るべきログも絞られており、sshデバッグが必要になることは少ないと考えFargateタイプを採用しました。

達成されたこと

第二フェーズ完了によりログを処理するのに必要なプロセスだけが走るスリムなサービスにprogate-logsを置き換えることができました。

運用コストに関しては、旧progate-logsではm5.largeのEC2インスタンスを5台運用していたところ新progate-logsでは最小サイズのFargateタスク3個で運用できるようになりマシンの運用コストを約92%削減することができました。

また、デプロイに関しては本体と分けることで必要なタイミングでのみデプロイされるようになり本体のデプロイ時間も元通りにできました。

まとめと所感

Railsから高負荷なエンドポイントを別サービスに切り出すことで、サービスの安定化と運用コスト削減を実現することができました。その際、段階的なアプローチを取ることでビジネス上の要件を満たしながら技術的な課題にも適切に取り組むことができました。

組織体制や開発効率の観点からサービスを小さく分割する話はよく聞きますが、SRE的な観点で安定化を目的としてサービスを切り出すのは事例として面白いと個人的に感じ今回紹介させていただきました。最後までお読みいただきありがとうございました。