こんにちは、_kenshです。
これから年末年始に入っていきますが、お気に入りのゲームを夜通し楽しんでも誰にも文句を言われない冬休みに入る方も多いんじゃないかなと思います!今年も1年お疲れ様でした!
ところで、タイトル回収しないといけないのでそろそろ本題に。
ちょうど、ゲームでマイクロサービスを使っていくことについての Advent Calendar 良記事が上がってました。
これらの記事に対する Answer Song として "ゲームとイベント駆動アーキテクチャ" について書いてみたいなと思います。
記事 マイクロサービスとゲーム では、いろんなゲーム開発で突き当たる壁について紹介されていました。
- スケールの壁 => スケーラビリティの確保
- コケた時の壁 => 障害影響範囲の最小化
- デプロイの壁 => デプロイ粒度、依存度最小化
- 開発選択肢の壁 => ポリグロット、ビルディングブロックによるサービスオーナーシップの獲得
各種の言葉を自分なりに置き換えてみました。(詳しくはマイクロサービスとゲームを参照)
記事 マイクロサービスとトランザクション では、対価と報酬のやり取りを、スタンプシートという抽象化を行い、発生するトランザクションを厳密なトランザクションからゲームのロジックやトレードオフに沿った、結果整合による解決を図っていました。
2つの記事の内容を理解したい。
マイクロサービスとトランザクション での結果整合による解決は視点を変えると、イベント駆動によるトランザクションの対処と考えることもできます。
ゲームではクエストクリア時に「経験値」と「ゲーム内通貨」を手にいれるというような複数の報酬は一般的に存在します。
GS2 では、ここをジョブキューを挟むことで完全性を両立しています。
具体的には「経験値を入手する処理」と「ゲーム内通貨を入手する」という2つのジョブをジョブキューに登録する という1つの報酬にしてしまいます。
これによって、スタンプシートが実行できたタイミングではジョブキューにジョブが登録された状態になり、その後ジョブキューが報酬付与の処理を順番に処理し、成功するか回復不能なエラーが発生するまでリトライを続けることで完全性を保証します。
この時も Duplication Avoider を指定して報酬付与を行うことで、ジョブキュー内のジョブが複数回実行されるようなことがあっても、報酬が付与されすぎることがないようになっています。
この記事では具体的な要件とそれの解決策としてコレオグラフィー型のアプローチをとっています。
この図のように、前段のサービスの責務はユーザーからの要求を受け止め、キューに要求を登録するところまでになります。あとは自律的なサービスA,B,Cが良しなにそれぞれの独立した責務を果たしてくれるはずです。前段サービス側は後段サービスのエラーに対して無関心になれるように、後段のサービスはそれぞれ自律的にエラー対処します。例えばリトライやメッセージの退避処理などを行い、それぞれの必要な信頼度まで対処を行います。
アーキテクチャ的な恩恵としては、上に挙げた4つの何が得られているでしょうか? 一つずつ見ていきます。
- スケーラビリティの確保
- 前段のサービスはキューにメッセージを入れるまでが仕事となり、FATで大規模なビジネスロジックを持つ必要がない
- 後段のサービスは、それぞれ自身に見合ったスケーラビリティを確保すれば良い。スループットやレイテンシー要件も個々のサービスごとに要件を持つことができる。
- 障害影響範囲の最小化
- 仮に前段のサービスが Fail 状態になったとしても、すでにキューに登録されているメッセージについては、後段サービスは前段サービスの状態を意識せずに自律的に処理を続けることができる。
- 同様に後段のサービスの一つが Fail 状態になったとしても、前段のサービスや他の後段のサービスは自律的に処理を続けることができ、疎結合が保たれている。
- デプロイ粒度、依存度最小化
- 後段のサービスをリリースする際に仮に停止を伴うとしても、他のサービスには影響を与えない。復帰後、キューに滞留しているメッセージを順次処理すれば良い。
- イベントメッセージスキーマ変更を伴わない、あるいは単純なフィールド追加などの増加、拡大スキーマ変更の場合は、影響なく処理を継続するように実装が可能 (破壊的スキーマ変更を伴う場合は、段階リリースをするなど工夫が必要となる)
- ポリグロット、ビルディングブロックによるサービスオーナーシップの獲得
- 図に登場するすべてのサービスで採用されるミドルウエア、プログラミング言語、データストアはサービス側に決定権をもたすことができ、互いに固有の観点で決定できる。
ゲームにイベント駆動がマッチするか?
ゲームとイベント駆動でよく話に出るのがこちらのテーマになります。
- マイクロサービスはイベント駆動に必要か?
- 同期的なrequest/responseでないとビジネスロジックは組めないか?
- これまでの慣れたツールセットから離れることができないか?
マイクロサービスはイベント駆動に必要か?
ゲーム機能はマイクロサービスとして構築されていないケースも多いと思います。それではイベント駆動にできない?
そんなことは全くなく、モノリシックデザインでも上記のアーキテクチャ的な恩恵を受けることは十分可能です。イベント駆動にするために、例えば既存モノリスからメッセージを発行してキューに入れて、それを新規構築したマイクロサービスで非同期に処理していくといった後付けの疎結合アプローチも取れます。
同期的なrequest/responseでないとビジネスロジックは組めないか?
同期的なrequest/response処理が求められる場所としてまず思いつくのが、レイテンシーにシビアな要求がある場合です。例えば、複数ユーザーが同一画面をシェアして互いの行動を確認しながらオペレーションをするアクションゲームなどです。こういったレイテンシーがユーザー体験に影響を与えるようなケースでは、その体験のメインな操作性の構築をイベント駆動にするのは難しいかもしれません。
ただ、このようなアクションゲームに代表されるレイテンシーにシビアなゲームでも、ポイント獲得がある閾値に到達するとイベントシナリオが発生したり、アイテムがランダムに登場したりする機能の構築にはイベント駆動が向いていると思われます。
よって、同期的なrequest/response処理と、非同期的なイベント駆動処理を合わせて適材適所に使っていくことが良いアーキテクチャだと考えることができます。
一部の機能を非同期に処理することにより、同期処理により多くのコンピュートリソースを割いてパフォーマンス最適化を図ることもできると思います。つまりイベント駆動処理を導入することにより、同期処理の体験向上にも繋がるというわけです。
これまでの慣れたツールセットから離れることができないか?
同期系の処理を組むためのツールセットとして、使い慣れたクライアントSDKやサーバーサイドのミドルウエアをお使いだと思います。これらのツールセットはこれまで通り使い続けていただいて大丈夫です。合わせて非同期のためのミドルウエアやサービスとしてキューやストリームサービスとそれを扱うSDKの使っていただくと、多くの部分はこれまでと同じ開発体験を得られますし、イベント駆動にした部分は疎結合であることから、上記に述べた通りポリグロット(多言語、異種環境)にして構築することができます。
そして、レイテンシーにさほど影響を受けない機能で、独立したコンポーネントとしやすいゲーム機能群をイベント駆動に作っていくのは理に適っています。
マイクロサービスとゲームで述べられていたゲーム機能のうち、
- アチーブメント機能
- ランキング機能
などは、直接レイテンシーがユーザ体験に影響を与えるものではないことから、モノリスから切り出して、非同期的なマイクロサービスコンポーネントとして構築するのも良いと思います。
まとめ
ここまで、アドベントカレンダーにすでに投稿されているゲームのアーキテクチャについての良記事2本について、自分なりに勝手にインスパイアされて解説をしてみました。
同期的なアーキテクチャからイベント駆動アーキテクチャを部分導入することから始めてみてはいかがでしょうか?
これからイベント駆動アーキテクチャを実装しようと少しでも思っていただけたら幸いです。
その時に役に立つかもしれない情報としてAWSでイベント駆動を司るサービスの一つである EventBridgeについて発表したスライドがありますので、こちらもご参考ください!