Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

貯金箱でHerokuの費用節約をイメージ

理論的には、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)

Heroku R14 - Memory Quota Exceeded in Ruby errors

メモリ使用量が急上昇した後に、メモリエラーが大発生して自動的に再起動した

この問題を調査してメモリ使用量を最適化したところ、グラフは以下のようになりました。

Heroku R14 - Memory Quota Exceeded in Ruby fixed

メモリ使用量が安定し、その後ガベージコレクションが行われた

方法は次のとおりです。

1. Gemfileをダイエットする

Ruby世界にはさまざまなお便利gemがありますが、gemの削除は多くの場合最も楽チンな問題解決方法です。メモリの肥大化は、その他のコストよりも見落とされがちです。

gemごとのメモリ使用量をチェックするには、derailed benchmarksが最適です。

gem 'derailed_benchmarks', group: :development

Gemfileに上を追加してbundle exec derailed bundle:memを実行するだけでメモリ使用量をチェックできます。

私のプロジェクトではTwitterFacebookのボットプロファイルを強化します。今回驚いたのは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ドル節約しませんか。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

データベースのランダム読み出しは要注意(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