KMS の裏側

今回は、以前このブログで紹介した KMSについて、そのサーバサイドの構成を紹介したいと思います。

KMS の基本的なところ

汎用性

KMS は、チャットシステムを手軽に実現するための、SaaS型サービスとして企画されました。 つまり、1つの KMS のシステムを複数のゲームアプリが利用することが前提にありました。 そのため KMS は汎用性を常に意識して設計しています。

汎用的なシステムとして設計したため、KMS ではチャットに関するもの以外のデータ、例えばゲーム内のギルド情報などのデータは扱いません。 一方で NGワードなどのチャット機能に必要なデータは、利用ゲームごとに個別に保持し、必要に応じてゲームサーバから更新できるようにしています。

チャンネルを軸としたシステム

KMS はメッセージをやりとりする場であるチャンネルを軸としたチャットシステムです。 ユーザは特定のチャンネルに所属することで、そのチャンネルに対して発言することができるようになり、また同じチャンネルに所属する他のユーザが発したメッセージを受け取ることができます。 ユーザが発言したメッセージは、リアルタイムに他のユーザに転送されます。

KMS のチャンネル構成は、KMSを利用するゲームアプリ次第です。 例えば、自由に参加できるワールドチャットや、ギルドやチームに参加するユーザに限定したチャット、また 1 対 1 の個人チャットなどの用途が考えられます。

ユーザ認証

KMS はゲーム内で使うためのチャットシステムですので、クローズドなチャットです。 つまり、クライアントが KMS のサーバに接続する際は、ユーザ認証を実施する必要があります。 ユーザ認証に必要なデータは KMS 自体では生成せずに、ゲームサーバ側から受け取る形にしています。

KMS サーバの構成

KMS のサーバサイドの主要部分は、次の4種類のサーバで構成されています。

KMSの構成

UA サーバ

UA とは User Agent の略で、 クライアントアプリが接続する先のサーバです。 ユーザの発言をリアルタイムに他のユーザへ配信するために、クライアントアプリとサーバとの間の通信プロトコルとしては WebSocket を使っています。

構成としては、フロントエンドに nginx を置き、バックエンドには tornado を使っています(※)。 また tornado のスーパーバイザとして Circusを使っています。 nginx と Circus = tornado の間は Unix Domain Socket で接続しています。

※ フロントエンドサーバ、バックエンドサーバ
KLab では Web アプリケーションサーバを立てる際、フロントエンドとバックエンドの 2種類のサーバを動かします。 クライアントからの接続はフロントエンドのサーバが受け付け、バックエンドのサーバに proxy する構成です。

バックエンドのサーバは、Web アプリケーションの本体を動作させる環境になります。

フロントエンドのサーバの役割はいくつかあります。 例えば

  • アクセスログの標準化
    • tornado や uwsgi など、異なるバックエンドを併用する場合でも、アクセスログの形式を揃えられる
  • バックエンド処理の、タイムアウト管理
    • バックエンドの処理に時間がかかりすぎている場合に、フロントエンドがそのリクエストを強制的にエラーにすることで、接続リソースを開放する
  • クライアントリクエストのパーキング
    • 大量アクセス時に、全てのリクエストを並列に処理するとスラッシングなどの状態に陥るので、バックエンドの並列数は絞りつつ、クライアント接続をパーキングすることで混雑時のユーザエクスペリエンスの低下を緩和する

などがあります。

フロントエンドとバックエンドのサーバは、前記のようにその間を Unix Domain Socket で接続するので、同一のマシン上で動作させています。

API サーバ

ゲームサーバからのリクエストを受け付けるサーバです。 こちらはプロトコルとして HTTPS を使っています。 ユーザ認証用のデータなど、クライアントアプリが KMS を利用する上で必要なデータの追加更新削除の操作に加えて、ゲーム運用者がチャット上のデータを参照したりするためのリクエストも、この APIサーバが担当します。

構成としては、フロントエンドに nginx を置き、バックエンドには uwsgi を使っています。 こちらも、nginx と uwsgi の間は Unix Domain Socket で接続しています。

IRC サーバ

UA サーバ間及び、UAサーバと API サーバの間でのメッセージ配信用基盤として利用しています。

UA サーバも API サーバも、当然ながら複数台のサーバマシンを用意しています。 なので、 例えばサーバ S1 に接続している U1 ユーザが C チャンネルに対して発言した内容を、同じ C チャンネルに所属している S2 サーバ上の U2 ユーザに配信するためには、 S1 サーバと S2 サーバの間でメッセージを転送する必要があります。 そのための基盤として KMS では IRC サーバをチューニングした上で利用しています。

