テックレポート - TechReport

一覧はこちら

  • now_btn.png
  • ameblo_btn.png

Socket.IO, Redisを使用し各ゲーム間でプッシュ通知するシステム

執筆者

執筆者:
古谷 薫
所属部署:
アメーバ事業本部 ピグ部門
業務経歴:
アメーバ事業本部にて「アメーバピグ」の機能開発リーダー。認証、通知システムなど、アメーバピグ関連サービスが横断的に利用するサブシステムの開発にも従事。

概要

オンライン通知システム(開発コード「homing」)に関する研究内容です。

homingは、アメーバピグ、ピグライフ、ピグアイランド、ピグカフェ、ピグワールドにおいて各サービスの通知情報をログインしているユーザに横断的にプッシュ通知するためのシステムです。

homingでは、クライアントーサーバ間はSocket.IO、サーバサイドにNode.js、データストアにRedisを利用しています。

本研究では、homingのアーキテクチャ、およびNode.js、Socket.IO、Redisの利用においての問題点およびワークアラウンドについて説明します。

目次

 

1. 序論

アメーバピグは、異なるドメイン(別システム)において、複数のサービスを提供おり、以下のようなアーキテクチャの違いがあります。
このように異なるシステム間で相互にプッシュ通知を送るためのシステムとして、homingを開発しました。

201405132157_1-600x0.png
図1-1. アメーバピグ関連サービスの違い

表1-1. アメーバピグ関連サービスの違い

サービス名

サーバ

サーバ-クライアント間プロトコル

クライアント

プラットフォーム

アメーバピグ

Java

独自バイナリプロトコル

Flash

PC

ピグライフ

Node.js

WebSocket

Flash

PC

ピグアイランド

Node.js

WebSocket

Flash

PC

ピグカフェ

Node.js

WebSocket

Flash

PC

ピグワールド

Node.js

WebSocket

Flash

PC

アメーバピグSP

Java

HTTP

HTML, JavaScript

SP

どこでもピグライフ

Node.js

socket.io(xhr-polling)

HTML, JavaScript

SP

2. システム概要

homingは、アメーバピグ関連サービスのいずれかにログインしているユーザに対して、各サービスの通知情報を横断的にプッシュ通知します。

システムの構成は以下の通りです。

201405132213_1-600x0.png
図2-1. システム概要

2-1. APIサーバ

ユーザへの通知要求を受け付けるREST fullなAPIを備えたWebサーバです。

Node.jsのClusterによる、master/worker構成で稼働します。

通知要求はワーカプロセスで処理します。

201405132213_2-400x0.png
図2-2. APIサーバ

2-2. Redisサーバ(データストア用)

一時的なデータをストアするためのRedisサーバです。
sentinelプロセスによる、master/slave構成で稼働します。
ストアされたデータはメモリ上のみで管理し、ディスクへの書き込みは行いません。
以下の情報を管理します。

  • ユーザのオンライン情報
  • ユーザIDの索引情報

201405132146_3-300x0.png
図2-3. Redisサーバ(データストア用)

2-3. Redisサーバ(Pub/Sub用)

publish/subscribe処理のみを行うRedisサーバです。
sentinelプロセスによる、master/slave構成で稼働します。
通知処理のpub/subで主に利用されます。
負荷状態に応じて、この構成を複数に増設可能です。

201405132157_2-300x0.png
図2-4. Redisサーバ(Pub/Sub用)

2-4. Redisサーバ(Socket.IO用)

本システムは、socket.IOのセッション管理用に利用するRedisサーバです。
sentinelプロセスによる、master/slave構成で稼働します。

201405132215_1-300x0.png
図2-5. Redisサーバ(Socket.IO用)

2-5. フロントサーバ

ユーザとの接続を管理し、ユーザへ通知データを送信します。
Node.jsのClusterによる、master/worker構成で稼働します。
サーバ、クライアント間の接続は、Socket.IOを使用しています。

通知データの送信処理はワーカプロセスで処理します。

201405132157_3-400x0.png
図2-6. フロントサーバ

3. 利用した技術

3-1. Node.js

Node.jsはサーバサイドJavaScriptの実装の1つで、以下のような特徴を持っています。

  • V8エンジン上の実行環境
  • シングルスレッドベースの非同期処理
  • ノンブロッキングI/O

本システムでは、特性上I/Oバウンドな処理が多いためNode.jsを採用しました。

