例えば GC を止める・Ruby ウェブアプリケーションの高速化

最近クックパッドでは、アプリケーションサーバの大半が Rails 2.3 から Rails 3 に置き換わったのですが*1、リリース前のベンチマークの時点ではあまりパフォーマンスが出ず四苦八苦していました。具体的には Rails 2.3 の時と比べ MRI 1.8.7 だとレスポンスタムが200%ぐらい遅い結果でした。Rails 3 になって実装が Merb core を取り入れ疎結合で綺麗になった反面、より多くのオブジェクトと・メモリを利用する様になった影響かと思います。

そこで Ruby インタプリタの変更*2を行い検証をしたところ

  • MRI 1.8.7
    • (Rails 2.3と比べ) 約200%遅い
  • MRI 1.8.7 -> Ruby Enterprise Edition 1.8.7 2011.03 (tcmalloc 無効)
    • 約180%低速
  • MRI 1.8.7 -> Ruby Enterprise Edition 1.8.7 2011.03 (tcmalloc 有効)
    • 約140%低速

のような感じで、無視できないほど速度が遅く、これではリリースが危ぶまれました。なお、どこで速度が遅いのかというと、GC の実行でだいぶ時間を使っていました。Rails3 でオブジェクトが増えたため、より GC の実行時間がかかる様になってしまったようです。

Unicorn の導入

次にアプリケーションサーバApache + Passenger の組み合わせから Nginx + Unicorn の組み合わせに変更しました。Unicorn について簡単に説明すると、Rack 環境をロードした master プロセスが fork して子の worker を作るため、以下の様な特性を持ちます。

  • fork による worker 生成のため、子 worker の起動が高速
    • そのため再起動が高速になり、再起動時のサーバ負荷が少ない
    • ただユーザからの処理を捌かない master プロセスが必ず存在するため、メモリ使用量が1プロセス分増える
  • epoll / libev 等のイベントでうまく複数接続を捌くモデルでなく、worker 1プロセス1接続のため安定性が向上する
    • そのため、大量のコネクションを要求する必要なウェブサービスには向かない

Unicorn の特徴について、より詳しくは以下のエントリーで述べられてます。

実際に Apache + Passenger の組み合わせから Nginx + Unicorn の組み合わせにすると、わずかにスループットが上がるなあ、といった感じで大きなパフォーマンス改善は見られませんでした。これは Unicorn の特性が高速化よりも安定性や再起動時のメリット*3、シグナルを送るだけで操作できるシンプルさに重みが置かれてるからで、Rack というインターフェイスを通して操作される Ruby アプリケーションの直接的な速度にはあまり関わってこないことを考えても納得の結果でした。

明示的な GC.start

しかし Unicorn について調べている途中、Tuning UnicornUnicorn::OobGC という面白いアプローチを見つけました。
これは指定のリクエスト回数アプリケーションが処理を終えたら、ユーザにコンテンツを返した後に明示的に GC.start するというアプローチです。何が良いのかというと、GC がユーザのリクエスト外で実行するため、リクエスト中の処理では GC が走りにくくなり、結果ユーザへのレスポンス返却速度が高速化されるというわけです。
これを適用したところ、140%ぐらい遅かった速度が 130%ほどへとわずかに高速化されました。たぶん小規模なアプリケーションだと効果が結構見込めそうなのですが、メモリをだいぶ食ってる大規模なアプリケーションだとこれを使っても GC が頻繁に実行され、結果あまり速くならないようでした。

GC を止める

ここでいっそ GC.disable でユーザからのリクエスト処理中は GC を止めてしまい、リクエスト外で GC をしたらどうなるか、と思いついたので試してみました。
Unicorn は worker が master から fork される特徴があり、fork した直後のプロセスに対して設定で処理を書けるので、unicorn.conf.rb の設定に

after_fork do |server, worker|
  GC.disable if RAILS_ENV == 'production'

の、GC.disable で GC が走らない様にする処理を入れ、OobGC のコードを GC.disable 時にも GC.start を実行できる様に変更し、その後再度 GC.disable で止める処理を入れました。

