マルチスレッド・プログラミングの落とし穴、その2
2008.09.22
ずいぶん前に、「マルチスレッド・プログラミングの落とし穴、その1(かもしれない)」というエントリーを書いたが、今回はPhotoShareサーバーを運営していて、まさにこのあたりの深い考察が必要になって来たので、良い機会なので続編エントリー。
PhotoShareのバックエンドのようにCRUD(Create/Read/Update/Delete)のAPIをサポートするバックエンドを作る場合、Create/Update/Deleteのリクエストに対してはクライアントからのAPIコール時にすぐに(HTTP Requestに返事をする前に)データベースに変更を加え、Readの際にも(キャッシュを使う・使わないを別にして)データベースの最新の状況を反映するデータを返すように設計するのが普通である。
このアーキテクチャの問題は、ユーザーのアクティビティが増えた時に、データベースやI/Oがボトルネックとなり、アクセス量に応じてレスポンスタイムが伸びるため、同時に処理しなければならないHTTP Requestの数が増え、さらにレスポンスタイムが遅くなり、結果的にはレスポンスタイムはリニアにでなく、幾何級数的(exponential)に増えていく。そのため、あるしきい値を超えたアクセスが来ると急激にレスポンスが遅くなり、しまいにはサーバーが落ちてしまう。
アクセス数が増える→HTTP Requestへのレスポンスタイムが遅くなる→同時アクセス数が増える→しまいにはサーバーが落ちる、という悪循環に陥るのだ。
通常、その場合はキャッシュを導入したり、スレッド・プロセス・CPUを増やしたり、データベースを多重化したり対処するのが普通だが、私はPhotoShareのようなCGMサービスに限って言えば、まず最初に、そもそものアーキテクチャを非同期なものにする必要があると考えている。
PhotoShareのように、大勢の人が写真やコメントを投稿してコミュニケーションをとるサービスの場合、オンラインバンキングのように「常に最新のデータを見せなければ大問題が起こる」サービスとは根本的に違う性質がある。具体的には、「この写真に付いたコメントを読みたい」というシナリオではすばやいレスポンスが要求されるが、自分が投稿した写真やコメントが他の人のアプリに反映されるまでの時間はそれほど重要ではない。新しい写真やコメントに関するNotificationも、最終的に届きさえすれば良い訳で、秒単位でのリアルタイム性は必要ない。
そう考えると、私にはCreate/Update/Deleteのリクエストに対して、クライアントを待たせながら(つまり、HTTP Requestの処理に必要なスレッド・プロセスを保持したまま)データベースに変更をかけることが根本的に間違っているように思える。
そうではなくて、Create/Update/Deleteのリクエストに関しては、そのリクエストをキューにしまい、クライアントにはすぐにレスポンスを返した上で(つまり、HTTP Requestの処理に必要なスレッド・プロセスはすぐに解放して)、別プロセス(それもシングルプロセス)でキューにたまったリクエストを順繰りに非同期で処理すべきだ。
アクセス数が上がると、ユーザーがした投稿がデータベースに反映されるまでの時間がかかるようになるが、それが直接的におもてなしの低下に繋がることはない(ユーザーから見ると、単にコメントのレスが遅くなったように見える)。
結果的には、先の悪循環が解消し、アクセス数が増える→ユーザーの投稿が反映されるまでの時間がかかる→ユーザー間のコミュケーションの速度が落ちる→ユーザーによる投稿数が自然に減る、というより自然なスケーラビリティを持った「より落ちにくい」サービスとして提供することが可能になる。
それに加えて、キャッシュの生成もオンデマンドで行うのではなく、Create/Update/Deleteのリクエストをキューから取り出してデータベースに変更を加えるときに行えば、Readは基本的に静的ファイルを返すだけになるので、データベースへのアクセスを極端に減らすことができて、WriteよりもReadの方が多いというCGMサービスに適した設計となる(MTとかはすでにそんな設計になっている)。
Twitterがスケーラビリティで苦しんでいるのをみると、同じ過ちは絶対に犯したくないので、ユーザー数の少ない今のうちに、根本的な設計でスケーラビリティを上げて置くべきだとつくづく思う。富豪プログラミングの時代とは言え、このあたりの設計を誤ると、いくらサーバーの台数を増やしたところで追いつかないので。
このあたりの設計に関しては、私よりももっと多くの経験を積んだ方がいると思うので、ぜひともコメントやトラックバックを通じた議論・ご教授をお願いしたい。
Rubyになりますが、AP4Rもそんな感じのメッセージングサービスを実現するツールだった気がします。釈迦に説法だと思いますがよろしければ。
Posted by: あっき | 2008.09.22 at 19:37
mixiやモバゲーなどが十分にDBをテーブル単位で分割したり、memcacheを導入することで対応しきれていることを考えると、twitterのように、ユーザ数に対して線形ではない速度でデータ量が増え、且つ最も使われるテーブルが(データ間が密に結合しているために)分割できないようなサービスでない限り十分スケールすると思います。
つまりtwitterはかなり特殊なので比較対象として正しくなくて、mixi程のユーザ数で十分スケールしているのだから自分のサービスもスケールするだろうと考えるのが一般的かと思います。
また遅延書き込みのモデルを採用すると、ユーザの書き込み要求が、node failureが生じる状況下で、少なくとも1回実行されることを保証するのか、多くても1回実行されることを保証するのか、正確に1回実行されることを保証するのかなど、新たな問題を考慮する必要が出てきます。このような問題を持ち込むことがPhotoShareというサービスの性質と照らし合わせて利点となりうるのかなどを十分考慮する必要があると思います。
Posted by: 通りすがり | 2008.09.22 at 22:22
アクセス数が増える
↓
ユーザーの投稿が反映されるまでの時間がかかる
↓
ユーザー間のコミュケーションの速度が落ちる
↓
ユーザーによる投稿数が自然に減る
というのは本末転倒なのではないかと。
サービス提供者としては、ユーザーのアクティビティ率を下げてまでスケーラブルにするというのは自殺行為なのではないかと思われます。
Posted by: John Doe | 2008.09.22 at 23:04
「私の写真」にいるときのように、データベースの最新の状況を反映しなければならないシーンのケアはどうしても必要なようですね。そうすると同期書き込み(もしくは非同期書き込み後の、対象を指定した同期読み込み)要求はやっぱり用意する必要があるということかな。。。
自分の要求はわかっているのでクライアント上で勝手に更新表示しておくこともできるけど、再表示などで読み込みが発生するときまでに書き込みが反映されている保証はないし。同期書き込みの優先度は上げたいけど時系列は崩せないからFIFOのシングルキューにせざるを得ないし。バグるわけにはいかないのでシンプルでエレガントなロジックにしなければいけないし。うーむ。
などど考えていたらお昼寝の時間を逃してしまいました。続きはジョギングしながら考えます。
Posted by: Tatsu | 2008.09.22 at 23:25
アクセスが増える
↓
ストレージへのI/Oリクエストキューが増える
↓
リクエストに対するレスポンス=リクエスト数/ストレージの処理能力(だいたい定数)
なので、リクエスト数が増えると1リクエストあたりのレスポンスが遅くなる
↓
レスポンスが遅くなるとリクエストキューがもっとたまる
↓
リクエストキューが増えるのでさらにレスポンスが遅くなる…
というように、ストレージへの書き込み、とくにランダムな書き込みはとても遅いので、いったんキューに入れてから、後でシングルプロセスでシーケンシャルに書き込むようにしたほうがいいのでは。
と思ったのですが、間違っているでしょうか?
もし私の理解で合っているのであれば、RDBのトランザクションログがだいたい同じようなことをしていると思います。
サーバーのストレージ構成をいじれるのであれば、トランザクションログファイルをデータファイルとは別のディスクに置くことで、書き込み時の遅延はある程度減らせるかもしれません。読み込み時の遅延は、メモリーをガンガン増やしてとにかくメモリーにキャッシュを載せれば何とかなりそうな気がします。
アプリケーション側でどうにかしようとかではなくて、データベースの設定でどうにかしようという、論点がずれていそうな話で恐縮なんですが…。
Posted by: tippex | 2008.09.23 at 05:09
>別プロセス(それもシングルプロセス)でキューにたまったリクエストを順繰りに非同期で処理すべきだ。
そうだね。こんな所で凝っても、後から後悔することになるだけだろうから。
時間の無駄ですね。
もちろん、スーパーマンなら別ですが・・・そう簡単には、いませんね。
というか、このサマリーだけ、開発始めにすれば、他に何か大筋で話すことあるっけか?
なんで、今頃。
コボルプログラムでもする話と同じレベルの話だと思うのですが。
やっぱり、この程度のボリュームで、サクサク動かないのは、問題ですか・・・
あと、ラージ画像のWEB上のファイル名ユニークにして。画像投稿の常識ですね。
PS.確か、大昔コボルでも2パターン作って、結局片方捨てた記憶がある。詳細は忘れたけど・・・
Posted by: You'll stealing from as? | 2008.09.23 at 07:47
リクエストのキューをどこに置くかを一応考えた方がいいような気がします。うっかりメモリに溜めて溢れさせたり、遅いディスクに書こうとしちゃって結局そこがボトルネックになったりしそう。
ユーザのコミュニケーション速度が落ちる、というのはあまり気にする必要は無いんじゃないかな?
体感できる程度に、かつ恒常的にデータ更新反映が遅れる状態だとどちらにしてもDBサーバなりのパフォーマンスが全く追い付いていない状態なので、同期的なアーキテクチャなら壊滅的にレスポンスが遅くなるケースだと思う。
Posted by: tagomoris | 2008.09.23 at 08:34
実際問題HTTPのコネクションを張るだけで数百msという莫大なコストがかかるという事を考えると どこまでをセッションキープで行い、どこまでを次のセッションで行うか?というバランスを設計するのがアートかなぁと。しかもケースバイケースですからね。その辺が職人芸だと思っています。
結局、チューニングする職人の腕と、環境の条件次第で どの程度スレッドにして、どの程度、非同期(ファイバー)にして、どの程度セッションを維持して、どの程度次セッションに回すかというバランスをアートしていくわけですからね。結局、研鑽の世界なんだと思います。
何が言いたいかというと、常にマルチスレッド有利という事もなければ、常に非同期有利という事もないと思います。OSによっては多少マルチスレッドで書いた方が有利な時もありますし、ファイバー状に非同期にすることで有利になることも多いかと思います。そして、たいがいの場合はマルチスレッドと、非同期を組み合わせていきますので、分散がアートであると。最初の主張に戻るわけです。
Posted by: 心は萌え | 2008.09.23 at 12:24
おかえりなさい。
なるほど。私の出入りしているRails界隈では、REST REST RESTと三連呼するのが当たり前なので、この問題提起はとても勉強になりました。思えば私の一つの専門であるComputer Trading界隈でも、注文は市場に投げた時点でまず完結、注文が成立した(ランデブー相手が見つかった)段階でnotificationをもらえる、というのは当たり前のことでした。PhotoShareのようなサービスでは、自分がコメントを書いた、というのは、自分の手元のiPhoneの表示上、コメントが書き込まれた表示になってれば気分は落ち着くのですから、いっか、と。
ただ、気になるのは、注文をキューに溜めたところで、更新処理負荷が時間軸に対して平滑化されるだけで、最後の最後はデータベースの負荷がいずれボトルネックになるということ。ストレッジとしてRDBMSを使っているのが間違いなのかも知れませんよ。これはTwitter騒動を見ていつも思う事です。PhotoShareもMySQLを使っていますが、コンベンショナルなRDBMSを使うのが本当にいいのかな、と。
Posted by: Koichi Hirano | 2008.09.25 at 00:23