ずいぶん前に、「マルチスレッド・プログラミングの落とし穴、その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がスケーラビリティで苦しんでいるのをみると、同じ過ちは絶対に犯したくないので、ユーザー数の少ない今のうちに、根本的な設計でスケーラビリティを上げて置くべきだとつくづく思う。富豪プログラミングの時代とは言え、このあたりの設計を誤ると、いくらサーバーの台数を増やしたところで追いつかないので。
このあたりの設計に関しては、私よりももっと多くの経験を積んだ方がいると思うので、ぜひともコメントやトラックバックを通じた議論・ご教授をお願いしたい。