【ソシャゲ】レイドシステムの実装とその技術課題について

# 概要
ソシャゲでレイドシステムというのがある。上手く負荷を捌くにはどうすればいいだろう

# きっかけ
マギレコというゲームで新たにレイドシステムが実装されたのだが、何度も落ちて臨時メンテナンス祭りだ。開発会社は相当優秀であるにもかかわらず。（一年間の運営状況や実装状況をみるに、少なくとも日本の他のモバイルゲームと比較すると圧倒的に技術力がある）。

じゃあレイドシステム固有の難しさとはなんだろう？考察したい。

# 01. ソシャゲの一般的な特徴
本稿で取り上げるソシャゲの特徴。日本のソシャゲは大体当てはまると思う:

- 通信方式
    - HTTP(S) でのステートレスな通信
- 負荷パターン
    - ユーザ数が非常に多い
    - ユーザ間の連携は少ない（フレンドシステムくらい）
    - 突発的な負荷の集中（イベント時・新キャラ実装時など）
- サーバとクライアントの担務
    - サーバ側は重要資産のみを管理
        - 主にユーザの所持品管理
    - クライアントがゲーム機能の大部分を提供

なお、最近では、PUBG, Minecraft のマルチプレイのようにフレンドのアクションをリアルタイムに反映するような即応性の高いソシャゲもあるが、本稿では考慮の対象外とする。

この章で何が言いたいのかというと、**サーバサイドだけ見ると ECサイトとソシャゲは結構似てて同じような負荷対策が有効である** ということ。

# 02. ソシャゲのスケーリング手法
レイドシステムの前に、一般的なソシャゲの性能上の考慮点を記載しよう

## 02-1. シャーディング

日本のソシャゲはソーシャルな側面は非常に薄く、**ゲームシステムのほとんどはシングルプレイ**だ。したがってユーザIDを使ったDBインスタンスの物理的な分割（シャーディング）がぴったりはまり、性能問題は起こりにくい:

- シャーディングで負荷分散できる機能:
    - **ユーザ情報管理機能**: 石とかスタミナとかキャラ一覧とか
    - **ガチャ・ショップ機能**: 課金石やゲーム内通貨とアイテムの交換（購入・売却） 
    - **シナリオプレイ機能**: ステージ情報をサーバから引っ張ってきてプレイ
    - **強化機能**: アイテムを使ってキャラや武器を強化

一方、ユーザID によるシャーディングを選択する場合、いくつかの「ソーシャル」な機能に関してはインスタンスをまたいだ処理が必要になる

- シャーディングで（直接的には）負荷分散できない機能:
    - **フレンド管理機能**: 検索、追加、削除、一覧機能
    - **サポート要員選択機能**: シナリオプレイ時に選択（非フレンドも対象になる）
    - **PvP機能**: 他ユーザとの対戦

これらに関しては、2.3で再考する。

## 02-2. マスタの複製保持
ソシャゲでは、メンテナンス時間を設けることが許容されている。そこで、多くのマスタデータ（ステージ情報・ガチャ情報・武器やキャラマスタ）を、全てのシャードに格納して、マスタDBに負荷が集中しないようにすることができる。新ステージ実装時や、新ガチャ実装時には、すべてのシャードに配置された複製を更新すればよい。これは手間だし、不整合を生じさせうるが、運用でしっかりカバーすれば性能上の問題をだいぶ軽減できる。

ここで一つ補足が必要だろう。ECサイトと異なり、ソシャゲでは（ユーザ情報以外の）マスタデータのサイズはものすごく小さい。なぜならクライアント側にアセットがほとんどあるので。サーバ側が管理するのは、ユーザ固有の情報とかガチャ情報とかステージ情報とかそんなもんだ。したがってシャードDBにマスタデータを配布するという同期作業のコストは面倒ではあるが、それほど時間がかかるというわけではないと指摘しておこう。

## 02-3. クライアント側キャッシュ
シャードをまたぐ機能は性能がスケールしないので、リアルタイムな情報を取得しようとするのではなく、キャッシングするという妥協（という仕様上の制約）が必要。使用頻度の多寡で設計をするのが原則。

