tail my trail

作るのも使うのも、結局は、人なのだ

Socket.io + Redis PubSubでリアルタイムメッセージ配信

とあるサービスに「チャット機能」を追加しようという話になり、急ピッチで仕組みを用意することになった。 仕様/要件はふわっとしているものの、 2週間後にはリリースというケツは決まっている。 はてさてどうしたものかとその瞬間は思ったものだが、無事仕組みとして載せられたので、備忘記しておく。

リアルタイムチャット機能

要件は以下。

  • クライアントはWebブラウザとネイティブアプリ (iOS, Android)
  • 視聴者に軽量なテキストメッセージをbroadcastする
  • メッセージの永続化必須
  • 可用性/負荷分散も考慮する

例えば、動画を視聴しているとして、その動画の横に、自分含む視聴者のコメントが流れてくる、 ようなものを想像してもらえれば良い。

Socket.IO

「クライアント - サーバ間のリアルタイムなメッセージのやり取り」ということで、

という方針は早々に決まった。自動的にサーバは Node.js に決定。 ブラウザからのアクセスがあるため、xhr-polling / WebSocket をSocket.IO が両方サポートしていることは有難かったし、WebSocket通信を数行で始められる手軽さも魅力的だった。 ケツが2週間後なので実装コストがなるべく少なく済み、ビジネスロジックに集中できる方法を採択する必要があったため。

Socket.IOのポイントをまとめると、

  • WebSocket, xhr-polling をサポート、クライアントの動作環境に応じていずれかを自動採択する
  • "1 : 1" や "1 : N" のようなBasicな特定双方向通信、一斉配信 (broadcast)、接続単位を論理的に分離する 名前空間/namespace や 部屋/room 機能 など様々な配信方法をサポート

インストールは npm で一発 。 実装イメージは以下。

server side

var io = require('socket.io').listen(3000);
io.sockets.on('connection', function (socket) {
  
  ... your logic ...
  
});

client side

<script src="http://your.domain.com:3000/socket.io/socket.io.js"></script>
<script>
  var socket = io('http://your.domain.com:3000');
  socket.on('connect', function() {

    ... your logic ...

  });

  ... and so on...
</script>

ネイティブアプリ用のクライアントライブラリもある。 Socket.IOのGithub を見たところ、 C++ の実装もあるようだ。

Redis PubSub

Socket.IO on Node.js なWeb/Appサーバに対して、各クライアントはWebSocket通信で接続する。 接続数増と耐障害性を担保するためWeb/Appサーバの冗長化を考えた時に、一斉配信するメッセージを複数台のサーバにどのように伝達させるかで一瞬悩んだ。 所謂Publish / Subscribe ... ということで思い出したのがRedis。 "危険なほどのスピード" で有名なオンメモリKey-Value データストアで、扱えるデータ構造がMemcached と比べて豊富、ディスクへの永続化もあり人気のKVSだが、このRedis、PubSub も提供している。

1. 購読者 (subscriber) 、任意のkeyをsubscribeする

$ redis-cli
127.0.0.1:6379> SUBSCRIBE hoge
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hoge"
3) (integer) 1

2. (別のターミナルで) 出版者 (publisher) 、1で指定したkeyでメッセージをpublishする

$ redis-cli
127.0.0.1:6379> PUBLISH hoge "hello pubsub"
(integer) 1

3. 購読者 (subscriber) の端末に、2で配信されたメッセージが表示される

$ redis-cli
127.0.0.1:6379> SUBSCRIBE hoge
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hoge"
3) (integer) 1
1) "message"       ←追加
2) "hoge"          ←追加
3) "hello pubsub"  ←追加

Socket.IO - Redis の連携は容易。Socket.IOのバージョンによりインストール方法が若干変わる点に注意。

  • Socket.IO v0.9 系まで
    • Socket.IO 内に同梱されているため、特に追加インストールは必要なし。
    • 呼出は require('redis')
  • Socket.IO v1.0 以降
    • 外部ライブラリとして切り離された socket.io-redis をインストール
    • 呼出は require('socket.io-redis')

ちなみに、AWSのElasticCacheはRedisをサポートしており、Read Replica & Multi-AZも対応している。

PubSubはReplica Nodeに対しても有効で、PubSub sessionに関しても負荷対策が可能。

  1. Node.jsからReplica Nodeに対してSubscribe sessionを張っておく
  2. コメント投稿はMaster Nodeに対してPublish
  3. Read Replicaに対してSubscribeしている全購読者に配信される

WebSocketとELB

Socket.IO on Node.js サーバを冗長化させた場合の振り分け方も一工夫必要だった。 通常のHTTP通信と異なるため。注意点は以下の2点。

  • Cient - Server間の通信は通常のHTTP通信と異なりWebSocketで接続持続される
  • Socket.ioによるhandshake処理は必ず同一サーバに接続させなければならない

例えば、ロードバランサーを上段に挟む場合、全てのセッションがロードバランサーに対して張られることとなる。 通常のHTTPと異なりWebSocketのセッションを張り続けるという特性上、接続を集約させるロードバランサーボトルネックになる危険がある。 また、ロードバランサーによっては、Socket.IO/WebSocketの間に立てない場合がある。例えば、AWSのELB。

AWSのSolution Architectも、ELBは最初のセッション確立時にのみ利用してdispatchしてWebSocketさせるのが良いと言及している。

教えに従い、接続情報を返すAPIをクライアント側で呼出してから、サーバに対して直接WebSocket通信させる方式とした。

システム構成

行き着いたシステム構成は以下。

主機能がApache / PHPで動くWebアプリケーションのため、APIは既存資産を流用して実装。 Apahce/PHP を Node.js/Socket.IOと同梱させAPIを提供するようにした。 同梱させた理由は、接続情報管理の実装を省き自分のホスト名を返すようにしたかったため。 本来は、APIサーバは切り離して、ステータス含めて接続情報を管理するようにしたほうが望ましいと思う。

f:id:uorat:20150830185442p:plain

次回は、WebSocket接続のSSL/TLS対応をまとめようと思う。

Redis入門 インメモリKVSによる高速データ管理

Redis入門 インメモリKVSによる高速データ管理

Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

Amazon Web Services パターン別構築・運用ガイド

Amazon Web Services パターン別構築・運用ガイド