概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Limit Rails memory usage, fix R14 and save money on Heroku
- 原文公開日: 2018/01/15
- 著者: Paweł Urbanek
Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)
理論的には、Herokuの512MB dynoが1つあればRails webサーバーとSidekiqプロセスを両方とも動かせます。トラフィックの少ないサイドプロジェクトで月7ドルを節約できればとても助かります。残念なことに、1つのdynoでRubyプロセスを2つ動かすとメモリの問題が生じることがあります。本記事では、Railsアプリのメモリ使用量を制限する方法について説明します。
最近読んだBilal Budhaniの良記事では、1つのHeroku dynoでSidekiqプロセスとPumaを同時に実行する方法について説明していました。私のサブプロジェクトの1つに適用したところ、R14エラーが大発生しました。
Error R14 (Memory quota exceeded)
この問題を調査してメモリ使用量を最適化したところ、グラフは以下のようになりました。
方法は次のとおりです。
1. Gemfileをダイエットする
Ruby世界にはさまざまなお便利gemがありますが、gemの削除は多くの場合最も楽チンな問題解決方法です。メモリの肥大化は、その他のコストよりも見落とされがちです。
gemごとのメモリ使用量をチェックするには、derailed benchmarksが最適です。
gem 'derailed_benchmarks', group: :development
Gemfile
に上を追加してbundle exec derailed bundle:mem
を実行するだけでメモリ使用量をチェックできます。
私のプロジェクトではTwitterやFacebookのボットプロファイルを強化します。今回驚いたのはtwitter gemが起動時に13MBものメモリを消費していたことです。最初にこのgemをより軽量なgrackleに置き換え(〜1MB)、最終的にTwitter APIへのHTTP呼び出しを行うカスタムコードを書きました。同様にkoala gem(〜1MB)も何とか取り除けました。
もうひとつ効果的だったのは、gon gem(〜6MB)をJavaScriptのカスタムデータ属性に置き換えたことです。
たった数行のJavaScriptコードを書かずに済ませたいという理由で、数MBのメモリを消費するgemファイルをいくつもインポートするのはぜひとも避けるべきです。
2. jemallocを使う
jemallocは、公式のMRIメモリアロケータの代わりに使えます。Herokuの場合、buildpackを使ってjemallocを追加できます。その結果、私のアプリではメモリ使用量が最大20%も削減されました。production環境にデプロイする前に、staging環境でjemallocを徹底的にテストしておきましょう。
3. コンカレンシーとワーカー数を制限する
トラフィックの多くないサブプロジェクトでは、スループットはさほど必要にならないでしょう。SidekiqやPumaのワーカー数やスレッド数を減らすことでメモリ使用量を制限できます。私のconfig/puma.rb
は以下のとおりです。
threads_count = 1
threads threads_count, threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "production" }
workers 1
preload_app!
on_worker_boot do
@sidekiq_pid ||= spawn('bundle exec sidekiq -t 1')
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
on_restart do
Sidekiq.redis.shutdown { |conn| conn.close }
end
plugin :tmp_restart
config/sidekiq.yml
は次のとおりです。
---
:concurrency: 1
:queues:
- default
- [critical, 100]
Pumaは、スレッドの最大数に1を指定しても最大7つまでスレッドを生成できます。これらの最小限の設定でも、私のSmart Wishlistアプリは100K程度のSidekiqジョブを引き続き処理可能であり、ReactフロントエンドとモバイルJSON APIの両方のサービスをこなしています。
4. JSONパーサーを最適化する
これらのSidekiqジョブは、iTunes API(一括リクエストは私のToDoリストで行います)から最新の価格をダウンロードしたりディスカウントを通知したりするのに必要です。つまり、そこではJSONのパースが相当行われているということです。こういう場合は、以下の1行修正でメモリ使用量とパフォーマンスの両方が改善されます。
gem 'yajl-ruby', require: 'yajl/json_gem'
yaji-rubyは、JSON gemと互換性のあるAPIを提供します。JSON.parse
呼び出しをフックしてパフォーマンスとメモリ使用量を改善します。
まとめ
制限された環境での作業は、プログラミングのスキルを鍛えて新しい最適化方法を発見するよい方法のひとつです。理論的にはサーバーのメモリを増やせばいつでも問題を解決できますが、それよりも7ドル節約しませんか。