また、ユーザの発言を UA サーバ間で配送するためだけでなく、ゲームサーバがAPIサーバを通じてチャットへ送ってくるシステムメッセージの配信や、チャンネルへのユーザの入退室情報の配信などにも、IRCサーバを利用しています。

DB サーバ

ユーザ認証情報や、 チャンネルに対して発言されたメッセージのログや、ユーザが所属するチャンネル情報などを保管しています。 MySQL を利用しています。

KMS サーバアプリのバージョン管理

KMS は、複数のゲームアプリが利用するシステムです。 当然ながら KMS とゲームアプリの開発および運用は独立しています。 ですので KMS がバージョンアップしたからと言って、それに合わせてゲームアプリのバージョンアップを強制することはできません。

そのために KMS では UA サーバ・API サーバ共に、クライアントがサーバに接続する際に利用したい KMS バージョンを選択できるようにしています。 この仕組みにより KMS では下位互換性の無い改修を新規バージョンに施すことを容易にしました。 極端な話をすれば、利用するゲームアプリ専用バージョンの KMS を用意して同時に運用することも、可能になっています。

バージョン選択の仕組み

クライアント側からの、KMS サーバのバージョン選択は、次のような形で実現しています。

バージョン選択の仕組み(UAの場合)

まず、tornado(UAサーバ)と uwsgi(APIサーバ)上では、KMS を利用する全ゲームアプリが利用する予定のバージョンのサーバアプリを全て動かしています。 これらのサーバアプリと(フロントエンドの) nginx の間は Unix Domain Socket で接続しますが、この待ち受け用の Unix Domain Socket をサーバアプリのバージョンごとに用意しておきます。 つまり、バージョン1.1 とバージョン1.2 のサーバアプリが平行稼働していた場合、バージョン1.1用の待ち受けソケットと 1.2用の待ち受けソケットの2つの待ち受けソケットができます(※)。

※待ち受けソケット

Unix Domain Socket なので、待ち受けソケットとは、具体的にはファイルシステム上に配した socket ファイルのことです。

クライアントからの KMS サーバへの接続は、最初にフロントエンドの nginx が受け付けます。 その際にクライアントは利用したい KMS バージョンを、URL の中に埋め込みます。 nginx はその URL を見て、クライアントからの接続をどのバージョンのサーバアプリにプロキシーするかを判断し、適切な待ち受け用の Unix Domain Socket へ接続を回します。

この仕組みにより、KMS を利用するゲームアプリは、任意のタイミングで、利用する KMS のバージョンを変更することができます。 また KMS の運営側としては、利用するゲームアプリに気兼ねすること無く、新しいバージョンのサーバアプリをデプロイすることができます。

KMS の裏側の裏話

ここまでで説明した構成は、実装前の設計段階で固まっていて、大きな変更はありませんでした。 しかしながら、細かいところでは当然ながら初期の設計どおりではまずい箇所がいくつか出てきました。 また、負荷テストを実施してみると思わぬところがボトルネックになっていることが分かりました。

IRC サーバと UA の関係

最初期の構想では、UA の役割はクライアントと IRC ネットワークの間の橋渡し役として、ユーザ認証と NG ワードチェック、そして参加チャンネルの管理程度に留めたいと考えていました。 そのため、クライアント = UA 間の接続に 1 対 1 対応する UA = IRC 間の接続を作り、接続クライアントの数だけ IRC ネットワーク上にユーザを登録する構成を考えていました。

この方式では、UA を動かしている tornado が管理する接続の数は、1クライアントにつき2つの接続になります。 当然ながら tornado が管理する接続の数が増えるとそれだけリソースを消費するため、さばけるクライアントの上限が低くなってしまいます。 性能テストをしてみたところ、このクライアント接続に 1 対 1 対応する IRC 接続を作るという設計では、目標とする性能を実現できませんでした。

この問題に対処するために、当初の設計を変更して、UA と IRC の間の接続は 1つで済ませることにしました。 これに伴って、当初の設計では IRC の機能に依存していたクライアント間のメッセージの適切な配送機能を、UA 上にも実装する必要に迫られることになりました。

UA のチューニング

KMS では、クライアントと UA の間の接続は WebSocket を使った常時接続です。 前述したように、同時に取り扱う接続の数が多ければそれだけ tornado のリソースを消費することになります。 また UA の応答性はクライアントのユーザビリティに直結します。 そのため、UA チューニングには特に時間を費やしました。

