QA@IT

Rails3でのリアルタイム通知実装のベストプラクティスは?(Pusher以外)

3324 PV

よくあるタイムラインの未読数を表示する部分や、チャットでページをリロードせずに、他の人の発言を同じルームに入っている人、全員のウィンドウに反映させるなどのリアルタイムな処理をRails3に実装する場合、現時点でのベストプラクティスや、それに近い実装方法、個人的に最も良いと考えている方法などありますでしょうか?

また、Rails4でリアルタイム系の機能が導入されるという話も聞こえて来ていますが、その機能をRails3に組み込む方法もあるのでしょうか?

今回はなるべく安価で手数が少なく済む方法で、と考えているため、Pusherは除外でお願いします。
(Pusherは実装は簡単ですが、実サービスで使うとなるとさすがに無料枠では収まらないので。。)

自分ではPrivate PubというGemを試してみたのですが、今のところ、とりあえずはそれなりに動きそうという感触です。
(別アプリを立ち上げる必要がありますがHerokuでも動きそうです。)

基本的にはRailsアプリに最終的に組み込めればよいので、Node.jsなどで作成したAPIを叩くなどでもOKです。

希望要件としては、なるべく手数が少なく実装できること、出来ればHeroku上でも動作すること、なるべく環境依存が少ないことなどがありますが、どのような方法があるのか?という部分も知りたいと思っているので、多少、外れていても大丈夫です。
(パフォーマンス面、どの程度スケールするかという点についての情報もあるとありがたいです。)

回答

たびたび失礼します。。。

個人的に http://lingr.com というサービスを、まだCometという言葉もなかった2006年頃から作ってきていて、また http://pankia.com というサービスでは60fps単位で同期するリアルタイムなiOS/Android対戦ゲームの通信システムなどを作ってきていますが、それらの経験を踏まえると、プッシュを実装するうえで一番の悩みどころは、プッシュ用のサーバをシングルトンにするかマルチインスタンスにするかだと思います。

なぜかというと、ステートレスな通常のwebリクエストと違って、永続的なコネクションの場合は、あるクライアントの接続を特定のインスタンスがずっと(あるいは長時間)保持することになるため、マルチインスタンスだと、あるクライアントへメッセージを届けたいときに、どのインスタンスがそのクライアントからのコネクションを握っているか?というルーティング情報がクリティカルになってくるからです。

シングルトンの長所

  • ルーティングの心配がいらず、実装が超シンプル
  • user-to-connectionマッピングをクラス変数などに持たせられるのでデータベースなどをひく必要もなく、高速

シングルトンの短所

  • CPUは1コアしか使えないので、プッシュがすごく多いビジーな用途には向かない(このへんに昔書いた解説があります)
  • ビジーではない用途だとしても、同時接続数が数万を超えると現実的にはいろいろと限界が出てくる(いわゆるC10K問題)。ただし、実際の同時接続数は、技術者が想像するよりもたいてい一桁以上少ないので見積りが過剰になりがちなのは注意したい。
  • SPOFになる。そのプロセスが死んだらシステム全体が落ちる。(というのは実はけっこう机上の空論で、実際には実装がシンプルなので超安定したサーバにできる)

という感じです。マルチインスタンスの場合には、ブロードキャストやマルチキャストやユニキャストなど、性能と可用性ともろもろのトレードオフによって様々なルーティング実装方法がありますが、とてもややこしくなりそうなことは想像できると思います。

で、なぜこのような話を先にしているかというと、ほとんどの場合にはシングルトン実装で事足りるのですが、Railsのアーキテクチャは原則としてshared nothingで水平分散・対称性を重視しているので、非対称性を生むシングルトンのモデルと相性が悪いのです。

たとえば、thinのプロセスを4つ上げるとすると、そのうちどれか一つをプッシュ通信用に使う、というのは、なんとなく気持ち悪いのがおわかりいただけるかと思います。

UnicornやPassengerのように、マスタープロセスが1個いて、ワーカーがN個いるようなモデルなら、マスター上だけで別スレッドを作ってそこでEventMachineを初期化してイベントループを回せば、マスター=プッシュサーバとして使うことが可能になりますが、これでも物理サーバが複数になると、マスタープロセス自体が複数になるのでうまくいきません。

そのようなこともあって、個人的に一番よくやっている構成は、通常のRailsとは別にasync_sinatraサーバ(注:EventMachineを使いたいので、こちらはthinを使います)を一個だけ上げておき、プッシュ系の処理は全部こいつにやらせる、という感じです。つまり、クライアントからの接続は全部こいつに持たせておき、Rails側でプッシュしたいイベントが発生したら、sinatraを叩いてプッシュさせるのです。

こうすることで、アプリロジックの大部分(95%ぐらい?)は通常のRailsアプリとして書くことができ、プッシュ通知の部分だけがsinatraへ行くので、コードの分離・見通しがとてもよくなります。

で、Herokuでどうやるか、という話ですが、上記のやり方は、「理論上は」Herokuでも可能です。RailsとSinatraでアプリ自体を分けて、2つにすればいいのです。実際、LingrのボットなどをHeroku上でasync_sinatra + thinで動かしている実績は多数あります。

ただし、実際にはHerokuが無料版だと(少なくとも80番ポートに関しては)同時接続数がリバースプロキシレベルの上流で2-3本に絞られていて、大量のコネクションを保持するというのはできなかった記憶があります。だいぶ昔の話なので、今は回避方法があるのかどうか、ポートを変えれば大丈夫なのか、状況が変わったのか、などなどわかりませんが。

ただ、C10K問題に対応するにはFD数の拡張などカーネルパラメータのチューニングもしたいので、やはりVPSなどroot権限のある環境でやるのが現実的だろうとは思います。

追記

なんか前にも似たようなこと書いたことあるなと思ったら、ありました。

http://qa.atmarkit.co.jp/q/2192 です。ご参考までに。

編集 履歴 (1)
  • たびたびありがとうございますw 毎回、分かり易くとても参考になる回答で助かります。今回の用途ではC10Kはおそらく越えないかな、という感じなので、シングルトンでいけそうです。なので、やはりasync_sinatraなどを使って別アプリとして分離してしまった方が良さそうです。回答ありがとうございました。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。