apeescape2.com
  • メイン
  • 技術
  • モバイルデザイン
  • データサイエンスとデータベース
  • 製品の担当者とチーム
バックエンド

Rubyでのメモリの問題の追跡:決定的なガイド

幸運なRuby開発者の中には、メモリの問題に遭遇することのない人もいると思いますが、それ以外の人にとっては、メモリ使用量が手に負えなくなっている場所を探して修正するのは非常に困難です。幸い、最新のRuby(2.1以降)を使用している場合は、一般的な問題に対処するために利用できる優れたツールとテクニックがいくつかあります。その感情の中で私は一人かもしれませんが、記憶の最適化は楽しくてやりがいがあると言うこともできます。

Rubyでのメモリの問題の追跡

バグが厄介だと思った場合は、メモリの問題を探すまで待ち​​ます。 つぶやき

すべての形式の最適化と同様に、コードが複雑になる可能性が高いため、測定可能で大幅なメリットがない限り、実行する価値はありません。



ここで説明するすべては、正規のMRI Rubyバージョン2.2.4を使用して行われますが、他の2.1以降のバージョンも同様に動作するはずです。

メモリリークではありません!

メモリの問題が発見された場合、メモリリークがあるという結論に簡単にジャンプできます。たとえば、Webアプリケーションでは、サーバーを起動した後、同じエンドポイントへの呼び出しを繰り返すと、要求ごとにメモリ使用量が増加し続けることがあります。確かに正当なメモリリークが発生する場合がありますが、実際にはリークではない同じ外観のメモリの問題がはるかに多いと思います。

____テストは、モジュールまたはメソッドのグループの動作をテストします。

(考案された)例として、ハッシュの大きな配列を繰り返し構築して破棄するRubyコードを少し見てみましょう。まず、この投稿の例全体で共有されるコードを次に示します。

# common.rb require 'active_record' require 'active_support/all' require 'get_process_mem' require 'sqlite3' ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: 'people.sqlite3' ) class Person

そして配列ビルダー:

# build_arrays.rb require_relative './common' ARRAY_SIZE = 1_000_000 times = ARGV.first.to_i print_usage(0) (1..times).each do |n| foo = [] ARRAY_SIZE.times { foo << {some: 'stuff'} } print_usage(n) end

ザ・ get_process_mem gemは、現在のRubyプロセスで使用されているメモリを取得するための便利な方法です。私たちが見ているのは、上記と同じ動作であり、メモリ使用量が継続的に増加しています。

$ ruby build_arrays.rb 10 0 - MEMORY USAGE(MB): 17 1 - MEMORY USAGE(MB): 330 2 - MEMORY USAGE(MB): 481 3 - MEMORY USAGE(MB): 492 4 - MEMORY USAGE(MB): 559 5 - MEMORY USAGE(MB): 584 6 - MEMORY USAGE(MB): 588 7 - MEMORY USAGE(MB): 591 8 - MEMORY USAGE(MB): 603 9 - MEMORY USAGE(MB): 613 10 - MEMORY USAGE(MB): 621

ただし、さらに反復を実行すると、最終的には横ばいになります。

$ ruby build_arrays.rb 40 0 - MEMORY USAGE(MB): 9 1 - MEMORY USAGE(MB): 323 ... 32 - MEMORY USAGE(MB): 700 33 - MEMORY USAGE(MB): 699 34 - MEMORY USAGE(MB): 698 35 - MEMORY USAGE(MB): 698 36 - MEMORY USAGE(MB): 696 37 - MEMORY USAGE(MB): 696 38 - MEMORY USAGE(MB): 696 39 - MEMORY USAGE(MB): 701 40 - MEMORY USAGE(MB): 697