Node.jsで利用した主要なモジュールは以下の通りです。

モジュール

バージョン

備考

Node.js

0.10.21

0.10.21に以下の修正を適用したものを利用しています。
・Cluster API利用時の、ワーカプロセスへの分散ロジックをラウンドロビンに修正。

socket.io

0.9.16

0.9.16に以下の修正を適用したものを利用しています。
・handshakeデータ共有遅延による接続率低下防止
・RedisStore利用時のpub/subによるメモリ増幅防止

express

3.2.6

 

redis

0.8.3

 

redis-sentinel

0.0.5

 

cookie

0.1.0

 

log4js

0.6.6

 

uglify-js

1.3.5

 

heapdump

0.2.0

 

3-2. Socket.IO

Socket.IOは、クライアントとサーバ間の通信プロトコルを複数実装したクライアント/サーバのライブラリで、プロトコルを意識することなくリアルタイム通信を利用できます。

本システムは、クライアント環境にPC、およびSPを想定しており、複数の通信プロトコルをサポートする必要があるためSocket.IOを採用しました。

3-3. Redis

Redisは、オープンソースのkey-valueデータストアです。

以下のような特徴を持っています。

  • 格納するValueがリスト、セット、ハッシュといったデータ構造
  • データセットはメモリ内に読み込まれるため高速
  • publish / subscribeが備わっている

本システムでは、大量なユーザ接続情報、およびユーザへの通知データを複数プロセス間で高速に処理する必要があるため、Redisを採用しました。
利用しているバージョンは以下です。

version : 2.6.16

4. アーキテクチャ

4-1. クライアントの接続

4-1-1. 接続分散

本システムでは、クライアント接続は各プロセスへ分散されます。

  1. ロードバランサによる各サーバへの分散
    ラウンドロビン方式により、クライアント接続は各サーバへ分散されます。
  2. Node.jsのCluster構成による各プロセスへの分散
    Cluster構成により、クライアント接続は各ワーカプロセスへ分散されます。
    ※本システムの場合、接続済みソケット共有型によりラウンドロビン方式で分散されます。

201405132157_4-500x0.png
図4-1. Node.js Cluster 接続済みソケット共有型(ラウンドロビン方式による分散)

Icon

Node.jsの問題点

通常バージョン(0.10.21)のNode.jsの場合、Cluster構成はリスニングソケット共有型であるため、各ワーカプロセスへの分散はカーネルによって行われます。そのため、プロセスごとに接続が偏る傾向があります。
本システムのように接続数に応じて処理量が増えるシステムの場合、特定のプロセスに接続が偏るため負荷によるプロセスダウンの可能性があります。

201405132157_5-500x0.png
図4-2. Node.js Cluster リスニングソケット共有型(カーネルによる分散)

 

回避策

本システムで利用しているNode.jsは、Cluster構成時に接続済みソケット共有型によるラウンドロビン方式の分散を行うよう修正を加えています。
修正内容は以下を参照してください。
https://github.com/tico8/node/tree/v0.10.21-patchedhttp://www.cyberagent.co.jp/files/user/img/common/open_window.png

4-1-2. 接続処理

本システムでは、フロントサーバ、クライアント間の接続は、Socket.IOを利用しています。
また、フロントサーバはマルチプロセスで稼動するため、プロセス間のセッション共有にSocket.IOのRedisStoreを使用しています。

201405132157_6-500x0.png
図4-3. 接続処理のシステム構成

201405132157_7-600x0.png
図4-4. 接続処理フロー

  1. ハンドシェイク
    socket.IOは、永続的な接続を確立する前にハンドシェイクを行います。
    この際に、ユーザ認証処理を行っています。
  2. ハンドシェイク共有
    ハンドシェイクを行ったプロセスは、ハンドシェイク結果をRedisのpub/subにより他プロセスへ共有します。
  3. 接続
    ハンドシェイク済みの場合、接続を確立します。
    ※本システムの場合、ハンドシェイク結果が未共有の場合、接続状態を維持したままハンドシェイク結果の共有を一定期間待ちます。
  4. 接続情報の維持
    接続完了後、本システムで利用するユーザの接続情報をRedisへ期限付きで格納します。
    接続が維持されている間、ユーザの接続情報は定期的に更新し、期限を延長し続けます。
  5. 切断
    クライアントとの接続が切断された場合、ユーザの接続情報を破棄します。
