「CEDEC2024」講演レポート、全世界同時リリースに向けた多言語対応の仕組みも解説
目指すは“毎秒20万リクエスト”対応、「FF7エバークライシス」におけるパフォーマンス向上策の数々
2024年09月02日 10時00分更新
2024年8月21日から23日にかけて、ゲームを中心としたコンピューターエンターテインメント開発者向けのカンファレンス「CEDEC2024」が開催された。本記事では、「『FINAL FANTASY VII EVER CRISIS(以下FF7エバークライシス)』全世界同時リリースを支えた大規模負荷に耐えるハイパフォーマンスなゲームサーバーの仕組み」と題した、セッションの様子をお届けする。
FF7エバークライシスのゲームサーバーに求められた要件は、「“250万DAU(デイリー・アクティブ・ユーザー)”に耐えられる構成」かつ、「全世界同時リリース」の実現だ。開発を担当したのはサイバーエージェントのゲーム・エンターテイメント事業部(SGE)に所属する子会社のひとつ、アプリボットである。
同ゲームにおけるバックエンドのリーダーを担当した永田員広氏から、独自開発のORM(Object Relational Mapper)を用いたデータベースアクセス削減を始めとする、パフォーマンス向上のための数々のアプローチ、そして、多言語対応の取り組みなどが語られた。
目標は負荷試験による「20万rps」達成、全世界同時リリースに向けた多言語対応
FF7エバークライシスは、2023年9月7日にスマートフォン向けにリリースされたFINAL FANTASY VIIシリーズのコンピレーションタイトルのひとつ(2023年12月にはSteam版もリリース)。原作シリーズから完全新作オリジナルストーリーまでを楽しめ、戦闘はアクティブターンコマンドバトル制となり、マルチプレイにも対応する。
前述の通り、同ゲームのサーバーに求められた要件は、250万DAUに耐えられること、そして全世界同時リリースだった。アプリボットではそれぞれの要件に対して「負荷試験による20万rps(Request Per Second)の達成」、「多言語対応」という目標を立てた。
まずは、クライアントとのメインの通信を行うゲームサーバーにおけるパフォーマンス向上の取り組みが紹介された。
ゲームサーバーは、AWSをクラウドサービスとして利用して、言語は「Go」を、フレームワークは「Echo」を用いて実装している。データベースは「Aurora MySQL」を利用して、ユーザー別の水平分割でシャーディング対応。キャッシュサーバーは「ElastiCache For Redis」を利用して、ワンタイムトークンによるセッション管理を行う。
なお詳細は触れられなかったが、オンラインでのリアルタイムバトルを担う「GameLift」を利用したC#で書かれたマルチバトル用サーバーや、「Octo」と呼ぶ社内のアセット配信基盤も、同一のAWS上に構築されている。
データベースアクセスの削減:リクエスト単位でキャッシュする独自開発のORMを活用
20万rpsの達成に向けて、アプリボットは「データベースアクセスの削減」「CPU負荷の軽減」「メモリ使用量の最適化」の3つの施策に取り組むことを基本方針として、APIを実装した。
最も注力したのは「データベースアクセスの削減」だという。これには、Go言語で社内開発した「ContextCashedORM」ライブラリによるキャッシュ管理が大きく寄与したという。
ContextCashedORMは、ゲームに必要な処理に特化したORMであり、APIリクエスト単位でcontext中のインメモリにキャッシュを保持する。キャッシュの有効期間はリクエスト中のみで、リクエストごとに破棄される仕組みだ。
キャッシュ管理では「読み込みキャッシュ」「書き込みキャッシュ」「バルク処理」の3つの機能を用意している。
まずは、データベースから取得したデータを、テーブルキャッシュとしてメモリ上に保持する「読み込みキャッシュ」の仕組みと工夫から解説された。
テーブルキャッシュ上に何もない状態で、特定ユーザーのアイテムを全取得する検索クエリが発行された場合には、データベースから取得したデータをテーブルキャッシュに保持する。これにより、データベースへのアクセス数を減らせる。
ただし、テーブルキャッシュだけを管理するのであれば、存在しないアイテム(item_id)を取得する際に“キャッシュに存在しない”のか“データベース自体に存在しない”のかが判断できない。そこで、テーブルキャッシュだけではなく、検索時のクエリ条件を保持するクエリキャッシュも管理する仕組みにした。
このクエリキャッシュは、SELECT文のWHERE句以下の条件をクエリ条件としている。AND条件において、部分一致するクエリ条件の検索結果は、特定のクエリ条件の検索結果を含んでいるという関係性を活かして、新規で発行するクエリ条件に対して、クエリキャッシュに部分一致するクエリ条件が存在する場合には、検索対象がキャッシュ済みであると判断することできる。
キャッシュしていないと判断した場合は、データベースからデータを取得してテーブルキャッシュに追加する流れとなり、「検索済みの場合は、データベースアクセスを一切発生させない仕組みができた」と永田氏。
続いては、データベースの更新・追加・削除などの処理をキャッシュ上で管理する「書き込みキャッシュ」の仕組みだ。これらの更新処理は、テーブルキャッシュ上の“レコード状態”を管理することで実現した。
レコード状態は、データベースから取得した未変更のデータであれば“SELECT”、更新する必要があるデータであれば“UPDATE”、新規追加する必要があるデータであれば“INSERT”、削除する必要があるデータであれば“DELETE”と定義。この各レコードの状態を、書き込み処理の際に適切に設定する。
例えば更新処理の場合は、更新したいテーブルキャッシュのデータのステータスが“SELECT”から“UPDATE”に変わる。「ただし更新処理をした場合に常にステータスが変わるわけではない」(永田氏)といい、更新対象の状態がデータベースに追加予定の“INSERT”の場合は“INSERT”のままで更新され、“DELETE”の状態やキャッシュ自体に存在しない場合は、エラーを出力する。
最後に、書き込みキャッシュに保持する複数の処理を最低限のクエリ数でデータベースに書き込む「バルク処理」の仕組みだ。テーブルキャッシュ上のすべてのデータをレコード状態単位で集計して、更新・追加・削除に分けてクエリを発行、それぞれまとめてバルク処理で書き込む。「クエリの発行数をひとつのテーブルで最大3件に収められ、書き込みクエリ数を大幅に削減できる」と永田氏。
バルク処理は、すべての検証が終わったAPIの最後の処理として行われるため、エラーによるロールバックが発生しないのも特徴だ。
ContextCashedORMでレコード状態を把握することで、「ユーザーデータの差分同期」も可能になった。本ゲームでは、ログイン時にユーザーの全データをクライアントに返却しているが、すべての通信の共通レスポンスとして、更新・追加・削除したレコードを返すことで差分同期を実現した。不要なデータベースアクセスや通信コストの削減につながり、基盤で対応していることから、サーバー・クライアント間のユーザーデータの整合性も向上している。
事前ロードでの検索クエリの最適化も実現した。これは、ContextCashedORMの読み込みキャッシュ機能を用いて、事前にまとめてデータベースの情報をテーブルキャッシュにロードしておく仕組みによるものだ。報酬付与・消費や条件チェックなどのゲームでよく利用される共通処理基盤に適用することで、処理を変更することなく、検索クエリを削減することができた。