最終更新時刻:2009年11月9日(月) 7時30分
-

梅田サロン中止のお詫び、およびアーキテクチャ変更についての技術詳細レポート

公開日時:
2007/02/06 14:09
著者:
kenn

先週末に予定されていたJTPA企画の梅田さん主催オンラインサロンですが、会場に多くの人が集まるにつれてLingrが重くなってしまうという事態に陥ってしまい、まるでイベントの体をなさないまま時間が過ぎてしまい、あえなく中止となってしまいました。

当イベントを楽しみにしていた皆様、そして梅田さんはじめJTPAスタッフの方々には、本当に申し訳なかったと思います。ここに改めてお詫び申し上げます。

Macworld 2007のときには180人を収容して何の問題もなく快適に使えていたので、「1000人はわからないけど、200人ぐらいなら大丈夫だろう」とたかをくくっていたのが間違いでした。

今回はその反省も含めて、内部で検証した技術情報をすべて公開し、どのような問題に直面し、どのように解決にあたっているのかをお伝えすることで、特に技術者の皆さんに役立つフィードバックにしたいと思います。

■今回のアーキテクチャの変更について

まず前提としてご理解いただきたいのは、Lingrはインスタントメッセンジャーのような1対1の通信ではなく、1つのイベントが発生するごとにN人に一斉配信しなければいけない=N人全員の状態を同期させなければならないIRC型のアーキテクチャなので、ひとつのルームに参加する人数が増えると単純増加ではなくて級数的に問題が大きくなるということです。

すごく単純化して言えば、あるルームへの参加者N人中の1人あたりの平均的な信頼性を R (0<R<1) とすると、ルームの信頼性 = RN という感じで急速に信頼性が減衰していくモデルです。

これはComet技術を採用することによる「大量の開けっ放しコネクション問題」を乗り越えた他の企業、たとえば Google (GTalk in Gmail) や Meebo でさえも解いたことがない、まったく新しい種類の技術的な問題だといえます。

上記をふまえて、今回の現象を理解する前提として、1月25日のリリースで行われたバックエンドのアーキテクチャ変更についてご説明したいと思います。

新しいアーキテクチャの図はこちら。

LingrBackend20070125

主な変更点としては、

  1. pingをクライアントから直接打たず、observeを発行した時点でChat Serverが代替わりしてくれるようにした(クライアント側での処理をサーバ側へオフロードした)
  2. Cometを使ったobserveのループにおいて、レスポンスを返してから再接続までの瞬間に発言されたメッセージを漏らさないため、observe発行の際にはクライアント側で受け取った最終のevent-idを申告させ、これとサーバ側で持っているevent-idにずれが検出された場合(クライアント側が遅れていた場合)には即座にChat Serverがobserveをクライアントへ返してgetというまとめ読みメソッドへとリダイレクトさせていたのだが、このget処理もChat Serverが代替わりしてくれるように変更。つまりクライアント側ではobserveからの戻りだけを意識すればよくなった。

これらの変更点は、すべてAPIでアプリケーションを開発しやすくするために行った変更でした。pingがなくなることでクライアント側ではタイマー処理がまるまるひとつ不要になり、getがなくなることで、クライアント側でリダイレクトをハンドリングする必要なくなり、とてもシンプルなループを書けばすむようになりました。

ところが、今回発生した問題は以下のようなものでした。

  1. 多人数が一斉にひとつの部屋に集まると、1秒間に数〜数十ものイベントが発生する。(いわゆる発言だけに限らず、部屋に入る、部屋を去る、ニックネームをセットする、などもすべてひとつのイベント)しかも、その全てのイベントが全員に毎回マルチキャストされるので、掛け算でSocket I/O負荷が集中する。
  2. 1秒間に数〜数十ものイベントが発生すると、ほぼ全てのクライアントで自分が持っているevent-idがサーバ側の最新状態より遅れてしまうので、ほぼ全てのクライアントからのobserveリクエストがWeb Serverに対してgetを要求するものになる。
  3. あまりに多くのクライアントがgetを要求するため、Chat Serverの全ワーカースレッドがgetをWeb Serverに対して発行した状態でブロックされる。
  4. get要求を受けているWeb Serverのほうでは、そもそもRailsの負荷が高くなっているところへ、enterやsayなどのイベントをリアルタイムにChat Serverにnotifyしようとしても、Chat Serverのほうでスレッドが飽和しているためにどんどんキューに詰まっていく一方となり、まったく配信されない。そしてRails側の処理もnotifyの完了を待つためブロックされてしまう。=いわばデッドロック状態ということになる。
  5. notifyによる配信が完全に途絶えた結果、誰一人としてリアルタイムにイベントを受け取れるクライアントがいなくなり、全員がgetモードへと移行する。
  6. さらに、全体のサイクルがどんどん遅くなっているので、クライアントがgetの結果をobserve経由で受け取ったときには常にevent-idが古くなっているため、ますますgetに依存するようになる。20秒毎にタイムアウトで空いたRailsをつかって細々とgetが処理されていくが、全然追いつかない。
  7. つまり結果としてWeb Serverがパンクした状態でさらにgetループ(=インターバル・ゼロのビジーポーリング)に陥ってしまうので、ますますWeb Serverの負荷が増える、という最悪の事態に。

もうこうなってしまうと何が起きても不思議ではありません。

アーキテクチャの変更前は、getはブラウザからWeb Serverへ直に要求されていたので、少なくともChat Serverがgetによってブロックされ、それがデッドロックを引き起こすという問題は起きたことがありませんでした。

