本記事はCraft Egg Advent Calendar 2021の12/14の記事です。
12/13の記事は @reo_chocsarさんの 「エディターのショートカットとデバッグ機能を全く使ってこなかったので、エンジニアになって痛い目を見た話」でした。
はじめに
株式会社Craft Eggでサーバサイドエンジニアをしている高川です。
タイトルが主張ちょっと強めですが(笑)、今回は技術というよりも、スマホゲームのサーバサイドをやっていて感じたことを書きます。
ゲームをプレイしていると当たり前な機能も、1つずつ紐解いてみると、やっていることが多く、面白みがあります。
※ なお、ゲームのジャンルによって性質が変わるため、今回はゲーム全般で共通しそうなことに絞ってます。
スマホゲーム業界に興味がある方に少しでも役に立つ記事になればと思います。
驚きポイント
(記事のタイトルがタイトルなので…)
驚きポイントをいくつか挙げたのちに、それぞれのポイントで工夫していくこと/工夫できることを書いていきます。
1. とにかくユーザーデータが多い
ゲームという特性上、向き合わなくてはいけない課題の一つ目です。
ゲームは新規機能をリリースするスパンが結構早いです。
既存機能の改善もしますが、新しい機能を作ることの方が多く、その度に必要なテーブルが増えていきます。
私が関わっているプロジェクトでは、ユーザーのデータを管理するテーブルだけで100テーブル以上存在します。
さらに、その中でもレコード数が数十億超えるテーブルもいくつか存在します。
2. ユーザーデータの更新が多い
ゲームという特性上、向き合わなくてはいけない課題の二つ目です。
ログインした、インゲーム(メインのゲームパート)をクリアした、プレゼントを受け取った、強化をした、シナリオを読んだ、ガチャを回したなどなど
ユーザーのデータが変化するタイミングは盛り沢山です。
そして、そのたった一回のタイミングでも、更新しなければいけないデータがたくさんあります。
例として、インゲームをクリアした時に、更新しなければいけない情報をざっと思いつく限り書いてみました。
- スタミナの消費
- インゲームクリア情報保存
- ハイスコアの更新
- クリア報酬アイテムの付与
- プレイヤーランク経験値付与
- 編成メンバーの経験値付与
- イベントポイント付与
- イベントポイントの累計に応じた報酬の付与
- ランキングに反映
- ミッション進捗の更新
- チート情報の更新
3. API通信が少ない
よくあるシステムでは、画面が遷移するたびにサーバにAPIを投げてその画面で必要な情報を取得することが多々あると思いますが、UXがかなり悪くなってしまいます。
特に、ゲームでは各画面でさまざまなゲーム内リソースを表示する必要があり、一回の通信も重くなるので、都度通信行うのはユーザーの離反につながります。
工夫できること
結構当たり前なことも多いのですが、気をつけるべきポイントやサーバがダウンしないようにするために工夫していることを書きます。
特に、バックエンドにおける一番のボトルネックはDBのI/Oなので、DB周りは気をつける必要があります。
シャーディング(水平分割)
シャーディングとは、同じテーブル構成のDBを複数用意して、ユーザーIDなどに応じて別々のDBに保存していく手法です。
(MMORPGなどでサーバが分かれているのと似たような感じですね)
ゲームだけに限らず、大規模サービスであればシャーディングを導入しているところは多いと思います。
別々のDBに保存できるので、DBアクセスを分散させることができます。
また、数十億あるユーザーデータもいくつかに分散できるので、データ量が増えることによるパフォーマンス劣化もある程度防ぐことができます。
最近は、複数のDBを一つのDBとして扱ってくれる、マネージドRDBのCloud Spannerが良さそうなので、導入も検討したりしてます。(ちょっとお値段高めなのが悩ましいですね)
適切なインデックス
インデックスとは、テーブルの特定のカラムにWhere句を使って検索するときの効率化を行うものです。
主キーもデフォルトでインデックスになっています。
適切なインデックスを張っていないカラムを検索する場合、上から下まで全部見る必要があるので、データ量が多いと一回のSELECTでも相当な時間がかかってしまいます。
以前、適切なインデックスが張られていないカラム検索をするクエリが大量に発生して「DBインスタンスのCPUが100%、空きコネクションなし」という状態になり、サーバが落ちるという苦い経験がありました…
適切なインデックスを張るには、プログラム内で実行するSQLを把握することも大事ですし、そのクエリの実行計画を確認することも必要になります。
// 実行計画
explain select * from table where column1 = 〇〇 and column2 = □□;
機能に合わせたテーブル設計
運用が長くなると、ユーザ数の増加につれてデータ量も増えていくので、パフォーマンス劣化は免れないです。
拡張性が高い設計にするのか、パフォーマンス重視の設計にするのかでテーブルの設計もかなり変わってきます。
拡張性を高くしたいのであれば、一つのテーブルで複数の表現をできるようにするので、レコード数が増えて縦に長いテーブルになります。
パフォーマンス重視にしたいのであれば、必要なデータを一つのレコードに詰め込み、ユーザIDだけで検索/更新できるようにするので、カラムが増えて横に長いテーブルになります。
ソフトウェア品質の観点から見たときに、拡張性がある設計は非常に大事ですが、ゲーム仕様的に拡張性があっても意味がない場合も多々あります。(デッキは5枚が上限で6枚以上にならない、マルチプレイは4人が上限など)
この場合は、割り切ってカラムを増やすという設計にして、パフォーマンスを選ぶという選択も良いと思います。
データをまとめて返す
APIの通信を減らすために、ユーザーの保持しているゲーム内リソースに関する情報を一回にまとめてクライアントに返しています。
ログインAPI | そのユーザーのユーザーデータを全てまとめて渡す |
リソース状態が変わるAPI | 変更された箇所のユーザーデータだけを渡す |
変更された箇所だけ返すので、通信量も少なく実現することができます。
その分、ログイン時のAPIは重くなってしまうのですが、全体的に重たいよりは部分的に重い方が体験としては良いという思想です。
まとめ
個人的に感じた内容を書き連ねてみました。
ゲームは、ユーザーとの距離がかなり近く、どんどん新しい機能が追加されるサービスなので、やはりユーザーデータ周りの管理が非常に大事だと思います。
ゲームならではというよりは、大規模なサービス寄りの話になってしまったかと思いますが、スマホゲーム事業のサーバサイドに興味がある方々に少しでも参考になる情報があると幸いです。
ここまで読んで頂き、ありがとうございました。
Craft Egg Advent Calendar 2021 明日は @ce_gassyさんの記事になります!