ピグ事業部でサーバーサイドエンジニアをしている有馬です。
先日、弊社よりスマートフォン向けネイティブアプリとして、 「PiggPARTY」がリリースされました。
PiggPARTY Androidアプリ
iOSアプリは近日公開予定です
PiggPARTYは、スマートフォンのアプリ上で、 顔や洋服などの様々パーツを組み合わせて、自分好みのピグ(アバター)を作成することができ、
渋谷エリアや、原宿エリアといった現実を模したエリアや、 好みの家具で模様替えした自分のお部屋でパーティ(イベント)を開催したり、 他のユーザーと、テキストチャットやスタンプなどで、 リアルタイムにコミュニケーションを楽しむことができるサービスです。
PCアメーバピグをご存じの方には、そのスマートフォン版というと伝わりやすいかもしれません。
PiggPARTYでは、同期的なリアルタイムコミュニケーションを実現するために、 新たにリアルタイム通信サーバーライブラリをスクラッチで開発しており、 今回はその仕組みについて紹介いたします。
なお、弊社内ではそのライブラリをtoychatという名称で開発しており、 以降文中でもその名称を使用させていただきます。
(PiggPARTYでは、他にも、clay(クレイアニメーション)や、origami(折紙)といった、 ユニークな名称でライブラリ開発が行われています。)
toychatの特徴
toychatは、Node.jsで書かれたサーバーライブラリで、 リアルタイムのチャットアプリケーションや、 オンラインゲームのゲームサーバーなどでの利用を想定して作成されています。
特徴としては下記になります。
- TCPベースの分散リアルタイムメッセージングサーバー
- MQTTやWebSocketなどマルチプロトコルを選択可能
- フェイルオーバー
toychatの仕組み
分散リアルタイムメッセージング
TCPベースで、チャットのようなリアルタイムメッセージングサーバーを考えた場合、 最小構成は下記のようなものが考えられます。
1台のサーバー、ユーザー間を、WebSocketなど、サーバーからのPush通信が可能なプロトコルで接続し、 サーバーは、あるエリアに送信されたメッセージを、そのエリアに接続中のユーザーに配信します。
しかし、当たり前ですが、1台で大量ユーザーの負荷を捌くのは限界があります。 そこでサーバーを分散するのですが、サーバー間で同一エリアへのメッセージをどう同期するかが問題になります。
社内外事例から、いくつかのパターンを参考に検討しました。
RabbitMQ、Redisなどメッセージキューを利用したパターン
この構成では、エリアに問わず、ユーザの接続はロードバランサーにより均等に振り分けられます。
バックエンドにメッセージキューを配置し、 キューを介してサーバー間でメッセージをやりとりすることによって、 同一エリアへのメッセージを同期します。
分散チャットシステムでは定番的な構成なように思います。
非常にシンプルでステートレスなため、スケールもしやすいですが、 PiggPARTYのようにメッセージ送信だけでなく、エリアに関する情報を多数取り扱うようなサービスでは、 同一エリアの情報を同期するためのデータストアが必要になり、アプリケーションコードが複雑化しやすい印象です。
例えば、PiggPARTYではエリア内におけるユーザーの座標情報を、ユーザーが移動するたびに更新しますが、 これらを複数のサーバーで共有更新するとなると、 更新負荷や並列更新の同期化など一筋縄ではいきません。
ログインサーバー、 チャットサーバーのパターン
この構成では、 ユーザーはエリアへの入室に際し、どのチャットサーバーに接続したら良いのかをログインサーバーに問い合わせ、 指示された接続先に接続します。
同一エリアへの接続は必ず同じサーバーに振り分けられるため、 エリアに関する情報をサーバーのオンメモリで取り扱うことができ、 高速かつ比較的シンプルにアプリケーションを構築することができるように思います。
PCアメーバピグはこれに近い構成になっています。
チャットサーバーを並列に並べることでスケールしますが、 チャットサーバー分グローバルIPが必要になるなど、運用面で若干複雑になる傾向がありました。
toychatの構成
これらを参考に、toychatでは下記のような構成で分散リアルタイムメッセージングを行っています。
ピグライフなどPCピグゲームの仕組みにかなり近い構成になっており、 より役割を明確にシンプルにした構成になっています。
それぞれの役割を簡単に説明します。
ProxyServer
ユーザーの接続を受け付け、 エリアへのメッセージや、接続断などのイベントを、エリアに対応するChatServerプロセスへルーティングします。 また、ChatServerから送信されたメッセージをユーザーへ送信します。
ChatServer
ProxyServerを介して、ユーザーとのメッセージングを行います。
各ChatServerプロセスは、それぞれ異なる一つ以上のエリアを担当し、同じエリアにいるユーザーへメッセージを送信したり、 エリア内へのメッセージのブロードキャスト等を行います。 またインターナル通信により、異なるエリアを担当するChatServerプロセスにメッセージを送信することもできます。
アプリケーションでの利用
ProxyServerやChatServerはNode.jsのEventEmitterを継承しており、 ユーザーの接続、接続断、エリアへの入退室、メッセージ送信など各アクションに応じてイベントを生成します。
ライブラリを利用するアプリケーションはリスナーを登録することで、 各イベントのタイミングで任意の処理を実行することが可能です。
下記は、ライブラリを利用するコードのイメージです。
分散環境でも、単一のサーバーで処理を行っているかのような感覚で開発できる、というイメージが伝わればと思います。
/** ChatServerに送信されたメッセージを、* エリアに入室中の他ユーザーにブロードキャストするコード例** chatUser メッセージ送信者を表すオブジェクト* room ルーム(エリア)を示す文字列* body メッセージ*/chatServer"message" on// エリアに入室中のユーザーを取得var joinedUsers = chatRooms room;if joinedUsers// メッセージの送信者以外を取得var others = _ values joinedUsers filterreturn joined userId !== chatUser userId;;// 送信者以外にメッセージをブロードキャスト(実際にはProxyServerを介して送信される)chatServersend others room body;;
通信プロトコル
toychatではユーザー端末との通信プロトコルは差し替え可能な作りになっています。
例えば、PiggPARTYではMQTT(※)プロトコルを使用していますが、 WebSocketなど異なるプロトコルを使用することも可能です。
ProxyServerとChatServer間はJSON over TCPでsokcet通信を行っています。
(※) Facebook Messangerなどで利用されているPub/Subモデルの軽量メッセージングプロトコル
https://www.facebook.com/notes/facebook-engineering/building-facebook-messenger/10150259350998920
スケールアウト
サーバーの負荷が増えてきた場合、下記のようなイメージで スケールアウトしていくことが可能です。
このように、サーバーを横に並べていくことで、 ProxyServer、ChatServer間の接続数がTCPポート数の上限を超えないレベルまでは、 この構成で負荷を分散することができます。
ルーティングとフェイルオーバー
各ChatServerプロセスがどのエリアを担当するかは、 Consistent hashingを使用して決定しています。
ChatServerプロセスに障害が発生した場合は、 ハッシュリングの再計算を行い、エリアの割り当てを更新します。
各サーバー間で正しくエリアの割り当てを行うには、 各ChatServerプロセスのリストを同期し、 各サーバー間で同様のハッシュリングが計算されている必要があります。
プロセスリストの同期にはいくつかの方法をとることができますが、 PiggPARTYでは、Serf (※)を利用して同期を行っています。
プロセスを監視し、異常があった場合には、 Serfを通して情報を伝播し、プロセスリストの更新を行っています。
アプリケーション側でプロセスリストの更新イベントを拾い、 ハッシュリングの再計算を行っています。
まとめ
以上、簡単ではありましたが、 PiggPARTYでのリアルタイム通信の仕組みについて紹介いたしました。
toychatの開発にあたり、アメーバピグやピグライフなど、 既存の大規模リアルタイムサービスの仕組みが大変参考になりました。
これらを作り上げた先人のエンジニア達に感謝するとともに、 今回の記事が少しでも皆様の参考になれば幸いです。
最後までおつきあいいただき、ありがとうございました。