他にもいくつか小さな問題点はあるのですが、一番クリティカルなのは上記の問題でした。とにかくgetストーム状態に落ち込んだらどうにも打つ手がありません。

これに対応するために、現在ダニーが取り組んでいるのは以下の方法です。

  1. getを同期処理にするのをやめ、BackgrounDRbを使用して非同期にする。つまり、Railsのほうではgetリクエストを受け付けたら即座にChat Serverにレスポンスを返し、実際の処理はRailsから分離された別プロセス(BackgrounDRb)で行い、ここから別便でChat Serverに届けるようにイベントチェーンすることで、誰かが誰かにブロックされる部位を極小化する。
  2. いつもChat Serverはスカスカ、Web Serverが忙しいという傾向にあるので、サーバの構成比を見直し、Web Serverの比率を高くする。もちろんJettyのスレッドプール数もチューニングする。また、サーバ自体も追加する。

上記もまだ、仮説と実装と検証を繰り返す過程にあるので、最終的にどうなるかはわかりません。

他にも、

  1. 全てのイベントデータにはoccupantsという、anonymous observerも含めた全員分の参加者リストが毎回含まれているので、参加者が200人もいると、それだけで毎回発行されるイベントデータが巨大になった。
  2. レスポンスが遅くなればなるほどユーザがブラウザの再読込ボタンをクリックしてしまう確率が上がった。(再読込はダントツで負荷が高いが、こういうときのユーザのふるまいとしては当然なので仕方がない)
  3. 多数のLingr Radarが自動でリトライを続けたのでさらに負荷が上がった。

などなど、付随する小さな問題はいくつも考えられるのですが、どれもgetストーム問題によるデッドロックがroot causeであり、これさえ解決されれば自然と解消する問題なので、こいつから順番にやっつけているところです。

あとは Hot Rooms / Hot Tags の計算でボトルネックになっていたrecent messagesのカウントをmemcachedにキャッシュしたり、可能なところではどんどんフラグメント・キャッシュを使ったり、Railsを1.2.1に上げることでルーティングまわりが若干速くなったりと、パフォーマンスの改善については総合的な見直しを進めています。

近いうち、予行演習をかねて何かIT関連のネタでチャットイベントをやるかも知れません。そのときにはまた改めて告知させていただきますので、よろしくおねがいします!

中島美嘉 / 見えない星 (日テレ系水曜ドラマ「ハケンの品格」主題歌:大学時代からの親友・長瀬弘樹が作詞作曲を担当しました。おめでとう!)

※このエントリは CNET Japan ブロガーにより投稿されたものです。朝日インタラクティブ および CNET Japan 編集部の見解・意向を示すものではありません。

このエントリーへのコメント

5

過去幾つかの event-id 毎に、送るべきデータ(最新状態との差分?)をサーバで生成しておいて返してやるようにすれば負荷が軽くなるのではないでしょうか。
(クライアント毎ではなくイベント発生毎に生成するのでクライアントが増えても負荷は変わらない)
これである程度の取りこぼしを許容できるので「リアルタイムにイベントを受け取れるクライアントがいなくなり、全員がgetモードへと移行する」というのが救えるはず。これと似たような仕組みを普通の周期読み込みで最近実装しました。Cometだったらわざとイベント通知を間引いて負荷調整ができたりするかも知れませんね。参考になれば幸いです。

  ごろくま on 2007/02/08

4

hyoshiokさん、ありがとうございます。いただいたエントリを読んで、オラクルでパフォーマンスチューニングとベンチマークに明け暮れていた頃を懐かしく思い出しました。。。今回の件は、アプリケーションレベルで取っていたログでプロファイリングを行って、問題点については正しく把握できたと考えています。これから着実に手を打っていきたいと思います。

  kenn on 2007/02/07

3

http://blog.miraclelinux.com/yume/2007/02/lingr_9ccf.html
ブログに書きました。釈迦に説法ですが何かの参考になればと思います。

  hyoshiok on 2007/02/06

2

Chat ServerはなるべくDumb Serverになるようにしています。ほぼすべてのロジックとデータ生成はRails側で処理し、Chat Serverは単なるコネクションパーキング兼土管。これは、同一のnotify電文をブロードキャストするだけでクライアントがChat Server Clusterのどのマシンにぶらさがっていようが届くようにするためで、現時点のアーキテクチャにおいてスケーラビリティを確保するための肝になっています。仮にJettyをちょっと賢くして全Roomオブジェクトを持たせたとしても、Eventテーブルを引っ張るには結局Rails側に行かないと取り出せないので、本質的な解決になりえないのです。残念ながら。

  kenn on 2007/02/06

1

読んでいて疑問に思った箇所があるので脊髄反射的なコメントです。
「3.あまりに多くのクライアントがgetを要求するため、Chat Serverの全ワーカースレッドがgetをWeb Serverに対して発行した状態でブロックされる。」
ここの部分について、Chat Server上にはRoomオブジェクトみたいなものは存在しないのでしょうか?もし、Roomオブジェクトが存在して、Web ServerからのイベントをRoomオブジェクト経由でRoom参加者に配信する形でしたら、Room上でイベントをキャッシュしてObserver接続時にRoom自身がキャッシュからイベントを配信することで Web Serverへのgetを減らせそうな気がするのですが……。

  nak2k on 2007/02/06

ブログにコメントするにはCNET_IDにログインしてください。

この記事に対するTrackBackのURL: 

CNET_ID

メンバー限定サービスをご利用いただく場合、このページの上部からログイン、またはCNET_ID登録(無料)をしてください。