ども、@kimihom です。
今うちが運用しているサービスの Heroku メトリクスを眺めていたら、いつの間にかメモリが90%近くまで届く瞬間が来てしまって、いよいよこれは R14(Out of Memory) が発生してしまいそうな感じが出てきたのでいろいろ対策を練った。
詳しい方法は Heroku 公式の英語ドキュメントに書いてあるが、そのうち効果的と思われる幾つかのことを実践したのでその結果とともにまとめようと思う。
Derailed Benchmarks Gem の導入
Derailed Benchmarks は Rails 及びその他 Ruby アプリの各種ベンチマークを取ることのできる Gem だ。
この中で、最も手軽に実践できるのが、 bundle exec derailed bundle:mem
コマンド。 これは、 Gemfile で使っている Gem のうち、どの Gem がメモリを食っているのかを調べることのできるツールである。これで、例えば明らかにメモリを食い過ぎている Gem があれば、その代替を探して置き換えたりすることで Rails アプリ起動時のメモリを抑えることができる。
私はこれで 80 MiB くらいだったのを 60 MiB くらいに下げることに成功した。明らかに無駄にメモリを消費している Gem を見つけられたのが幸運だった。これにより、Heroku Dyno 再起動直後のメモリ使用量をだいぶ抑えることに成功した。
とはいえ、Heroku の1日1回行われる再起動の時にメモリがガクッと落ちて、しばらくするとまたどんどん上がっていってしまう問題は継続中だった。
そこで Derailed には、その他に各アクションごとに定期的にリクエストを走らせてメモリリークが起きていないかをチェックできるツールなども用意されている。ただこれは URL, HTTP メソッド ごとに設定を変えて何度もやらないといけないし、見つけるのが結構困難だったので、私の場合はうまくいかなかった。
Scout の導入
Scout は Rails アプリ専用のモニタリングツールだ。この分野では NewRelic がダントツで有名かとは思うが、あの Gem 自体も結構メモリを食うし、何より最近重いとの評判だ。そんな中、 Scout は Rails のモニタリングツールの新星として登場してきた。
Scout のいいところは、 N+1 クエリを勝手に集計して管理画面で対象アクションを表示してくれたり、メモリーブロートの起きたアクションを表示してくれたりするという点。 それ以外のメトリクスってぶっちゃけ Heroku 標準の メトリクスで十分だよねってのがある。
ここで、メモリリークとメモリブロートの違いについてちょっとだけ説明する。詳しくは Scout のドキュメントに書いてある。メモリリークはリクエストがどんどん来て、ちょっとずつ徐々にメモリが増加していってしまう問題。Ruby の GC がうまく働かないようなソースを書くと、これがどんどん増えていってしまう。リクエストが増えれば増えるほど、その増加率は高くなって、しまいには Out of Memory (R14 エラー) となって極端に Rails アプリが遅くなる現象が起きる。対してメモリブロートはある特定のユーザーがスロークエリを投げてサーバー内のメモリが一気に増加する現象だ。ここで、両者について Scout のドキュメントでは以下のような記述がある。
If your app is suffering from high memory usage, it’s best to investigate memory bloat first given it’s an easier problem to solve than a leak.
Rails アプリが高メモリの問題を抱えているなら、まずはメモリブロートを調べるのが最善の策である。それはメモリリークよりも解決するのが簡単である。
ってことで、 Scout アドオンを導入すると、メモリブロートのリクエストが何だかってのが一発で表示される。
今回の例では、人のアイコンが一人だけなので、1つのリクエストで一気にメモリブロートが発生したことがわかる。それぞれどこでメモリを食っているのかがわかるので、何かしら対策ができるとのこと。
ここで私が今悩んでいるのが、対象のアクションはわかったものの、具体的にどう直せばいいのかが全くわからないという点だ(致命的)。よく見てみると、どうやらDynoの再起動直後のリクエストがほぼ間違いなくメモリブロートが起きているようだが、これを対策することなんてできるのだろうか・・。一応 Scout のドキュメントにあるのが ActiveRecord の select
を用いて text や binary のカラムを無駄に取ってこないようにする、N+1 クエリーを除去する、無駄にたくさんのオブジェクトをレンダリングしない、重いファイルアップロードを Rails を通して行わない、といったものだ。ひとまず ちょっとした N+1 クエリを Scout で見つけることができたので、そこでどれくらい改善できるかを見ようと思っている。
N+1 クエリも今までは 適当に includes
を使って対応してたけど、eager_load
や preload
, joins
といったのをちゃんと使い分けた方がいいことを学んだ。ここら辺ちゃんと学んでおいた方がいいだろう。
素晴らしいまとめ! 結合先で where 使うときは eager_load, 使わないときは preload って感じか。ActiveRecordのjoinsとpreloadとincludesとeager_loadの違いhttps://t.co/Mtp36OpvCr
— kimihom (@kimihom) 2016年10月4日
それでも解決できないなら・・
それでも解決できず、メモリ使用量が100%超えを連発してしまっているなら、 Dyno アップグレードの時が来たということなのかもしれない。どれ選べばいいのっていうのも Heroku の記事として上がってるので、これをちゃんと読むようにしよう。
ほとんどは standard-1x
dyno を増やしていけばいいように思うが、どうなんだろうか。ここら辺の知見がとても欲しい!
終わりに
まだまだメモリ使用量が落ち着いている状況ではないため、引き続き調査・実験・検証していく次第だ。
幸いにも最後の手段として Standard-2x に上げたり、 Standard-1x を3,4個くらい増やすくらいの金銭的余裕はあるので、ここら辺も実践してみてどれくらい改善したのかが分かり次第レポートできればと思っている。
本格的に Heroku で Web サービスを運営するなら、パフォーマンス問題は必ず通る道だと思うので、まずは私が突っ走っていきたいと思っている次第である。