その中で実施した施策の1つに、PyPy の導入があります。 PyPy は python インタプリタ実装の 1つで、 JIT を実装しているため標準のインタプリタである CPython よりも実行速度が期待できます。 PyPy の導入によりパフォーマンス向上の効果は一定ありましたが、それでもまだ目標とするパフォーマンスには到達しませんでした。

そのため、ひたすら cProfile プロファイリングを取得して、ボトルネックを解析し、対策を施しました。 その際に活躍したのが、 KCachegrind です。 KCachegrind を使った解析により、主なボトルネックとしては JSON の解析部分と tornado のマルチタスク処理部分であることが分かりました。

JSON

KMS はクライアントと UA の間や、(IRC を通じた) UA 同士、UA と API の間などの通信の全てにおいて JSON を使っています。 そのため JSON 処理の高速化が肝になりました。

一般的には JSON エンコードには json.dumps() を使うかと思いますが、その実装の実体は json.encoder.JSONEncoder クラスの encode() メソッドになります。 json パッケージでは json.dumps() 呼び出し時の JSONEncoder クラスの初期化コストを抑えるために、パッケージの初期化時にこのオブジェクトをキャッシュしますが、このオブジェクトキャッシュが使われるのは json.dumps() のデフォルト引数群に、デフォルト値以外の値が渡されない場合だけです。 KMS では separators オプションを指定したかったので、 独自に json.encoder.JSONEncoder クラスのオブジェクトをキャッシュしておくようにしました。

また、PyPy の JSON エンコードライブラリでは呼び出しオプションの与え方によって、その実装の一部分で C言語実装のコードが使われる場合と Python 実装が使われる場合があります(※)。 C言語実装が使われるか否かは、json.dumps() の引数 ensure_asciiTrue (default) か False かで変わります。 True であれば C言語実装が使われ、False であれば Python 実装が使われます。 当然ながら Python 実装版よりも、C言語版の方がパフォーマンスが高いので、コードを見直して必ず C言語版の実装が使われるようにしました。

※ JSON encoder の実装切替ポイント

前述の通り json.dumps() の実装の実体は json.encoder.JSONEncoder クラスにあります。 このクラスのコンストラクタ中で、 ensure_ascii 引数の値によって利用する実装を切り替えている部分(160行目)があります。

        if ensure_ascii:
            self.__encoder = raw_encode_basestring_ascii
        else:
            self.__encoder = raw_encode_basestring

この raw_encode_basestring_asciiraw_encode_basestring の実装の実体はそれぞれ同じファイルの49行目40行目にあるのですが、 raw_encode_basestring_ascii だけは更に536行目付近の

try:
    from _pypyjson import raw_encode_basestring_ascii
except ImportError:
    pass

このコードで、C言語実装をロードしています。

マルチタスク処理

Python はインタプリタの実装上、ネイティブ thread による並列実行のパフォーマンスがよくありません。 そのため tornado はノンプリエンプティブ型のマルチタスク処理システムを独自実装して、並列処理を実現しています。 このマルチタスク処理システムでは、ユーザコードから tornado への実行権限の譲り渡すタイミングを、ユーザコード上で指定する形になっています。 プロファイリングを取得してみると、このマルチタスク処理の部分がそれなりに重いことが分かりました。 その対策として、実行権限の譲渡のポイントを精査し、不要な実行権限の移譲が発生しないように対策しました。

最後に

KMS は SaaS として、1つのシステムで複数のゲームアプリ向けにチャットシステムを提供するために開発しました。 SaaS 型のシステムにした理由は、リアルタイムにチャットメッセージを配信するためのサーバシステムは、どうしても一般的なゲームのサーバシステムとは異なる部分が大きいため、利用案件の数だけチャットサーバシステムを立てていたのでは、運用が回らなくなると考えたからでした。

汎用的なシステムにしたために少し複雑になっている部分もありますが、KMS を利用するゲームアプリの自由度を損なわずに、それでいて運用性の高いシステムに仕上がりました。 この KMS の汎用的かつ柔軟な構造は、将来あるであろう、何か新しいことを KMS の上で実現したい、というリクエストに十分応えてくれることでしょう。

KG SDKに関するお問い合わせ

KLabでは、開発パートナー様と共同開発したゲームをKLabにてパブリッシング、プロモーションを行うというモデルを積極的に進めており、開発パートナー様にKG SDKの提供もしています。
パブリッシング事業につきましてはこちら
KG SDKに関するお問い合わせにつきましてはこちら
をご覧ください。

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

おすすめ

合わせて読みたい

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。