web-dev-qa-db-ja.com

Rubyでメモリリークの原因を見つける

Railsコード-メモリリークを発見しました-つまり、whatコードリークを見つけましたが、whyリークします。Railsを必要としないテストケースに減らしました。

require 'csspool'
require 'Ruby-mass'

def report
    puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)[1].to_s + 'KB'
    Mass.print
end

report

# note I do not store the return value here
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))

ObjectSpace.garbage_collect
sleep 1

report

Ruby-mass おそらくメモリ内のすべてのオブジェクトを見ることができます。 CSSPoolracc に基づくCSSパーサーです。 /home/jason/big.cssは 1.5MB CSSファイル です。

この出力:

Memory 9264KB

==================================================
 Objects within [] namespace
==================================================
  String: 7261
  RubyVM::InstructionSequence: 1151
  Array: 562
  Class: 313
  Regexp: 181
  Proc: 111
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 29
  Gem::Requirement: 25
  RubyVM::Env: 11
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Memory 258860KB

==================================================
 Objects within [] namespace
==================================================
  String: 7456
  RubyVM::InstructionSequence: 1151
  Array: 564
  Class: 313
  Regexp: 181
  Proc: 113
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 30
  Gem::Requirement: 25
  RubyVM::Env: 13
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

メモリがway上がっているのがわかります。カウンターのいくつかは上がりますが、CSSPoolに固有のオブジェクトは存在しません。 Ruby-massの「インデックス」メソッドを使用して、次のような参照を持つオブジェクトを検査しました。

Mass.index.each do |k,v|
    v.each do |id|
        refs = Mass.references(Mass[id])
        puts refs if !refs.empty?
    end
end

しかし、繰り返しますが、これはCSSPoolに関連するものではなく、gem情報などだけを提供します。

また、「GC.stat」を出力しようとしました...

puts GC.stat
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))
ObjectSpace.garbage_collect
sleep 1
puts GC.stat

結果:

{:count=>4, :heap_used=>126, :heap_length=>138, :heap_increment=>12, :heap_live_num=>50924, :heap_free_num=>24595, :heap_final_num=>0, :total_allocated_object=>86030, :total_freed_object=>35106}
{:count=>16, :heap_used=>6039, :heap_length=>12933, :heap_increment=>3841, :heap_live_num=>13369, :heap_free_num=>2443302, :heap_final_num=>0, :total_allocated_object=>3771675, :total_freed_object=>3758306}

私が理解しているように、オブジェクトが参照されず、ガベージコレクションが発生した場合、そのオブジェクトはメモリから消去される必要があります。しかし、それはここで起こっていることではないようです。

Cレベルのメモリリークについても読みました。CSSPoolはCコードを使用するRaccを使用しているため、これは可能性があると思います。 Valgrindを使用してコードを実行しました。

valgrind --partial-loads-ok=yes --undef-value-errors=no --leak-check=full --fullpath-after= Ruby leak.rb 2> valgrind.txt

結果は here です。 RubyはValgrindが理解できないメモリで処理を行うことも読んでいるので、これがCレベルのリークを確認するかどうかはわかりません。

使用されているバージョン:

  • Ruby 2.0.0-p247(これは私のRailsアプリが実行するものです)
  • Ruby 1.9.3-p392-ref(Ruby-massでのテスト用)
  • ルビーマス0.1.3
  • CSSPool 4.0.0 from here
  • CentOS 6.4およびUbuntu 13.10
56
Jason Barnabe

ここにThe Lost Worldと入力しているようです。 raccのc​​バインディングにも問題があるとは思わない。

Rubyのメモリ管理はエレガントで扱いにくいものです。オブジェクト(RVALUEsと名付けられた)を、約16KBのサイズのいわゆるヒープに保存します。低レベルでは、RVALUEはc構造体であり、異なる標準Rubyオブジェクト表現のunionを含みます。

そのため、ヒープはRVALUEオブジェクトを格納します。サイズは40バイト以下です。 StringArrayHashなどのオブジェクトの場合、これは小さなオブジェクトがヒープに収まることを意味しますが、しきい値に達するとすぐに外部メモリが追加されます。 Rubyヒープが割り当てられます。

この追加のメモリは柔軟です。オブジェクトがGCされるとすぐに解放されます。そのため、big_stringのテストケースはメモリの上下動作を示します。

def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end
report
big_var = " " * 10000000
report
big_var = nil 
report
ObjectSpace.garbage_collect
sleep 1
report
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

しかし、ヒープ(GC[:heap_length]を参照)自体は解放されません一度取得すると、OSに戻ります。ほら、テストケースにちょっとした変更を加えます:

- big_var = " " * 10000000
+ big_var = 1_000_000.times.map(&:to_s)

そして、出来上がり:

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

メモリはOSに解放されなくなりました。これは、配列の各要素がsuitsRVALUEサイズとは、Rubyヒープに格納されます).

GCの実行後にGC.statの出力を調べると、期待どおりGC[:heap_used]値が減少していることがわかります。 Rubyには空のヒープがたくさんあり、準備ができています。

要約:考えられない、cコードがリークする。問題は、cssの巨大な画像のbase64表現内にあると思います。パーサーの内部で何が起こっているのかわかりませんが、巨大な文字列がRubyヒープカウントを増加させます。