サーバサイドでキャッシングするという選択肢もあるが、ソシャゲの場合クライアントが賢いのでクライアント側で実施する方が実装上楽（なぜかを考えてみてほしい）。

- 頻度が低いので対策が不要な機能例
    - **ユーザ検索機能**: 知り合いなどを検索する機能
- 比較的頻度が高いのでキャッシュして、更新頻度を抑える機能
    - たとえば 必要に応じて15分に1回最新化
    - **フレンドサマリ機能**: フレンド一覧とログイン日時情報
    - **サポート要員選択機能**: 他ユーザの候補一覧を更新

## 02-4. レコード分割によるオンメモリ化
これまでは機能に注目したが、それとは別の軸でも負荷を軽減できる。

高頻度でアクセスされる transient な情報に関してはオンメモリになるよう設計するという手法がある。例えば「サポート要員選択機能」は非常に高頻度にアクセスされる（たとえキャッシュによりその頻度を抑えられるとしても）。この時必要になるのはユーザの全情報ではなく、ごく一部の情報のみだ。例えば、

- ユーザ名
- ログイン日時
- サポート要員としての強さ: Lv・ステータス値・装備

DBに格納する際に、フラットにユーザの全情報を詰め込むととてもじゃないけれどメモリに収まりきらないが、上記のような情報程度ならメモリにおさまることができる。これによりディスクアクセスのない高速なアクセスに対応できる。

数字を挙げよう。1シャードあたり 10万ユーザを格納するとする。上記のような厳選データとして 4KB 使うとすると 論理データ長はたったの 0.4GB  だ。一方、ユーザのすべてのデータを格納しようとすると、ユーザ当たり 1MB くらい必要になると思うので、メモリは 100GB くらい必要になり、メモリに乗りきらない。

このように、レコードをよく使う部分と、そんなに頻繁に使わない部分に分けて格納するようにデータをモデリングすることが特にソシャゲでは必須である。（UserSummary テーブルと UserData テーブルのように二つのテーブルに分けるわけだが、1:N ではなく 1:1 の対応関係になる。こういうのは一般にレコード分割とか垂直分割と呼ばれる）

なお、上記のような厳選データは、DBに格納するのではなくキャッシングミドルウェア(memcached や redis)に格納することも可能である（ミドルが異なると整合性のチェックがとりにくくなるという欠点はある）

# 03. レイドシステム
以上の知識をベースにレイドシステムについて考えてみたい

## 03-1. レイドシステムの仕様
あまり詳しいわけではないので何とも言えないが、ここで考察するレイドシステムは以下を想定（マギレコに準ずる）:

- とても強いBOSSが出現
    - 初戦はBOSSを発見したユーザが戦う。
        - 勝てれば終わりで報酬がもらえる。
        - 負けてもBOSSのHPが減った状態で何度も再戦できる
        - 再戦のたびにBOSSのHPは減るので一人でもなんとか倒せる
    - 二戦目以降は救援要請が可能（他ユーザに戦ってもらう）
        - 他ユーザ ＝ 非フレンドも含む
        - 救援要請対象のユーザを指定することはできない
        - もちろん救援要請後に自分が再戦してもよい
        - 自分・他ユーザが累積してBOSSのHPを削り切ったら勝ち
        - 報酬を山分け（与えたダメージで比例配分）
- 累計BOSS打倒数で報酬がもらえる
    - 500万体倒したらユーザ全員にガチャチケットをあげるとか
    - 自分が発見したBOSSの打倒数でも報酬がもらえる（50体倒したらガチャチケットとか）

##03-2. レイドシステムの技術課題
上述のレイドシステムを実装する上での技術的課題を洗い出そう。