Icon

Socket.ioの問題点(1)

「3. 接続処理」のとおり、ハンドシェイク結果の共有が遅れるケースがあります。
この場合、通常バージョン(0.9.16)のSocket.IOでは、ハンドシェイクが行われていないと判断し接続を拒否します。
Redisの負荷が高い場合などにpub/subの遅延によってハンドシェイク結果の共有が遅れるため、高負荷時の接続率の低下につながります。また、接続拒否後、クライアントの再接続を行うような場合、再接続の回数が増加しそれによってシステムに負荷を与える可能性があります。

 

回避策(1)
本システムで利用しているSocket.ioは、ハンドシェイク結果の共有が遅れている場合に一定時間待つ処理を行うよう修正を加えています。

修正内容は以下を参照してください。
https://github.com/tico8/socket.io/commit/88f507c84585d7bd1a9c18e74f721a044a55cd40http://www.cyberagent.co.jp/files/user/img/common/open_window.png

 

Socket.ioの問題点(2)

この場合、通常バージョン(0.9.16)のSocket.IOでは、ハンドシェイク結果の共有のほかに、接続、切断、Roomへの参加などの情報を各プロセスにRedisのpub/subを利用して共有します。
これにより、ユーザの接続毎に大量のpub/subが行われることになり、Redisへの負荷が高くなります。

 

回避策(2)

本システムでは、ハンドシェイク結果、および特定の接続に対するpub/subのみ行うようにし、その他のSocket.IOによるpub/subは強制的に排除することでRedisへの負荷を低減しています。
※ただし、Socket.IOの一部機能(「Room」など、プロセス間での通信を隠蔽した機能)が利用できなくなります。本システムの場合、Socket.IOの最低限の機能しか利用しないためこの対応を行っています。

4-2. 通知の送信

4-2-1. 通知処理

通知処理には、Redisのpub/subを利用しています。

201405132157_8-600x0.png
図4-5. 通知処理のシステム構成

  1. 通知先のフィルタ処理
    APIサーバは、Redis上の接続情報を元に通知先のユーザが接続しているサーバ・プロセスを特定します。
  2. Redisにpublish
    サーバ・プロセスごとにsubscribeしているチャネルへ通知データをパブリッシュします。
    ※本システムの場合、通知処理により大量なpub/subが行われるため、pub/sub専用のRedisサーバを複数台用意し分散処理しています。
  3. サーバ・プロセスが受信
    各サーバ・プロセスは、Redisのslaveからsubscribeし、自プロセス宛のpublishのみ受信します。
  4. 送信
    各サーバ・プロセスは、接続されているユーザへ通知データを送信します。

4-2-2. 通知データをFlashで扱う

本システムでは、サーバ、クライアント間の通信はSocket.IOを利用しています。
アメーバピグの場合、クライアントサイドはFlash(ActionScript)で実装されているため、Socket.IO(JavaScript)で受信した通知データをFlashのExternal Interfaceを使用し、Flashへと受け渡ししています。

201405132157_9-500x0.png
図4-6. 通知データをFlashで扱う

4-3. 冗長化

4-3-1. Redis

本システムでは、master/slave構成で利用しています。また、Redis Sentinelによる自動フェールオーバーを導入しています。

 

Redis Sentinelによるフェールオーバー

本システムでは、Redisサーバごとにsentinelプロセスを起動しています。
sentinelプロセスは、masterプロセス、およびslaveプロセスに対して、PINGコマンド、およびINFOコマンドを定期的に発行し死活監視を行います。
閾値以上のsentinelプロセスがmasterプロセスのダウンを検知した場合にフェールオーバー処理が発動します。

201405132157_10-500x0.png
図4-7. Redis Sentinelによるmaster/slave構成

 

Redis Sentinel Clientによるフェールオーバー検知

本システムでは、「redis-sentinel」というNode.js用モジュールを利用しています。
redis-sentinelを利用することでsentinelプロセスが把握しているmaster、slaveプロセスの情報を元にRedisへの接続を管理することができます。

Redisにてフェールオーバーが発生した場合、sentinelクライアントによりsentinelプロセスからのフェールオーバ通知を受信し、昇格したmasterプロセスへ再接続を行います。
※「redis-sentinel」をそのまま使った場合、RedisクライアントとRedisとの切断が発生した場合のみ再接続を行う仕様です。sentinelプロセスからの通知では、再接続は行いません。