このプラトーにぶつかることは、実際のメモリリークではないこと、またはメモリリークが非常に小さいため、他のメモリ使用量と比較して表示されないことの特徴です。直感的ではないかもしれないのは、最初の反復後もメモリ使用量が増え続ける理由です。結局のところ、それは大きな配列を構築しましたが、すぐにそれを破棄し、同じサイズの新しい配列を構築し始めました。前のアレイによって解放されたスペースを使用するだけではいけませんか?私たちの問題を説明する答えはノーです。ガベージコレクターを調整する以外に、実行するタイミングと、build_arrays.rbに表示される内容を制御することはできません。例としては、古い破棄されたオブジェクトのガベージコレクションの前に行われる新しいメモリ割り当てがあります。

アプリのメモリ使用量が急増しても、慌てないでください。アプリは、メモリリークだけでなく、さまざまな理由でメモリが不足する可能性があります。

これは、特定の恐ろしいメモリ管理の問題ではないことを指摘しておく必要があります ルビー 、ただし、一般的にガベージコレクションされた言語に適用できます。これを安心させるために、Goを使用して基本的に同じ例を再現し、同様の結果を確認しました。ただし、この種のメモリの問題を簡単に作成できるRubyライブラリがあります。

分割統治

それで、大量のデータを処理する必要がある場合、問題に大量のRAMを投入する運命にあるのでしょうか。ありがたいことに、そうではありません。 build_arrays.rbを取る場合例として、アレイサイズを小さくすると、メモリ使用量がプラトーになり、アレイサイズにほぼ比例するポイントが減少します。

これは、作業を細かく分割して処理し、一度に存在するオブジェクトが多すぎないようにすることができれば、メモリフットプリントを大幅に削減できることを意味します。残念ながら、それは多くの場合、よりメモリ効率の高い方法で、きれいでクリーンなコードを取得し、同じことを行うより多くのコードに変換することを意味します。

メモリ使用量のホットスポットの分離

実際のコードベースでは、メモリの問題の原因はbuild_arrays.rbほど明白ではない可能性があります。例。実際に掘り下げて修正する前にメモリの問題を切り分けることが不可欠です。問題の原因について誤った推測をするのは簡単だからです。

私は通常、メモリの問題を追跡するために2つのアプローチを、多くの場合組み合わせて使用​​します。コードをそのままにしてプロファイラーをラップすることと、コードのさまざまな部分を無効/有効にしながらプロセスのメモリ使用量を監視することです。使用します memory_profiler ここでプロファイリングしますが、 ルビー教授 別の人気のあるオプションであり、 derailed_benchmarks Rails固有の優れた機能がいくつかあります。

大量のメモリを使用するコードを次に示します。どのステップがメモリ使用量を最も押し上げているかがすぐにはわからない場合があります。

# people.rb require_relative './common' def run(number) Person.delete_all names = number.times.map { random_name } names.each do |name| Person.create(name: name) end records = Person.all.to_a File.open('people.txt', 'w') out << records.to_json end

使用する get_process_mem 、Personが多い場合に、大量のメモリを使用していることをすばやく確認できます。作成中のレコード。

# before_and_after.rb require_relative './people' print_usage_before_and_after do run(ARGV.shift.to_i) end

結果:

$ ruby before_and_after.rb 10000 Before - MEMORY USAGE(MB): 37 After - MEMORY USAGE(MB): 96

コードを見ると、大量のメモリを使用するのに適していると思われる複数の手順があります。文字列の大きな配列を作成し、#to_aを呼び出します。 Active RecordリレーションでActiveRecordオブジェクトの大きな配列を作成し(素晴らしいアイデアではありませんが、デモンストレーションの目的で行われます)、ActiveRecordオブジェクトの配列をシリアル化します。

次に、このコードのプロファイルを作成して、メモリ割り当てが行われている場所を確認できます。

# profile.rb require 'memory_profiler' require_relative './people' report = MemoryProfiler.report do run(1000) end report.pretty_print(to_file: 'profile.txt')

runに供給される番号に注意してくださいプロファイラー自体が大量のメモリを使用し、すでに高いメモリ使用量を引き起こしているコードをプロファイリングするときに実際にメモリの枯渇につながる可能性があるため、これは前の例の1/10です。

