Docker と ECS と WebSocket で最強のリアルタイム・マルチプレイ環境を運用
概要
AWS ECS でマルチプレイゲーム用インスタンスの管理すると限りなく最強。
はじめに
リアルタイム・マルチプレイゲームを作る時、まず考えられることは、あるプレイヤーの行動や状態が他のプレイヤーに伝わることではないかと思われます。しかし、スマートフォンアプリは不安定であったり、複数端末同士で(基地局やバックボーンを介さずに)物理的に直接接続することは出来ませんし、理論的にできたとしても、だいたい開発が進んでいくうちに排他制御の問題などで炎上、もしくはとん挫してしまいます。軽い気持ちでマルチスレッド処理をおこない事故る現象と全くおなじです。
もっとも簡単な解決方法としてはマルチスレッド処理のときようにクリティカルセクションを設けることです。ようはサーバを用意してそこで処理すれば、比較的容易に一意な結果が得られますし、接続に関する問題も起こりにくくなります。
長くなるので → http://labs.gree.jp/blog/2014/12/13141/
おさらい
Web 系アーキテクチャでは、他の HTTP 処理と同じくサーバ処理部分はステートレスにして、 DB に状態を入れ、メッセージプッシュのためのバスとして Pub/Sub もしくはメッセージキューを利用することが多いようです。
この仕組みは HTTP 処理と相性が良く、あわよくば Websocket にアップグレードしてフルスタック HTTP としてふるまうこともできます。しかし、チャットアプリのような、頻度が多くないアプリケーションと相性は良いですが、ゲームのような高頻度かつ複雑な処理が絡んでくると DB のレスポンスや、メッセージバスの分散化が課題になってきます。
となると、いっそのことゲームの処理はステートフルにして、ロードバランサを使わずに端末側でなんらかの指示によりアクセス先を切り替える手法のほうが効率よくなります。
この仕組みはゲームプレイ中の障害に弱く、障害が起こるとゲームそのものの状態が揮発してしまいます。他のインスタンスとのバスもありません。なので大規模または長時間プレイを要するゲームと相性が良くないですが、そもそもスマートフォンのゲームでは1プレイ数分で終わるようなゲームが多く、この点での相性の悪さは目立たないのではないかと思われます。
運用について
WebSocket を扱うことになると、 node.js による実装を採用することが多く、たしかに node.js の非同期処理と書き味がとてもよく合います。そして開発、実装が進み、ゲームインスタンスがシンプルに動作したとして、運用の段階になると、いくつか考えなけばいけないことがあります。
- 開発したコードのビルド、パッケージ
- デプロイツールによる、サーバアプリのデプロイ
- マネージャがデプロイに応じてワーカーのリロード
- マネージャがワーカーの障害やゲーム終了に応じてリロード、デーモン化
- それらのプロセスやログのモニタリング
- サーバインスタンス全体の構成管理など
という具合で、マネージャ、デプロイ、モニタリング、構成管理も必要になります。もちろんこれらに対応するためのツールは社内にも、オープンソースにも、世の中には大量にあり、各社、それぞれのエンジニアが日々改善を企んでるかと思われます。
ここまでは Web 系アプリと同じように考えた場合です。
これをコンテナ中心のワークフローに変えてみると
- 開発したコードのビルド等 → Dockerfile ひとつで済み、ミドルウェアや設定も一緒に埋め込む
- デプロイツール → ECS Task によるフルマネージド
- リロード → ECS コンソール操作
- 障害対応 → コンテナが無くなり、空いたホストで必要分だけ勝手にワーカーが起動
- モニタリング → 状態に依存しないので、最初うまくいけばずっとうまくいく
- インスタンス全体の管理 → Auto Scaling と ECS によるフルマネージド
アプリをコンテナにまとめてしまうことで運用を相当手抜きができます。そしてマルチプレイゲームと相性が良いのはここからです。
まず、コンテナホストとコンテナで Listen する TCP ポートが被る問題が知られています。コンテナホストでは IP Masquerade のようなふるまいになるので、一つのポートを Listen して fork するという Prefork パターンが取れません。しかし、ステートフルサーバの場合はクライアントアプリに接続先の指示を行うので、このデメリットは全く無関係です。むしろ前世紀から人類を苦しめてきた Prefork モデルを積極的に否定できるので大きな利点になります。 fork とかマジ最悪。
これは、 ECS による Task を定義するときに Host 側のポート設定を 0 番にしておくことで、勝手に割り当てられるようになります。割り当て範囲のデフォルトは 49153 to 65535
までと書いてありますが、こちらで確認したところ実際の範囲はデフォルトではありませんでした。
1 2 3 |
$ cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000 |
つづいて、コンテナ起動後にコンテナ一覧を取得し、どのポートにマップされているかは CLI を使う場合、次のように確認できます。もちろん対応する AWS-SDK-API もばっちりあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ aws ecs list-tasks --container-instance 2d098a8b-03a2-47b6-8d45-6646d4e6211c { "taskArns": [ "arn:aws:ecs:ap-northeast-1:566423514641:task/0f8b1d86-b8b4-4c5a-bc8c-3dff277112da", "arn:aws:ecs:ap-northeast-1:566423514641:task/17d3de5f-e1dd-441f-9f18-cdce29a17ac7", ... $ aws ecs describe-tasks --tasks 17d3de5f-e1dd-441f-9f18-cdce29a17ac7 --query 'tasks[].containers[].networkBindings[]' [ { "protocol": "tcp", "bindIP": "0.0.0.0", "hostPort": 32768, "containerPort": 3000 |
コンテナホストのアドレスも ecs list-instances → ecs describe-container-instances → ec2 describe-instances のコンボで詳細に取得できます。コンテナホストの Public DNS Name と hostPort を組み合わせれば対象のゲームインスタンスに直結できるわけです。
これによる案内ができるので、 ELB や Auto Scaling との相性の悪さも当然ありません。
もう一つ、課題になるのがコンテナイメージの管理です。従来型のアプリの場合、アプリケーションのコードやバイナリはまとめて S3 に配置しておき、そこからデプロイするか、もしくはそのまま公開するケースが多いかと思われます。一方でコンテナイメージのレジストリはコンテナ用のものを使わなくてはならいのですが、当然ここでは ECR を利用します。 ECR は日本のリージョンにはサービスされてませんが、 US リージョンのもので十分に使えます。 ECR について特筆すべき点はありませんが、単純にマルチプレイゲームインスタンスの場合はアセット等を含まずゲーム処理に特化する傾向にあるのでコンテナイメージも小さく、効率が良いと考えられます。
node.js の WebSocket をワーカーとして扱う場合は、 Apache などと違い必ずマネージャ層の選定が必要になりますが、これも当然 ECS にお任せで運用できます。コンテナなのでマルチコア資源活用のための設計も不要ですし、 Prefork を用いるという愚かな選択肢はハナからありません。
課題と期待
コンテナホスト自体は EC2 で運用しなければなりません。一方で AWS Lambda のようなフルマネージドサービスもあるのでこの課題もじきに解決されることでしょう。
まとめ
マルチプレイゲームインスタンスをコンテナ運用すれば欠点にはヒットせず、利点のみを多く得ることができるので最強です。