**a. 状態を持つステージ**
既存のゲームでは（少なくともサーバサイドでは）ステージは状態を持たなかった。つまり、クライアントからステージ X をチャレンジするよ、とリクエストが来た場合、ユーザのスタミナを必要分減らして、ドロップアイテム等の情報を生成してクライアントに情報を返すだけでよかった。あとはクライアントサイドでゲームが進行する。ステージクリアか失敗かの情報がサーバに来ればそれに応じたユーザ情報の更新を行えばよい。
一方、**レイドシステムはこのようなステージとは概念が異なる**。残HPであったり、誰がどの順番で何回挑戦し、どれくらいのダメージを与えたか、それらの情報をサーバサイドで覚えておかねばならない。500万体のBOSSがいればそれだけの情報を覚えておかねばならないのだ。

**b. 実装基盤の抜本的な変更**
前述の考察を進めると、レイドシステムのためのサーバが新規に必要になる、と想定される。というのも、500万体のBOSS分のレイドの状態を保持し、時々刻々その状態をアップデートするというのは、全く異なる負荷パターンを結構なスループットで捌かなければいけないからだ。1時間に 10万体のBOSSが倒されるとなると、 ざっくり計算でも 1時間当たり 100万件位のデータのアクセスが要求されるのではないだろうか？そうなると1台のレイドサーバでは厳しく、複数台のレイドサーバを用意する必要がありそうで、しかも（ユーザIDでなく）レイドIDでのシャーディングが必要になりそう。このように考えると、実装基盤に対する変更は非常に大きくなると想定される。

**c. キャッシュ効果の薄さ**
既存のステージプレイでは、シャードをまたぐような重い処理はサポート要員の選択程度だ。これは何回ステージにチャレンジしようと、15分に 1回リフレッシュすれば十分であり、性能上の問題にはなりにくかった。
一方、レイドシステムではこの描像にならない。次々とBOSSが登場するので、ある意味レイド情報は使い捨てで、どんどん情報が必要になる。クライアント側のキャッシュが効きにくい構造になっているといえる

**総括**
レイドシステムの実装には、特にサーバサイドにおいて、これまでのイベントとは別次元の難しさや困難さが伴うと想定される

# 03-3. レイドシステム実装Tips
まあ、私は作ったことがないので勝手な妄想でしかないが、もし実装する立場になったら以下のような点を気を付けようと思う

**レイドの排他制御**
排他制御を厳密に行わない。つまり救援要請があった BOSSと戦う際にロックをとらない。すると同時に同じBOSSと戦うユーザが出てきてしまう可能性があるが、そこはOKとしよう。100 のHPだった場合、3人が100を与えてしまったら、報酬を3人に分け与えればよい。報酬を三等分するのではなく、3倍にするということ。

排他制御なんかしたら、BOSSと戦うボタンを押した際に、「他の誰かが戦っています。別のBOSSを選択してください」というださいメッセージをださなければいけなくなる。これは絶対避けたいシチュエーションだ。


**レイドの生成**
レイドはどのタイミングで生成されるのだろうか。一番ナイーブに考えると、ユーザがレイドサーバに create 命令を発行することだ

マギレコでは、BOSSの手下をクリックすることでサーバ側に刺激を送り、確率的にBOSSの生成を促しているように見える。サーバサイドで１ユーザあたりの最大同時BOSS数の確認をして、条件を満たせばBOSSを生成してそのIDをユーザに紐づけるのだろう。これはなかなかうまいやり方だな、と思ったが、手下をクリックするたびにサーバとの通信が発生しあまりよいUXとは言えなかった。

私なら、イベントを開始したユーザのクライアントが、裏で非同期に事前にいくつかのレイドを予約して、クライアント側で確率的にBOSSの存在を明らかにする。これならブロッキングな通信の発生頻度を抑えられるだろう。

**レイドのファイナライズ**
ファイナライズのトリガを引くのは、BOSSを倒したユーザのクライアントでよいと思うのだが、排他制御をしていない場合、トリガが複数回引かれる可能性がある（二つ前の排他制御の項を参照）。配分が二重にならないように配布フラグをつけるなどしておく必要があるね！

# まとめ
レイドシステムの実装上の困難さを考察し、実装時の考慮事項を検討した。