結果ファイルはかなり長く、gem、ファイル、および場所のレベルでのメモリとオブジェクトの割り当てと保持が含まれています。探索する情報は豊富にありますが、興味深いスニペットがいくつかあります。

allocated memory by gem ----------------------------------- 17520444 activerecord-4.2.6 7305511 activesupport-4.2.6 2551797 activemodel-4.2.6 2171660 arel-6.0.3 2002249 sqlite3-1.3.11 ... allocated memory by file ----------------------------------- 2840000 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activesupport-4.2.6/lib/activ e_support/hash_with_indifferent_access.rb 2006169 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activerecord-4.2.6/lib/active _record/type/time_value.rb 2001914 /Users/bruz/code/mem_test/people.rb 1655493 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activerecord-4.2.6/lib/active _record/connection_adapters/sqlite3_adapter.rb 1628392 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activesupport-4.2.6/lib/activ e_support/json/encoding.rb

Active Record内で最も多くの割り当てが行われていることがわかります。これは、records内のすべてのオブジェクトをインスタンス化することを指しているように見えます。配列、または#to_jsonによるシリアル化。次に、これらの容疑者を無効にしながら、プロファイラーなしでメモリ使用量をテストできます。取得を無効にすることはできませんrecordsそれでもシリアル化の手順を実行できるので、最初にシリアル化を無効にしてみましょう。

# File.open('people.txt', 'w') out << records.to_json

結果:

$ ruby before_and_after.rb 10000 Before: 36 MB After: 47 MB

それは確かにほとんどのメモリが行くところのようであり、メモリの前後のデルタはそれをスキップすることによって81%低下します。また、大量のレコードを強制的に作成するのをやめるとどうなるかを確認できます。

# records = Person.all.to_a records = Person.all # File.open('people.txt', 'w')

結果:

Cコーディングを学ぶ方法
$ ruby before_and_after.rb 10000 Before: 36 MB After: 40 MB

これにより、メモリ使用量も削減されますが、シリアル化を無効にするよりも1桁少なくなります。したがって、この時点で、私たちは最大の原因を知っており、このデータに基づいて何を最適化するかを決定できます。

ここでの例は考案されたものですが、アプローチは一般的に適用可能です。プロファイラーの結果は、問題が存在するコード内の正確な場所を示していない可能性があり、誤解される可能性もあるため、コードのセクションのオンとオフを切り替えながら、実際のメモリ使用量を確認してフォローアップすることをお勧めします。次に、メモリ使用量が問題になる一般的なケースと、それらを最適化する方法について説明します。

デシリアライズ

メモリの問題の一般的な原因は、XML、JSON、またはその他のデータシリアル化形式から大量のデータを逆シリアル化することです。 JSON.parseのような方法を使用するまたはアクティブサポートのHash.from_xmlは非常に便利ですが、ロードするデータが大きい場合、メモリにロードされる結果のデータ構造は膨大になる可能性があります。

データのソースを制御できる場合は、フィルタリングやページ付けのサポートを追加するなど、受信するデータの量を制限することができます。ただし、それが外部ソースであるか、制御できない場合は、ストリーミングデシリアライザーを使用することもできます。 XMLの場合、 牛 1つのオプションであり、JSONの場合 yajl-ルビー あまり経験はありませんが、同じように動作しているようです。

メモリが限られているからといって、大きなXMLまたはJSONドキュメントを安全に解析できないわけではありません。ストリーミングデシリアライザーを使用すると、これらのドキュメントから必要なものを段階的に抽出しながら、メモリフットプリントを低く抑えることができます。

Hash#from_xmlを使用して1.7MBのXMLファイルを解析する例を次に示します。

