Photo by harry harris
いまPhotoShareのサーバの実装を大きく変えようとして悩んでいます。 (参考: Life is beautiful: マルチスレッド・プログラミングの落とし穴、その2)
Rails 2.2でThread safeになるとか、NeverBlockで12倍速くなるっていう話もあるんだけど、負荷が上がればレスポンスが悪くなるのは、どうしようもない。マシンを増やせば解決できる部分もあるけど、マシンを増やせばコストは上がる。
Life is beautifulで書かれていますが、確かに全部の処理を同期的に行う必要はないんですよね。
PhotoShareでも、既にいくつかのページは非同期にerbを生成して、それをRailsとerubisで読み込んで実行しています。
しかし、Railsだけではこういった非同期の処理やviewの一部を事前に生成するという処理ができないので、この処理は別途プラグインを作って実現しています。
高速化の為にはキャッシュを使おう
Railsで高速化を考えていくと、特にキャッシュが重要になります。たとえばブログエンジンで、RSS Feedを生成するアクションがあったとします。
通常Railsで組むと、Feedを生成するアクションでページキャッシュを行なうようにします。こうする事で、Feedを生成する処理は一度だけ行い、あとはApacheがキャッシュファイルを返すようになり、Rails側の負荷が下がります。
この時、ブログを更新する処理を行なうと、Sweeperによって先ほどのキャッシュが破棄され、次回Feedのアクションにページにアクセスしたときに、再度Feedが生成されるようになります。
これでは、更新の度にキャッシュが破棄/生成され、処理待ちが発生してしまうことが考えられます。
そこで、更新の時にキャッシュを破棄するのではなく、投稿の処理の自体を非同期にして、更新処理を行った後、Feedなど関係するページを事前に生成します。
そして、Feedは常に事前に生成したファイルを返すようにします。こうすることで、投稿があった直後にFeedへアクセスした場合でも、キャッシュの破棄は行われずに、キャッシュ(というか非同期に生成したページ)を返します。
この非同期処理をキューで管理するようにすれば、ページの更新が沢山あってもFeedへの反映が遅くなるだけで、Feedそのもののレスポンスは悪化しません。
Webだってアクションごとに優先順位があるよね
重要なのは、RailsではFeedのアクションも普通のページも同じプロセス群で同じ優先度で処理されることです。
ほとんどのFeedは人間ではなくロボットからのアクセスです。人間と違ってレスポンスが悪かったり反映が遅くても問題になりません。
そのため、Feedアクションのレスポンスが悪化しても、それ自体は問題ないのですが、Railsが同時に処理できるのは、Feedや普通のページを含めて一定数です。(mongrelの起動数など)
その為、Feedのアクションが重いと、処理待ちが増え、普通のページのレスポンスも悪化します。
コントローラごとに実行するmongrelを振り分けるようなルールをリバースプロキシに書けばある程度回避できますが、どのように振り分けるのか決定するのは困難です。(Feedなら簡単だけどもっと難しいケースもあるよね)
このように実はアクションごとに優先順位があります。Feedやランキングと言った部分は、反映が遅くてもほとんど問題になりませんが、自分の写真リストや写真そのものはすぐにアクセスできるようにしたいのです。
現在のRailsはコントローラごとに優先順位を付けるような処理は苦手なので、これを行うには、なんらかの仕組みが必要になると思います。
でも全部を非同期にする訳にはいかないし
もちろん、処理のうち非同期にできる部分とできない部分があります。特に問題になるのは、エラー処理です。
ブラウザでは、一度レスポンスを返してしまうと、サーバからブラウザにメッセージを送る方法がありません。
その為、非同期処理の中でエラーがおこった場合、それをどのようにユーザに伝えるかが問題になります。
ブログの記事を書く処理では、タイトルの有無や、添付している画像のチェックなど、処理が正しく行えるかのチェックは非同期処理にまわす前にチェックします。事前にチェックできないものについては、何らかの方法であとで通知する仕組みが必要です。
投稿ボタンを押した後、処理中画面を出して、Ajaxを使って数秒おきにサーバへ確認にいくか、別のページに遷移したときに通知する方法で行けるかなと思ってます。
また事前に生成しにくいページもあります。PhotoShareの「すべての共有写真」では、100人フォローしている場合、100人分の写真リストがマージされてきます。
逆に、100人にフォローされている人がいる場合には、その人が写真をアップするたびに100人分の「すべての共有写真」を更新する必要があります。
これでは、写真をアップすることに100人分のXMLを生成する事になります。
しかし、ほとんどのユーザは誰かがアップする度に写真を見るわけではありません。このようなケースの場合は、いままでと同じように同期で処理をした方がよいでしょう。
このような処理の場合は、通常のキャッシュやDBのチューニングなどが有効だと思います。
Rails捨てちゃおうか
こんな風に非同期処理を行う為のRails向けのライブラリはいくつかリリースされていて、BackgroundFuやAP4Rなんかがメジャーです。
Photo by robotgirl
非同期処理をさせるためにRailsとこれらのソフトを使う方法もあるのですが、キャッシュの事前生成などを考えると、Railsのメリットがないので、メッセージキューとviewの事前生成を中心にしたフレームワークっぽいものを自作しようかなと思っています。
ユーザ登録、管理画面などRailsを残したい部分もあるので、データベースはActiveRecordのままで、ActionPackに相当する部分をRackの上で自作しようかと考えています。
実際の構造についてはなるべく早く次のエントリで書く予定です。
まだ漠然とした話なんですが、もし、お知恵がありましたら、コメントをもらえると幸いです。
ogijun
求めてる答とは違うと思いますが、私が以前やったケースだとUserAgent毎にフロントエンドを分けて、ロボットのアクセスで人間用のサーバが重くならないようにしてました。人間か機械かを判断するだけならこれで概ねOKなんだけど、もっときめ細かくやりたいわけですよね。
あと、そもそも優先順位が全然違うのであれば、優先順位別にアプリを分けてしまう(切り分けの条件をこの観点から行う)というのもいいのではないかと思います。ローテクですが。
ita_wasa
解決策ではなく、Amazon のお店ではこうやってるよ、というだけのコメントです。
注文処理は、受付のみ同期で、クレジットカードの処理などを非同期にしているっぽいです。そして、そこでエラーが起きたらメールで通知、という真っ当な形になってるようです。(口座に金が足らずにメールが来たことがあるのです :-<)