この GC.disable のベンチマークを結果は以下です。

赤のグラフが GC.disable しなかったサーバで、青のグラフが GC.disable するサーバです。あからさまに GC をユーザからのリクエスト処理中は無効化した方が断然高速な結果に終わりました。Rails 2.3 のサーバと比べても65%遅い、もとい150%ほど高速な結果となりました!!

GC を止める Production のアプリケーションサーバに適用

というわけで、このユーザのリクエスト処理中は GC を無効化する方法でだいぶ高速化することが解ったので、一足先に Unicorn 化していた Rails 2.3 サーバ(Ruby のバージョンは MRI 1.8.7 で Ruby Enterprise ではない)に適用したところ、それだけで 130% ほど高速化 & CPU 消費量が減る *4 という素晴らしい結果になりました。

なお Rails3 ほど顕著に高速化しなかったのは、Rails3 にくらべメモリ使用量が少ないからかと思います。その後 Rails3 に刷新後は当初の Unicorn 化する以前の Rails 2.3 アプリに比べ、最終的に 150% ほど高速化しました。

まとめ

Web アプリケーションの速度の高速化は、IO 処理の高速化を除いてしまうと、チューニングすればするほど大きく改善する方法はなくなっていくと思ってましたが、ユーザのレスポンス処理中は GC を止め、その外で実行するだけで大きくパフォーマンスが改善しました。Ruby 1.8 系統を使ってるサービスは試してみる価値があると思います。
なお今後リリースされる予定の Ruby 1.9.3 では nari3 が実装された Lazy Sweep GC が載るので、OobGC 的な事を Ruby 自体が行ってくれ、GC の最大実行時間が減ると思うので、Ruby 1.9 なサービスは 1.9.3 に変えるだけでパフォーマンス改善がされそうですね。

おまけ・運用ノウハウなど

メモリリークの対応

GC.disable している期間が長いと、GC.start したときに元のコードがメモリリークしてると、よりそのメモリーを解放できない様な感じで、どんどん Rails アプリケーションのプロセスサイズが肥大化して行ってしまいました。
Unicorn は worker プロセスに SIGQUIT を送ると、ユーザのリクエスト処理が終わった直後にプロセスが死んで、master プロセスがそれを検知してすぐに fork する仕組みがあるので、以下のユーティリティを書いて Rack レイヤーで対応しました。

use UnicornKiller::Oom, 400 * 1024 # 使用メモリが400Mを超えると自分自身に SIGQUIT
use UnicornKiller::MaxRequests, 1000 # 1000 回リクエストを超えると自分自身に SIGQUIT

プロセスを終了しても即座に fork で新しい worker が作られるため、こんな感じのざっくりとした対応でも問題無く運用できてます。

Unicorn + bundler でのデプロイ時の問題

突然はまるのでちゅうい…。特に Unicorn は master プロセスへの SIGUSR2 (graceful な再起動)で再起動に失敗した場合、何事もなかったのように昔のプロセスは正常に生き続けるため、ぱっと見問題に気づきません…。

アプリケーションサーバベンチマークとパラメータの調整

ベンチマークには最初 ab (apache bench) でいくつかの URL でベンチ、次に JMeter で本番リクエストの上位80%のリクエストをエミュレートしてベンチをとりました。環境さえ作ればあとは叩くだけなので、ab / JMeter を使いつつ、アプリケーションサーバのメモリー数、CPU の利用状況を見て Unicorn のプロセス数の調整と Ruby Enterprise Edition の GC 周りの環境変数を調整しました。
この辺はサービスごとに最適値が違うと思うので、ちまちま数字変えてやるのが良いと思います。

*1:Rails 2.3 -> 3 移行はクックパッド規模になるとかなり大変でしたが、いろいろな面白いアプローチを試せたので、これはこれで何処かでお話ししたいですね