# parse_with_from_xml.rb require_relative './common' print_usage_before_and_after do # From http://www.cs.washington.edu/research/xmldatasets/data/mondial/mondial-3.0.xml file = File.open(File.expand_path('../mondial-3.0.xml', __FILE__)) hash = Hash.from_xml(file)['mondial']['continent'] puts hash.map c.join(', ') end $ ruby parse_with_from_xml.rb Before - MEMORY USAGE(MB): 37 Europe, Asia, America, Australia/Oceania, Africa After - MEMORY USAGE(MB): 164

1.7MBファイルの場合は111MB!これは明らかにうまくスケールアップするつもりはありません。これがストリーミングパーサーのバージョンです。

# parse_with_ox.rb require_relative './common' require 'ox' class Handler <::Ox::Sax def initialize(&block) @yield_to = block end def start_element(name) case name when :continent @in_continent = true end end def end_element(name) case name when :continent @yield_to.call(@name) if @name @in_continent = false @name = nil end end def attr(name, value) case name when :name @name = value if @in_continent end end end print_usage_before_and_after do # From http://www.cs.washington.edu/research/xmldatasets/data/mondial/mondial-3.0.xml file = File.open(File.expand_path('../mondial-3.0.xml', __FILE__)) continents = [] handler = Handler.new do |continent| continents << continent end Ox.sax_parse(handler, file) puts continents.join(', ') end $ ruby parse_with_ox.rb Before - MEMORY USAGE(MB): 37 Europe, Asia, America, Australia/Oceania, Africa After - MEMORY USAGE(MB): 37

これにより、メモリの増加はごくわずかになり、非常に大きなファイルを処理できるはずです。ただし、トレードオフとして、以前は必要なかった28行のハンドラーコードがあり、エラーが発生しやすいようです。本番環境で使用する場合は、いくつかのテストを行う必要があります。

シリアル化

メモリ使用量のホットスポットの分離に関するセクションで見たように、シリアル化には高いメモリコストがかかる可能性があります。これがpeople.rbの重要な部分です以前から。

# to_json.rb require_relative './common' print_usage_before_and_after do File.open('people.txt', 'w') out << Person.all.to_json end

データベース内の100,000レコードでこれを実行すると、次のようになります。

$ ruby to_json.rb Before: 36 MB After: 505 MB

#to_jsonの呼び出しに関する問題これは、すべてのレコードのオブジェクトをインスタンス化してから、JSONにエンコードすることです。一度に1つのレコードオブジェクトのみが存在する必要があるようにJSONをレコードごとに生成すると、メモリ使用量が大幅に削減されます。人気のあるRubyJSONライブラリはどれもこれを処理していないようですが、一般的に推奨されるアプローチは、JSON文字列を手動で作成することです。あります json-write-stream これを行うための優れたAPIを提供し、例を次のように変換するgemは次のようになります。

# json_stream.rb require_relative './common' require 'json-write-stream' print_usage_before_and_after do file = File.open('people.txt', 'w') JsonWriteStream.from_stream(file) do |writer| writer.write_object do |obj_writer| obj_writer.write_array('people') do |arr_writer| Person.find_each do |people| arr_writer.write_element people.as_json end end end end end

繰り返しになりますが、最適化によってより多くのコードが得られたことがわかりますが、結果はそれだけの価値があるようです。

$ ruby json_stream.rb Before: 36 MB After: 56 MB

怠惰であること

2.0以降のRubyに追加された優れた機能は、列挙子を遅延させる機能です。これは、列挙子でメソッドをチェーンするときにメモリ使用量を改善するのに最適です。怠惰ではないコードから始めましょう。

拡張現実と複合現実の違い
# not_lazy.rb require_relative './common' number = ARGV.shift.to_i print_usage_before_and_after do names = number.times .map { random_name } .map name .map { |name| '#{ name } Jr.' } .select name[0] == 'X' .to_a end

結果:

$ ruby not_lazy.rb 1_000_000 Before: 36 MB After: 546 MB

ここで何が起こるかというと、チェーンの各ステップで、列挙子のすべての要素を反復処理し、チェーン内の後続のメソッドが呼び出される配列を生成します。これを怠惰にするとどうなるか見てみましょう。| ​​_ + _ |に呼び出しを追加するだけです。 lazyから取得する列挙子で:

