Railsアプリのパフォーマンスを上げるために最近やったこと

運用しているサービスに突然大量のアクセスがあって、パフォーマンスのせいでアクセス数が頭打ちになっていた。レスポンスタイムもかなり大きく障害と言っていい状況だった。アプリケーションサーバのスケールアウト以外に、その対策としてしたことを書く。

やったこと

DBサーバをSSDに

RubyのバージョンアップとGCのチューニング

Ruby 1.9.3を利用していたが、開発マシンでテストを走らせてみて問題が無いようだったのでRubyを最新の2.1.2に変えた。

GCのパラメータ

Ruby2.1のRGenGCについて

あたりにざっと目を通してみた。

環境変数でGCのパラメータをいじる。

bkeepers/dotenv-deploymentを使ってとりあえず

RUBY_GC_MALLOC_LIMIT, RUBY_GC_OLDMALLOC_LIMIT, RUBY_GC_HEAP_INIT_SLOTS, RUBY_GC_HEAP_FREE_SLOTSを指定

OobGC

アプリケーションサーバにはUnicornを利用している。リクエストの処理中にはGCを走らせないようにした。

Gemfileにgctoolsを追加してconfing.ruに

require 'gctools/oobgc'
if defined?(Unicorn::HttpRequest)
  use GC::OOB::UnicornMiddleware
end

とかしておく。

ローテートしていなかったログをローテート

特にディスクI/O待ちで時間を食っているというわけではなかったが、ログのサイズがなり大きくなっていたので今後のことも考えてローテートするようにしておいた。

セッションストアの変更

Redis Storeを使っていたが、よりスケールするであろうCookie Storeに戻した。そのままでは、セッションの情報をサーバ側で破棄できないのでセッションハイジャックされるとずっとされっぱなしになってしまうという問題がある。

No, Rails’ CookieStore isn’t broken
にあるようにsessionに有効期限の情報を入れておくことで対応できる。

ごくごく一部の稀に使われる機能でCookie Storeでの容量制限を越えるデータを一時的に持ち回したいことがあって、その部分はRedisにツッコむことにした。

キャッシュの効率化

キャッシュにはRedis Storeを使っている。

  • DBサーバが貧弱だったのでなるべくDBサーバへのクエリを減らしたかった
  • バックグラウンドジョブでモデルのオブジェクトを使う
  • viewのテンプレートがユーザの権限やUserAgentによって分岐していてキャッシュしづらい

ということがあってモデルのオブジェクトをキャッシュしていた。これを使える部分ではフラグメントキャッシュを使うようにすることにした。

これには次のような利点があると考えられた。

  1. Appサーバの仕事を減らせる
    New RelicのEvents > Thread Profilerを見てみるとRubyでかかっている時間の結構な部分がテンプレートのレンダリングに使われていた。フラグメントキャッシュにすることでこの部分を削れるということが期待できた。

  2. Appサーバとキャッシュサーバ間の通信量を小さくできる
    モデルのオブジェクトをシリアライズしたものよりもフラグメントキャッシュのほうが容量が小さい。redisを使っているのでredis-clidebug objectで少し実験してみたところ、実際かなり小さくなりそうであることがわかった。これでネットワーク帯域制限がボトルネックになるのを大分先延ばしにできる。

ということでユーザの権限などによって変わる動的な部分の表示ををJavaScripに寄せて、必要な情報をHTMLのdata attributesに入れておきそれをフラグメントキャッシュでキャッシュすることにした。もちろんサーバサイドでも権限の確認を行っているのでセキュリティ上の問題はない。

JSON API, JSON APIと聞くけどブラウザやスマートフォンアプリなどクライアントの種類に依らず利用できるから程度に考えていたけれども、転送量が小さくなる、キャッシュしやすい、HTMLとしてレンダリングする計算コストをサーバから除ける(これはJSONとして出力する場合に比べて小さいのかはちょっと実験しないとわからないけど)、というのもあるんだなと思った。馴染みのあるJavaScriptのテンプレートエンジンやフレームワークが無かったということもあって、時間の関係上JSON API化するのはちょっと厳しかったので今回はやめておいた。

結果

案外時間がかかって波が過ぎてしまったので、最大throughputがどれぐらい伸びたかはわからないけれど

  • Redisを置いていたセッション兼キャッシュサーバのnetwork outはデプロイ直後に1/10程度になった
  • レスポンスタイムもnew relicの表示ではApp serverの部分で元々数百msだったのが平均で60ms程度になった。

反省

  • 時間がなかったので仕方なかったとはいえ、いくつかのものを同時に変えてしまって、それぞれがどれ程効いてたのかをよくみることができなかった。
  • 計測して、それを基に決断していくっていうアタリマエのことを全然できていない。
  • Viewのテンプレートは共通部分を分離したりして、layoutが入れ子になっていたが、キャシュをしようと思った時にそれに縛られてテンプレートがかなりグロテスクになった。早すぎる抽象化だったかもしれない。

今後

  • サーバサイドはJSON APIにしてクライアント側でデータバインディングを利用してHTMLを構築するということの意義がわかったのでVue.js, AngularJS etc.をいじってみたい。
  • 現在使っているVPSだとスケールアウトしようと思った時にChefを使っても結構時間がかかる。サーバの台数も増えてきたが常にフル稼働している状態ではないので時間課金でないことが少しつらくなってきた。他のVPSやAWSを使うとどれくらいの費用がかかるのかもう一度真面目に見積もる必要が出てきた。