201405132157_11-600x0.png
図4-8. Redis Sentinel Clientによるフェールオーバ自動検知

4-4. 負荷分散

4-4-1. Redis

本システムは、Redisを中心にシステムが稼動するため、Redisへの負荷を考慮して3系統のRedis構成を導入し、役割に応じてそれぞれに分散しています。

本システムのRedis利用方法のほとんどが、pub/subによる通信であるためpub/subによる負荷が高くなります。そのため、pub/sub用のRedis構成を別途用意しています。また、subscribeするサーバプロセスの数が300以上になるため、subscribeは基本的にslaveプロセスから行うようにし、負荷に応じてslaveをスケールするようにしています。

201405132157_12-600x0.png
図4-9. Redis 分散

APIサーバ - Redisサーバ

APIサーバのワーカープロセスは、Redisとの接続を以下の3種類保持します。

APIサーバ自体は、Publishを除き、Redisへデータの書き込みは行わないためslaveプロセスのみに接続しています。

用途

接続先

備考

データ参照

データストア用Redisサーバ Slave

※どのSlaveに接続するかは、Redis Sentinel Clientによるランダム振り分けによる。

Publish

Pub/Sub用Redisサーバ Master

※pub/sub用Redisサーバが複数系統存在する場合は、系統分接続する。

Subscribe

Pub/Sub用Redisサーバ Slave

※どのSlaveに接続するかは、Redis Sentinel Clientによるランダム振り分けによる。
※pub/sub用Redisサーバが複数系統存在する場合は、系統分接続する。

フロントサーバ - Redisサーバ

フロントサーバのワーカープロセスは、Redisとの接続を以下の6種類保持します。

フロントサーバは、Publish、およびRedisへのデータ書き込みを行うため、Masterへの接続も行います。また、Socket.IOを利用しているため、Socket.IO用のRedisにも接続します。

用途

接続先

備考

データ書込/参照

データストア用Redisサーバ Slave

※どのSlaveに接続するかは、Redis Sentinel Clientによるランダム振り分けによる。

Publish

Pub/Sub用Redisサーバ Master

※pub/sub用Redisサーバが複数系統存在する場合は、系統分接続する。

Subscribe

Pub/Sub用Redisサーバ Slave

※どのSlaveに接続するかは、Redis Sentinel Clientによるランダム振り分けによる。
※pub/sub用Redisサーバが複数系統存在する場合は、系統分接続する。

データ書込/参照

Socket.IO用Redisサーバ Master

 

Publish

Socket.IO用Redisサーバ Master

 

Subscribe

Socket.IO用Redisサーバ Slave

※どのSlaveに接続するかは、Redis Sentinel Clientによるランダム振り分けによる。

4-4-2. APIサーバ、フロントサーバ

APIサーバ、フロントサーバについては、ロードバランサによる分散です。

5. キャパシティ

5-1. 想定

アメーバピグでの運用を想定し負荷をかけ、本システムのキャパシティを計測しました。

キャパシティ測定の基準としては以下です。

  • 各サーバ・プロセスがダウンしないこと。
  • ユーザの接続が可能なこと。
  • APIサーバが処理待ちのリクエスト数が増え続けないこと。(リクエストをさばけていること)
  • フロントサーバの処理待ちの送信数が増え続けないこと(送信処理をさばけている)

アメーバピグでは、「ピグとも」というユーザ同士の繋がり情報を持っており、ユーザのアクションを他ピグともへ通知しています。
現状ピグともは、最大で300人まで登録でき、全ユーザの平均をとると1人あたり10名程度のピグともを保持しています。
この状況下における運用を想定して、負荷をかけました。

5-2. 試験環境

本番で利用を想定している構成にて試験を実施しました。

表5-1. キャパシティ計測時の条件

項目

数値

APIサーバ数

5 server

プロセス数 / APIサーバ

16 process

Redisサーバ数 (データストア用)

Master × 1, Slave × 2

Redisサーバ数 (pub/sub用)

Master × 1, Slave × 2

Redisサーバ数 (Socket.IO用)

Master × 1, Slave × 2

フロントサーバ数

20 server

プロセス数 / フロントサーバ

16 process

5-3. 接続処理 試験結果

接続ユーザ200,000人が接続した際の負荷を計測した結果です。