*2: Ruby 1.9.2 化も考えましたが、テスト通すまでまただいぶ時間がかかる&いっぺんにやると問題切り分けが難しいので一緒にアップグレードすることは見送りました

*3: graceful な再起動がほとんどエラーや負荷無く行える重要性は、ウェブサービスを運用してる方なら解ると思います

*4: CPU 使用率はあまり変わらないと思ってたんですが大きく減ったのが不思議な感じです…

さいきんの Rails サービスを高速化をしてみた

先日のももクロハッカソンで出会った wantedly を作ってる仲さんが

と言ってたので、面白そうなので wantedly を速くしてみました。

ちなみにデータが数百万オーダーもなさそうなのに、どのページもログインすると2-5秒ぐらいかかっていたので、確実に速くできそうだなぁという感覚はやる前からありました。

アプリケーションサイドのチューニング

初心者*1にありがちな問題として

  • SQL に適切にインデックス張ってない
  • キャッシュすべき場所をキャッシュしていない
  • 無駄なデータを引きすぎてる

ことがよくあります。ので順に実装を見ていきました。

SQLに適切なインデックスを張ってない

張ってありました!びっくり!\(^o^)/

キャッシュすべき場所をキャッシュしていない

Facebook API を利用したアプリケーションなんですが、ユーザのデータの取得を毎回馬鹿正直に HTTP を叩いてとっていたため、適切にキャッシュを入れました。これで1秒強ぐらい速くなりました。

無駄なデータを引きすぎている

うっかり全件データ(!) を引いていたり、引かなくて良いデータを引いていたり箇所をログと New Relic *2から見ていって直していきました。これでログインユーザが1-3秒ぐらい速くなりました。
SQL のインデックス張ってない問題やこれの問題は、データが少ない当初は遅いと感じなくても、データが増えれば増えるほど徐々に重くなっていき、また開発環境では本番と異なるデータを使ってるため、データ量が少ないこともあるため、初心者には気づきにくい & どこを直したら良いか解らないといったことが発生しがちでしょう。

結果

約6倍速\(^o^)/


体感速度のチューニング

続いてユーザが実際に感じる速度を向上させました。

wantedly は heroku にあるためサーバが遠く、1ファイルの取得に 200ms-300ms のタイムラグが発生します。そのため、以下のアプローチをとりました。

リダイレクトは極力減らす

リダイレクトするとそれだけで200-300ms遅く感じてしまうため、よくクリックするリンクはリダイレクトで無く、URL の生成でちゃんとリダイレクト先を生成するように変更しました。

静的ファイルをまとめる

Rails 3.1 からは 3.1 の目玉機能の一つ、asset pipeline が利用できるようになり、何も考えなくともルールに従えば js / css ファイルを一つにまとめられるようになったため、Rails 3.0 系から Rails 3.1.0 にアップグレードして asset pipeline を有効化しました。なお Rails 3.1.0 化はコード上 Rails 本体を弄る等無理な書き方をしてなかったため、割とすんなりアップグレードできました。

なお、heroku で asset pipeline をうまく使うには memcached plugin を入れ

# production.rb
  config.serve_static_assets = true
  config.assets.compress = true
  config.assets.compile = true

と設定するとうまく動くと思います。

静的ファイルをブラウザにキャッシュさせる

いわゆる Expires / Cache-Control ヘッダの指定ですね。Rails 3.1 + Asset Pipipele + Heroku でうまくキャッシュさせるには

config.assets.digest = true

で asset の URL が内容が変更があったら変わるようにし(じゃないとキャッシュされ続けるため)

config.static_cache_control = "public, max-age=#{1.day}"

で、Rack の ActionDispatch::Static で Cache-Controle ヘッダをはかせるようにします。

結果

以前

以後

ちょっとわかりにくいですが、青のラインが250msぐらい速くなり、体感的にもロードが速くなりました。特に Heroku のようなサーバが遠い場合は効果がだいぶ出ると思います。

終わりに