それが役に立てば幸い。

38

さて、答えを見つけました。その情報は収集するのが非常に難しく、関連性があり、他の誰かが関連する問題を検索するのに役立つ可能性があるため、他の回答を残しています。

ただし、あなたの問題は、Rubyが実際にnotメモリをオペレーティングに戻すという事実に起因するようです。一度取得したシステム。

メモリ割り当て

Rubyプログラマーはメモリの割り当てを心配することはあまりありませんが、時々次の質問が出てきます:

大きなオブジェクトへのすべての参照をクリアした後でも、なぜRubyプロセスは非常に大きいままですか?私は/ sure/GCが数回実行され、大きなオブジェクトを解放しましたが、メモリのリーク。

Cプログラマーも同じ質問をする場合があります。

私はたくさんのメモリをfree()-edしましたが、なぜ私のプロセスはまだとても大きいのですか?

カーネルからユーザー空間へのメモリ割り当ては、大きなチャンクでは安価であるため、ユーザー空間はより多くの作業を行うことでカーネルとの相互作用を回避します。

ユーザー空間ライブラリ/ランタイムは、メモリアロケーター(例:libcのmalloc(3))を実装します。これは、カーネルメモリ2の大きなチャンクを取得し、ユーザー空間アプリケーションが使用するためにそれらを小さな断片に分割します。

したがって、ユーザー空間がカーネルにメモリを追加する必要がある前に、いくつかのユーザー空間のメモリ割り当てが発生する場合があります。したがって、カーネルから大きなメモリチャンクを取得し、その一部のみを使用している場合、その大きなメモリチャンクは割り当てられたままになります。

メモリをカーネルに解放することにもコストがかかります。ユーザー空間のメモリアロケータは、同じプロセス内で再利用でき、他のプロセスで使用するためにカーネルに戻さないことを期待して、そのメモリを(プライベートに)保持できます。 (Rubyベストプラクティス)

したがって、オブジェクトはガベージコレクションされ、Rubyの使用可能なメモリに解放された可能性がありますが、Rubyは未使用のメモリをOSに返さないため、プロセスのrss値は同じままです。ガベージコレクションの後でも、これは実際には設計によるものです Mike Perham によると:

...また、MRIは未使用のメモリを決して返さないため、デーモンは100-200しか使用していない場合でも300-400MBを簡単に使用できます。

これは本質的に設計によるものであることに注意することが重要です。 Rubyの歴史は、主にテキスト処理用のコマンドラインツールとしてのものであるため、迅速な起動と小さなメモリフットプリントを重視しています。長時間実行されるデーモン/サーバープロセス用に設計されていません。 Javaは、クライアントVMとサーバーVMで同様のトレードオフを行います。

15
Joe Edgar

@mudasobwaの説明に基づいて、私はついに原因を突き止めました。 CSSPoolのコードは、エスケープシーケンスの非常に長いデータURIをチェックしていました。エスケープシーケンスまたは単一文字に一致する正規表現を使用してURIでscanを呼び出し、mapの結果をエスケープ解除してから、joinで文字列に戻します。これは、URIのすべての文字に文字列を効果的に割り当てていました。 変更しました エスケープシーケンスをgsubに変更し、同じ結果(すべてのテストに合格)であると思われ、使用される終了メモリを大幅に削減します。

最初に投稿したものと同じテストケースを使用(マイナス[Mass.print出力)これは、変更前の結果です。

Memory 12404KB
Memory 292516KB

これは変更後の結果です:

Memory 12236KB
Memory 19584KB
9
Jason Barnabe

これは、Ruby 1.9.3以降の「Lazy Sweeping」機能が原因である可能性があります。

レイジースイープとは、基本的に、ガベージコレクション中に、Ruby作成する必要のある新しいオブジェクト用のスペースを作成するのに十分なオブジェクトを「スイープ」するだけです。これは、Rubyガベージコレクターが実行されますが、他には何も実行されません。これは、「Stop the world」ガベージコレクションとして知られています。

基本的に、レイジースイープは、Rubyが「世界を停止する」ために必要な時間を短縮します。レイジースイープの詳細については、こちらをご覧ください こちら

あなたのRuby_GC_MALLOC_LIMIT環境変数は次のように見えますか?

以下は Sam Saffronのブログ からの抜粋で、レイジースイープとRuby_GC_MALLOC_LIMITに関するものです。

Ruby 2.0のGCには2つの異なるフレーバーがあります。ヒープ内の空きスロットを使い果たします。

レイジースイープはフルGCよりも時間がかかりませんが、部分的なGCのみを実行します。目標は、短いGCをより頻繁に実行し、全体的なスループットを向上させることです。世界は止まりますが、時間が短くなります。

Malloc_limitはデフォルトで8MBに設定されていますが、Ruby_GC_MALLOC_LIMITを高く設定することで引き上げることができます。

あなたの Ruby_GC_MALLOC_LIMIT 非常に高い?鉱山は100000000(100MB)に設定されています。デフォルトは約8MBですが、Railsアプリの場合はかなり高くすることをお勧めします。高すぎる場合は、Rubyが削除されないようにすることができます成長する余地が十分にあると考えているためです。

9
Joe Edgar