試験結果から、本システムで利用しているSocket.IOによるpub/subの負荷が高いため、ユーザの接続数に応じてRedisサーバ(Socket.IO用)をスケールする必要があることが分かります。

表5-2. 接続内訳

項目

数値

接続ユーザ

200,000 user

秒間接続数

1200 user / sec

ユーザの切断

無し

各サーバの負荷状態は以下の通り。
※全ユーザが接続されるまでの間における、ピーク時の値です。

表5-3. フロントサーバの負荷

CPU / process

MEM

80 %

3GB

表5-4. Redisサーバ(データストア用、兼pub/sub用) Master

operation

CPU

MEM

mset : 1,200 / sec
set : 1,200 / sec

60 %

1.5GB

表5-5. Redisサーバ(データストア用、兼pub/sub用) Slave

operation

CPU

MEM

-

20 %

0.5GB

表5-6. Redisサーバ(Socket.IO用) Master

operation

CPU

MEM

publish : 1,200 / sec

80 %

8.0GB

表5-7. Redisサーバ(Socket.IO用) Slave

operation

CPU

MEM

message : 720,000 / sec

100 %

10GB

5-4. 通知処理 試験結果

接続ユーザ200,000人が接続完了した状態で、以下のAPIリクエストをコールし計測した結果です。
試験結果から、通知処理によるpub/subの負荷が高いため、ユーザの接続数に応じてRedisサーバ(pub/sub用)をスケールする必要があることが分かります。

表5-8. APIリクエスト内訳

項目

数値

計測時間

15 分

APIリクエスト数

1,200 req / sec

1リクエストあたりの通知対象ユーザ

300 user / req

1リクエストあたりの通知送信されるユーザ

150 user / req

通知メッセージサイズ

200 Byte

1秒間に送信される通知数

180,000 message / sec

各サーバの負荷状態は以下の通り。
※計測時間中のピーク時の値です。

表5-9. APIサーバの負荷

CPU / process

MEM

処理待ちリクエスト数

60 %

3GB

1 ~ 10

表5-10. Redisサーバ(データストア用、兼pub/sub用) Master

operation

CPU

MEM

publish : 90,000 / sec

70 %

5GB

表5-11. Redisサーバ(データストア用、兼pub/sub用) Slave

operation

CPU

MEM

mget : 2,400 / sec
message : 90,000 / sec

90 %

1.8GB

表5-12. Redisサーバ(pub/sub用) Master

operation

CPU

MEM

publish : 90,000 / sec

80 %

6GB

表5-13. Redisサーバ(pub/sub用) Slave

operation

CPU

MEM

message : 90,000 / sec

50 %

0.5GB

表5-14. フロントサーバの負荷

CPU / process

MEM

処理待ち送信数

50 %

5GB

1 ~ 10

6. まとめ

オンライン通知システム(開発コード「homing」)の開発に以下を導入したが、それぞれに課題があったためここにまとめます。

  • Node.js Cluster
    本システムで利用したバージョンでは、プロセスへの分散が均等に行われないため、接続を維持するようなシステムでは利用は避けたほうがよいです。
    次期バージョンにて、本システムで利用した接続済みソケット共有型によるラウンドロビン分散が組み込まれているようなので、利用するなら次期バージョン以降をお勧めします。
  • Socket.IO
    本システムで利用したバージョンでは、RedisStoreによるセッション共有の仕組みが高負荷時に大量のリソースを消費したり、接続率が低下したりと実際のサービスに利用するにはいろいろと工夫が必要だということが分かりました。
    次期バージョンではRedisStoreに変わる実装が検討されているようなので、そちらに期待したいところです。
  • Redis
    データをメモリ上で扱うため書込み速度が高速であり、本システムのような揮発性の高いデータを大量に扱うような場合は利用価値があると思われます。ただし、pub/subに関しては便利な反面多くのリソースを消費するため、利用方法は検討したほうが良いと思いました。

本システムは、通知データをユーザへ届けるためのハブのようなシステムであったためIOバウンドな処理が多く、Node.js、Socket.IO、Redisの組み合わせとしては相性が良かったように思います。

本システムは、アメーバピグの裏側で稼動中です。現在は、UIの表示は行っていませんが、今後アメーバピグ関連サービスから利用される予定です。運用を通して発生した問題点などはフィードバックしたいと思います。

7. ソースコード

8. 参考文献