これ以上の高速化には国内のサーバにする & 動的では無いコンテンツにフロントでもう一段階ちゃんと Varnish 等を入れキャッシュ設計するが必要になりますが、それをするのはコストがかかりすぎるので、やるとしてもサービスがだいぶ伸びてからになると感じました。

あとよく Rails アプリ遅いと言われてますが、200ms 前後の速度でしたらサービスがだいぶ大きくならない限りは、Heroku 等の PaaS でも簡単に維持できるなぁと改めて感じました。*3

あとあと、初めてちゃんと Heroku 触ってみたんですがすごく良くできてますね。ただやっぱり太平洋超えるのは日本向けのサービスだとするとつらいなー、とも感じたので Heroku の ec2 の東京リージョン版が出たらより日本でも流行りそうですね〜。

*1:仲さんはプログラミング初めて一年、Rails 初めて数ヶ月ですが、Ruby の実装から html / css / js ほぼすべて一人で wantedly 作ってすごい…

*2:Heroku 無料で New Relic プラグインが使えて便利

*3:200ms だと遅いという方も居ると思いますが、だいたい重いと感じるサービスはもっともっと処理コストがかかってると思うので…

第一回ももクロハッカソンに参加して Acme::MomoiroClover リリースしました

最近はかなこ推しになりつつある、週末エンジニアの secondlife ですこんにちは。
9/4(日)にももいろ週末エンジニアの方々と都内某所で、第一回ももクロハッカソンを開き参加し、Perl ライブラリの Acme::MomoiroClover を作りました。

何故今更 Acme::MomoiroClover を作ったかと云うと、今までに日本のアイドルの Acme シリーズは二つ、Acme::MorningMusume と Acme::AKB48 があります。その Acme が存在するアイドル2ユニットに共通していえることの一つに、どちらも紅白歌合戦に参加したことがあることが言えます。つまるところ、日本の Acme::アイドル が作られたのユニットは100%紅白出場しているため、ももクロちゃんの今の目標である紅白歌合戦出場をほんの少しでもサポートできたらと思い、験担ぎの意味を込めて作りました!!

使い方

至って簡単!

use Acme::MomoiroClover;
my $momoclo = Acme::MomoiroClover->new;

とふつうに使おうとすると

MomoiroClover is obsolete. Please use Acme::MomoiroClover::Z 

となって使うことができません…。(マシンの時刻が2011年4月10日以前なら利用できます)

というわけで今は Acme::MomoiroClover::Z を使いましょう。

use Acme::MomoiroClover::Z;
my $momoclo_z = Acme::MomoiroClover::Z->new;

APIAcme::MorningMusume とほぼ同じ*1ですが、各 member に say メソッドがあり、自分のカラーで発言することができます*2

use Acme::MomoiroClover::Z;

my @members = Acme::MomoiroClover::Z->new->members('active');
my $count = 0;
for my $member (@members) {
  $count = 0 if (++$count >= scalar(@members));
  $member->say($members[$count]->nick->[0] . "〜");
}

Acme::アイドル について

Acme::アイドルに新しい1ユニットを追加したわけですが、Acme::MorningMusume の作成者、id:antipop 御大にお言葉をいただきました。

  • 「本当に大好きな時期は、メンテナンスコストはどうってことないけど、メンバー変更の精神的ダメージがつらい」
  • YAMLなどでのクラス自動生成は邪道。一個一個メンバーのクラスファイルがないと愛がない」

とのことで、気を引き締めていきたいと思う所存であります。

ももクロハッカソンの週末エンジニアたち

BGM としてスピーカからももクロの曲を流しながら流しながら作業をしてたんですが、お気に入りの曲になるとみんな歌うたったりコール出したり踊ったりするのでノリノリで作業できて超楽しかったです!特にももクロちゃん自己紹介と怪盗少女の時はみんながみんなノリノリになって一体感あふれてわくわくすぎました、また開催したい!!1

*1:ほぼ id:antipop さん作の Acme::MorningMusume のコピペモジュールなので…

*2: ピンクは端末が 256色対応じゃないと表現できないので明るい紫でごまかしております…