こんにちは。@srockstyleです。
Ruby On Rails便利ですね。フレームワークとして「Railsライクだから便利だよ!」ってはなすフレームワークがいっぱいでてきて久しいです。
今回はRailsで作ったアプリを高速化した話です。と言ってもこれは2015年くらいにやった作業なので、モダンな環境では通用しないかもしれません。
この記事の結論は結局「キャッシュいいね」となってしまいます。
最近のキャッシュトレンドな話はまた別に書きます。
ことのはじまり
当時の僕は一人でアプリケーションを書いてました。
作業もほぼ終盤に終わり、バグも潰し終えたとき、とある現象に気づきました。
ちなみにこれリモートのサーバです。DB接続のところはキャッシュつかうようにしたのである程度早かったり、Cloudのほうも設定したのでよかったんですが遅いですね。Viewが9ms、ARが17msなのに全体で1570msでした。
Chromeで計測したところ全部表示まで2.23秒かかってました。
他サイト
とりあえずやたら重そうなサイトをピックアップしてみたんですが、某巨大ECサイトは戻るまで160ms、某巨大ECモールは88msでした。これに対していま手元にあるアプリは全部返るまで2sでした。遅いですね。
意味不明な1500msの正体を探る
まずサーバのミドルウェアを疑ってみました。構成はnginx + Passenger V5(当時はV5が最新だったんだ!)
設定は問題なかったので、今度はキャッシュが溜まってるハズのmemcacheを見てみました。
<35 GET ********(伏せてます)
<35 Read binary protocol data:
>35 Writing bin response:
このログはキャッシュされてないというログです。つまり本来キャッシュされるべきところがキャッシュされてなかったわけです。
Active Record 0.0msへの道
書き込みをするクエリを投げることはなかったので、読み込みのクエリをゼロにすることを目指しました。このアプリケーション、財政的な理由でレプリカをつくれなかったんです。今考えるとそれも原因ですね。
書き込み用DBと読み込み用DBは別々にするのが基本です。
お金の問題はどうしようもありませんので、とりあえず読むほうをなんとかしようと思いました。
徹底的なキャッシュです。
キャッシュストアはmemcached + Dalli。当時はこれがトレンドだったんです。
Active Record書く時の処理を修正します。
Rails.cache.fetch("key"){
Model.where(flg: true).order("created_at DESC")
}
これは間違った例です。
これはActiveRecordのSQL発行前のものを入れている挙動をします。
そこでこうしました。
ret = Rails.cache.read(key)
if ret.nil?
ret = Model.where(flg: true).order("created_at DESC").to_a
Rails.cache.write(key,ret)
end
冗長ですね。
本来はfetchのARの後ろにto_aつけるだけでいいです。
とりあえずこれに書き換えることで複数呼んでくるActiveRecordはキャッシュされるようになりました。一度ちゃんと配列にしないといけなかったんですね。
find、find_by
とかいいつつ、RailsのActiveRecordのあれは一つだけじゃないです。findやfind_byなんとかもあります。
これらは以下のようにして解決しました。
ret = Model.find(id).attributes.symbolize_keys
attributesにしてからシンボルにするわけなんですが、attributeしたらなぜかhash[""]でしかアクセスできなかったんで意図的にシンボルにしてます。
誰かうまいやりかた知ってたらおしえてください……
こいつをキャッシュすることでfind側は解決しました。
リレーション
Railsを使ってると以下みたいなことがやりたくなります。
user = User.find(1)
pages = user.pages
上でattributesしてキャッシュするとただのハッシュになるので、これではリレーションの意味がありません。
ここはキャッシュ使うところで読み込んで配列にすることで解決してます。
ret = Rails.cache.read(key)
if ret.nil?
model = Model.where(flg: true).order("created_at DESC").to_a
ret = model.pages.to_a
Rails.cache.write(key,ret)
end
ここも悩んでこの形にしたので、なにかいい方法あったらおしえてください。
この三つの施策でActive Recordが0.0msで返せるようになりました。
ビュー側のレンダリングスピードをあげろ
となると次はビュー側です。viewのレンダリングで平気で300msとかかかったりするので、その辺でRailsの遅延は避けたかったです。
ここはフラグメントキャッシュを使いました。
フラグメントキャッシュ系については以下が詳しいです。
このサイトを参考に設定していきました。部分テンプレートを使いまわしているところが多かったので埋め込んでいきました。使い方はこんな感じ。
- cache obj do
## haml
hamlは後ろを綴じ忘れる必要がないのでいいですね。objにはループのなかのオブジェクトをいれることでキーを変えてくれます。
これを全体的に行った結果かなりの改善が見られました。
結果
トップページのレスポンスが156msまで減りました! やった!
Chromeのデベロッパーツールでみても300ms程度を推移してます。
もうちょっと早くできると思うのでそのあたりは現在試行錯誤中です。なにか良い方法ご存知の方がいたらぜひコメントをいただけると嬉しいです!
つまったこと
コードの書き方
当時はレビューがおらず、一人で上のコードを書いてしまったので、正しいかどうかがわかりません。
同様のことをやるのであれば、もうすこしいいやり方があると思います。
ビューファイルの一斉書き換え
全部hash[:key]かhash["key"]になるのでいままでobj.nameみたいに書いていたところ全部書き換えしてました。
いい方法があるはずです。
リレーションのキャッシュ
Rails特有のobj.store.nameみたいな書方が全滅したので辛かったです。これうまくやるGemを使ってみたりしましたが煩雑になるのでやめました。
あとで調査してあれば追記します。
まとめ
最初はミドルウェアのチューニングやネットワークを疑いましたが、ログにあるmsは結局アプリを返したときのログなので、そこはアプリ側の修正が必要でした。
このドキュメントが誰かの役に立てば幸いです。