times

結果:

# lazy.rb require_relative './common' number = ARGV.shift.to_i print_usage_before_and_after do names = number.times.lazy .map { random_name } .map .map { |name| '#{ name } Jr.' } .select .to_a end

最後に、多くの余分なコードを追加することなく、メモリ使用量を大幅に増やす例です。最後に結果を蓄積する必要がなかった場合、たとえば、各アイテムがデータベースに保存されてから忘れられた場合、メモリ使用量はさらに少なくなることに注意してください。チェーンの最後で怠惰な列挙可能な評価を行うには、$ ruby lazy.rb 1_000_000 Before: 36 MB After: 52 MB に最後の呼び出しを追加するだけです。

この例についてもう1つ注意すべき点は、チェーンがforceの呼び出しで始まることです。 timesより前。これは、呼び出されるたびに整数を生成する列挙子を返すだけなので、メモリをほとんど使用しません。したがって、チェーンの先頭で大きな配列の代わりに列挙型を使用できる場合は、それが役立ちます。

すべてを巨大な配列とマップに保持するのは便利ですが、実際のシナリオでは、そうする必要はめったにありません。

ある種の処理パイプラインに遅延フィードするための列挙型を構築する実際のアプリケーションの1つは、ページ付けされたデータの処理です。したがって、すべてのページを要求して1つの大きな配列に配置するのではなく、すべてのページネーションの詳細を適切に非表示にする列挙子を介してそれらを公開できます。これは次のようになります。

lazy

結論

Rubyでのメモリ使用量の特性評価を行い、メモリの問題を追跡するための一般的なツールと、それらを改善するための一般的なケースと方法をいくつか見てきました。私たちが調査した一般的なケースは、決して包括的ではなく、私が個人的に遭遇した種類の問題によって大きく偏っています。ただし、最大のメリットは、コードがメモリ使用量にどのように影響するかを考えることです。

関連: Rubyの並行性と並列性:実用的なチュートリアル

インサイドセールスエグゼクティブ-EMEA

その他

インサイドセールスエグゼクティブ-EMEA
最適化された連続平均量子化変換

最適化された連続平均量子化変換

データサイエンスとデータベース

人気の投稿
ラベルなしデータを使用した半教師あり画像分類
ラベルなしデータを使用した半教師あり画像分類
効果的なランディングページをデザインする方法
効果的なランディングページをデザインする方法
コミュニケーションディレクター
コミュニケーションディレクター
3Dグラフィックス:WebGLチュートリアル
3Dグラフィックス:WebGLチュートリアル
インサイドセールスエグゼクティブ-東部地域
インサイドセールスエグゼクティブ-東部地域
 
デジタル遊牧民のための人間工学:自殺せずに道路で働く
デジタル遊牧民のための人間工学:自殺せずに道路で働く
Webレイアウトのベストプラクティス:分析された12の時代を超越したUIパターン
Webレイアウトのベストプラクティス:分析された12の時代を超越したUIパターン
生産的な行動の誘発:仕事の動機付けのヒント
生産的な行動の誘発:仕事の動機付けのヒント
HorusLPを使用した最適化アルゴリズムの設計
HorusLPを使用した最適化アルゴリズムの設計
WebVRとブラウザエッジコンピューティング革命
WebVRとブラウザエッジコンピューティング革命
人気の投稿
  • プロジェクトマネジメントオフィス(pmo)
  • モバイルウェブデザインのベストプラクティス2018
  • ヘッダーファイルの使用c ++
  • セレンのページオブジェクトモデルとは
  • sボディvscボディ
カテゴリー
分散チーム データサイエンスとデータベース プロジェクト管理 アジャイル 投資家と資金調達 財務プロセス 人とチーム 革新 ツールとチュートリアル 設計プロセス

© 2021 | 全著作権所有

apeescape2.com