Tech Racho エンジニアの「?」を「!」に。
  • 開発

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的なデプロイソリューションを構築する: 前編(翻訳)

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


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。