Railsアプリのメモリリークについて

最近参加しているプロジェクトでどうやらメモリリークが発生しているということで調査したので、そのときに調べた調査方法などをまとめたいと思います。

夏に北海道に行きたいなぁと思っていながら行けなかった旅したかったエンジニアの三宅です。

今回の開発環境

まずは今回のお話に関係しそうな登場人物だけ紹介しておきます。

  • Ruby 2.4.4
  • Rails 4.2.10
  • Sidekiq 5.1.3

そもそもメモリリークってなんでしょう?

メモリリーク (Memory leak) とは、プログラミングにおけるバグの一種。プログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうことを言う。プログラマによる単純なミスやプログラムの論理的欠陥によって発生することが多い。(太字は筆者による)

https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%AF

辛い…辛すぎる。

いや確かにそうなんですけど、やっちゃうんですよ、だって人間だもの。

ただRubyはガベージコレクタが良しなにしてくれているんじゃないのかと思ってしまいます。

そこでまずはガベージコレクタが動いていて、うまくメモリが開放されているのかというところから調べてみました。

調べてみると一番お手軽なのはObjectSpace::memsize_of_allを使う方法があるようです。

require 'objspace'

Array.new(100_000, 'ruby')
puts "#{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"
=> 3.579259 MB

このObjectSpace::memsize_of_allってのが何をやっているかと言うと、メソッドの中ではObjectSpace::each_objectをぶん回して、これまたObjectSpace::memsize_ofで各オブジェクトのサイズを合計しているというものになります。

ObjectSpace::memsize_ofはsizeof(RVALUE)した結果が返され、RVALUEは64bitシステムの場合は40バイトだそうです。

またObjectSpace::memsize_of_allにしても、ObjectSpace::memsize_ofにしても、正確な数値ではないため、参考程度とするようにドキュメントに書かれています。

Note that the returned size is incomplete. You need to deal with this information as only a HINT. Especially, the size of T_DATA may not be correct.

http://ruby-doc.org/stdlib-2.5.1/libdoc/objspace/rdoc/ObjectSpace.html#method-c-memsize_of_all

とは言え、メモリが開放されないままになっているかどうかくらいはわかりそうです。

そこで以下のような感じ(擬似コード)でガベージコレクション前後で量が変わっているのか検証してみました。

require 'objspace'


# code ...
puts "Before: #{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"
GC.start
puts "After: #{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"

結果を見るとメモリが開放されずに残っているようには見えず、ガベージコレクション後はメモリの使用量もちゃんと減っていました。

mallocでマルチスレッドプログラムのメモリが倍増?

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)
https://techracho.bpsinc.jp/hachi8833/2017_12_28/50109

上記の記事によると、マルチスレッドRubyプログラムではメモリの断片化(またはアロケーション)はObjectSpaceの中で起きているのではなく、ほとんどがmallocによって起こっているものだということのようです。

事件は会議室で起きているのではない、現場で…!ってやつですかね。(違う

そしてこの問題を緩和するためにMALLOC_ARENA_MAXという環境変数を利用してスレッド単位メモリアリーナというのを制限するとメモリの断片化を改善できるということのようです。ただこれにもトレードオフの関係としてレスポンススピードが劣化することがあるようです。

またはmallocの代わりにjemallocをRubyで使うようにするとメモリの断片化を抑えることができるようです。

しっかりと理解できていない部分も多いので、詳しくは記事を参照してください。

このあたりはまだ試していないのですが、今のまま解決の糸口が見えないままということであれば、一度試してみようかと思っています。

まとめ

今回初めてここまで内部について調べたので、まだわからないことだらけですが、今までにはない経験ができて非常に有意義に感じています。

アプリを作るだけではなく、こういった言語内部のことまで理解して知識として自分の道具箱に納められるようになりたいものですね!

また新たに解決方法などが見つかりましたら、この場で皆さんに共有したいと思います。

TimeCrowdに戻る