どうも、ゲームコミュニティ事業部Tonamelのサーバサイド担当の谷脇です。
今回はTonamelのサービス特性上、どうしても発生する急激なアクセス数の増加(以下スパイクアクセス)をどのように対処しているかをお話します。
Tonamelのサービス内容については以前の記事に書いています。一言でいうと「誰でもeスポーツ大会の運営ができるサービス」です。
大会が開始したときに発生するスパイクアクセス
上記は、あるゲーム大会が大会を開始した10:40ごろのリクエスト数の遷移です。Tonamelは大会の開始と同時にトーナメント表が公開されます。大会に参加している人はもちろん、観戦を行っている人もトーナメント表を見に来ます。観戦する人の同期としては、知人や推しの選手のトーナメント表上での位置や、相手を確認しに来ているようです。
Tonamelに限らず、Webサービス内で多数のユーザーが一斉に使うような「イベント」のようなものがあるサービスの特性として、このようなスパイクアクセスはどうしても発生します。
採用しなかった案: スパイクをなだらかにするアプローチ
私が以前関わっていたソーシャルゲームでは、プッシュ通知を用いてイベントを通知すると、一斉に負荷がスパイクしてしまう現象がありました。このときの対処として、プッシュ通知をある程度、ゲームの体験を損なわない程度に分散させることを行っていました。
しかし、ゲームのルール上、同時に通知しないと不公平感があったり、そもそも今回のケースのような通知のタイミングを契機としたスパイクではないなどでは「スパイクをなだらかにする」アプローチは使用できません。あるとすれば、APIとしてリクエスト過多のような状態を表現し、リトライさせて分散させるようなやり方もありそうです。しかしトーナメント表がなかなか見れないなどのサービス体験の劣化が予想されます。
というわけで、「来たリクエストは全部ちゃんと返す」「とにかくスパイクに耐える」方針をTonamelでは取っています。
職人が週末の負荷を予測してスケールアウト予約する
Tonamelの場合、スパイクアクセス時に問題になるのは、WebアプリケーションのCPU負荷でした。それ以外のDBの負荷などはまだ問題にはなっていませんでした。ですので、Webアプリケーションサーバーをスケールアウトさせれば、スパイクアクセスに耐えられます。
TonamelはインフラにAWSを採用し、このスパイクアクセス問題に取り組んでいたときはEC2上でPerlのWebアプリケーションを動かしていました。EC2のCPU負荷やALBのコネクション数をもとにしたオートスケールも動いていましたが、スパイクアクセスの場合、オートスケールは間に合いません。上記のメトリックが示すように数分以内に2倍から3倍のリクエスト数増加が見られます。
一方で、Tonamelのサービス特性上、スパイクアクセスが発生する時間を予測することが可能です。Tonamelの大会は、
- 主催者によって大会が作成される この時点で大会の開催日時と規模(エントリー人数)の上限が確定する
- 主催者によって大会の参加者募集が行われる
- 大会参加者の募集が締め切られる この時点で規模(エントリー人数)が確定する
- トーナメント表が確定し、大会が開始する ここでスパイクアクセスが発生する
という経緯をたどります。1や3は大会開始よりも1週間以上前のことが多いです。またeスポーツ大会の参加者の特性上、週末に大会が行われることが多いです。スパイクアクセスが発生するのも週末になることが多いので、「この先1週間のスケールアウトの予定を金曜日に立てる」ことを取りました。つまりサーバ台数の需要予測ですね。
また、この過程で監視サービスとして用いているMackerelを見ながら、「この大会のときはリクエストが詰まった」などの履歴を、大会の規模やゲーム、主催者などの記録と一緒に取っていきます。これにより、ある程度の精度の「任意の大会に必要なサーバ台数」というのを割り出せるようになりました。
チーム内では「職人芸」と言っていまいたが、上記のナレッジから、特定の時間帯に必要なサーバ台数を割り出すPull Requestがこちらです。スケールアウトのスケジュールはEC2 Auto Scalingに対してterraformで記述する形をとっています。このPull Requestにコメントで「この時間にこの台数が必要な理由」を書いて、レビューをした上で適用する運用をしました。レビューの形で職人の暗黙知がチーム内に広がることで、この作業もローテーションして行えるようになりました。
また、需要予測を手助けするための、redashのダッシュボードも作成しました。このissueでは、時間帯別の参加人数のヒートマップをもとに、台数を割り出しています。
「職人芸」を自動化する
定量的な基準を持ってスケールアウトを行い、スパイクアクセスへの対処が出来てきたものの、毎週このような定形作業を行うのは苦痛でした。また、定量的に出来ているわけですから、自動化が可能なはずです。というわけで、上記の職人芸を定期バッチ化し、crontabに載せることで自動化してみました。
上記issueは自動化を試みたときのものです。issueの目的に、
金曜日の調査時に引っかからなかった大会が開催時までに膨れて予想外の負荷スパイクを生む場合でも、サーバが重くならないようにする
とあるように、実はこのとき特定の主催団体さんの使い方で、需要予測調査時に引っかからないにもかかわらず、予期しないスパイクアクセスが発生するようになっていました。具体的にいうと、Tonamel上でエントリー募集を行わずに、Google Formなど他のサービスで行い、大会の開催直前に他サービスでエントリーした人にTonamelのエントリーフォームを案内する運用です。この当時はTonamelのエントリーフォームの機能が弱く、例えば選択式の質問であったり、画像の投稿がエントリー時出来ませんでした。2022年12月現在ではこのあたりの機能もTonamel単体で行えますが、当時にこのような大会運用をするのは致し方ありません。
しかし、自動化すればこのような悩みも解決できます。職人が調査をするのは、他のタスクとの兼ね合いもあり1週間に1度が限度ですが、調査と判断およびスケジュールの適用まで自動化してしまえば、1時間に1度の頻度などで走らせられます。また、スケールアウトの粒度も人力のときと比べて細かいですから、インフラコストも削減できます。自動化によって完全上位互換の恩恵を受けられるわけです。
Perlのバッチに係数をいっぱい書き連ねて職人の知識を移植
需要予測職人芸バッチは以下の流れで動作します。なお、上記の職人芸をやったときから、この自動化を行うまでにEC2からECSに移行しているため、AWS Application Autoscalingに対して適用しています。
- バッチの起動時刻の30分前から90分後までに開催する大会を探索
- エントリー人数をもとに必要な台数を割り出す
- スケールアウトもしくはスケールインを適用する
以下、なぜこの様になっているかを見ていきます。
1. バッチの起動時刻の30分前から90分後までに開催する大会を探索
30分前から探索しているのは、すでに開始予定時刻を過ぎた大会であってもまだトーナメント表を公開していないケースがあるからです。トーナメント表作成時に位置を調整したり、チェックインに遅れた人をDMを契機に手動で追加するなどの救済措置を取っている主催団体もあります。その時間を鑑みて30分前から見ています。
2. エントリー人数をもとに必要な台数を割り出す
このバッチには、MAX_PARTICIPANT_BY_TASK
という定数が設定されています。これは今までの大会から、1タスクあたりどれぐらいの人数がさばけるかを算出した値です。各大会のエントリー人数をこの値で割って、必要な台数を計算します。
しかし、ゲームや主催団体、また大会形式によって係数をかけています。ゲームによってはチームで参加する大会もあります。この場合、エントリーしている人以外に、チームメンバーもトーナメント表を見ることが予想されます。そこで「このゲームはこの人数で対戦するルール」とわかっている場合には、エントリー人数に人数分の係数をかけています。
また、注目度が高い主催団体が大会を開催される場合、多くの観戦者がトーナメント表を見に来る場合があります。これも係数で特定の主催団体に対して係数をかけています。
そして大会形式ですが、Tonamelの場合、シングルエリミネーション, ダブルエリミネーション, スイスドロー, フリーフォーオール形式に対応しています。この中で、シングルエリミネーションはキャッシュを効かせてある程度チューニングしていたのですが、スイスドローに関してはそれがなかったため、多くのCPUをトーナメント表の構築に使うため、多めに係数を設定していました。フリーフォーオールは、他の形式では1vs1のフォーマットであるところ、最大99人が1つの大戦カードに入れるため、多くのCPUをチャットのポーリングで消費するケースがあります。そこで係数で多くの負荷を消費するように設定しています。
参考まで、コード内で主催団体と大会形式で係数を設定しているところを貼ります。
3. スケールアウトもしくはスケールインを適用する
こちらは、AWS::CLIWrapperを用いて、ECSの台数を調整しています。
1時間から30分程度の定期実行でやれるので、スケジュールを登録するのではなく、バッチ実行時にスケールアウトを行うようにしています。
以上のバッチを適用し、ECS化で唯一残っていたEC2のデプロイ用のサーバのcrontabに30分毎に適用するようにしました。こちらも後日、完全ECS化を達成するために、ecscheduleを用いて、30分毎にバッチコンテナを起動して上記の挙動を実現しています。
実際の挙動を見てみる
これは先日行われた大会での台数の遷移の様子です。10:30に開始予定で、エントリーは216チーム x 5〜7人のゲームです。この大会は多くの人がTwitterでのリンク拡散からトーナメント表を見に来るので、主催団体の係数が適用されて、ドバっと台数をあげてスパイクに備えています。
一方でリクエスト数はこちらです。冒頭に上げたスパイクの例と同じものです。10:10から10:40の30分間に分間リクエスト数が10倍程度まで増加しています。
WebアプリケーションのCPU負荷とレスポンスタイムを見てみましょう。09:30ごろからスケールアウトバッチが反応して台数をあげています。はじめのほうは無駄ですが、スパイクのときに余裕にある程度を余裕を持った程度のCPU負荷でさばけているので、このスケールアウトはバッチリだったと言えます。
また、スパイクに耐えたあとは、最低台数を引き下げて、あとはオートスケールの負荷追従に任せます。これで勝手に今の負荷に最適な台数までスケールインされます。
レスポンスタイムを見ても、スパイクの発生時にp99のメトリックでも詰まった様子はありません。ちなみに他の時刻では跳ねているのは、ユーザーのリクエストを返しているのとは別のターゲットグループのメトリックです。
ここからのさらに改善としてTonamelのデータ基盤のデータからより良い精度の需要予測を与える話もあるんですが、まだ自動化された職人芸でも回っております。
まとめ
以下に実践したことをまとめます。
- スパイクアクセスのために事前に需要予測するのと、ナレッジを蓄積
- 需要予測のナレッジをプログラムに移植し自動化
- サービス運用の人的負荷の削減とともに、インフラコストの最適化と「大規模大会にいつでも耐える」というサービス自体の価値向上
SREingとしての定量的な観察からの可用性の向上とトイルの削減を行うと、サービスの価値が向上したよという話でした。最後に、職人芸の自動化に取り組んでくれた同僚の @koluku に感謝します。