Ruby 製バッチ処理を省メモリ化した
少し前に Ruby 製のバッチ処理を省メモリ化したときの話をメモしておきます。
どのようなバッチ処理だったか
動画共有サイトにアップされた動画がオトナの事情によって削除されることがしばしばあるということは周知のことだと思う。そこで、一定時間おきに配信元サイトをチェックして、もし動画が配信元から削除されていたら、当該動画をこちらのサイトでも動画一覧に表示させないようにする、検索に引っ掛からないようにする、ということをやっていた。
便宜上、そのバッチ処理のことを「(動画の)存在確認くん」と呼ぶ。
oom-killer に殺されていた
彼は非常にデキる子だったが、なんか最近、動画の落とし漏れがあるなーと感じた。原因を調べたら、メモリを食い過ぎて oom-killer に殺されていた。
cat /var/log/messages Apr 6 12:46:45 xxx001 kernel: Out of memory: Kill process 24085 (ruby) score 730 or sacrifice child
まあたしかに存在確認くんがメモリ大食いだとは認識していたが、チェック対象の動画数増加に伴いメモリ使用量もどんどん増えていって、とうとう限界に達したということか。
動画の存在確認くんのコード
仕方ないので省メモリ化することにした。簡単にコードを示しておく(ただし、コードはイメージなのでそのままでは動かない)
# 存在確認くん(バッチ処理。コードはイメージです) Video.find_each do |video| video.delete! unless video.existing? end
# コードはイメージです class Video < ActiveRecord::Base def deleted? match_removed? || match_deleted? end def doc # 動画配信元ページの HTML をキャッシュ @doc ||= Nokogiri::HTML(open(source_url)) end def existing? !deleted? end private def match_deleted? doc.at_css('.message').try(:text).try(:match, 'This video has been deleted') end def match_removed? doc.title.match 'Removed' end end
ポイントは下記あたり。
- 動画の配信元ページのタイトルまたは特定の要素が、指定の文字列とマッチすれば、動画は削除されたと判断する
- 1回の存在確認で動画配信元ページへ数回アクセスしたくないので、取得した動画配信元ページの HTML をキャッシュしておく
どの行がどのくらいメモリを消費しているか?
どこがメモリを消費しているのだろう?動画配信元ページの HTML を保持する箇所がメモリを消費しているのかなと思ったんだけど、経験上、そういった当てずっぽうな予想に基づいて対応すると痛い目に遭うことが多いのできちんと計測してみた。
(A の箇所がメモリ食っているとか、遅いとかで対応して、後で、あー、A だけではなく B の箇所もだったわー、とかよくある)
どの行がどのくらいメモリを消費しているか ObjectSpace
を使って出力するようにした。このあたり、下記が詳しい。
# 存在確認くん(バッチ処理。コードはイメージです) require 'objspace' ObjectSpace.trace_object_allocations_start Video.find_each do |video| video.delete! unless video.existing? end ObjectSpace.trace_object_allocations_stop ObjectSpace.each_object do |object| file = ObjectSpace.allocation_sourcefile(object) line = ObjectSpace.allocation_sourceline(object) memsize = ObjectSpace.memsize_of(object) next if memsize < 10_000 next if file.nil? puts "#{memsize} #{file}:#{line}:#{object.class}" end ObjectSpace.trace_object_allocations_clear
結果
51199 /opt/rubies/2.1.5/lib/ruby/2.1.0/net/http/response.rb:334:String
とか 16385 /opt/rubies/2.1.5/lib/ruby/2.1.0/net/protocol.rb:153:String
とかが表示されたのでだいたい予想は当たっていそうだけど、他にも下記のようなものが表示された。
51199 /opt/rubies/2.1.5/lib/ruby/2.1.0/delegate.rb:343:String 10024 /opt/rubies/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-4.1.8/lib/active_support/dependencies.rb:443:RubyVM::InstructionSequence 12496 /opt/rubies/2.1.5/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/associations/association.rb:97:RubyVM::InstructionSequence 14064 /Users/inouetakuya/Projects/my_application/app/models/video.rb:1:RubyVM::InstructionSequence 14424 /opt/rubies/2.1.5/lib/ruby/gems/2.1.0/gems/activerecord-4.1.8/lib/active_record/base.rb:321:RubyVM::InstructionSequence
バッチ全体のメモリ使用量の推移(Before)
バッチ全体のメモリ使用量の推移を調べた。結果的にここの数字が下がるようにしなければならないので。
ObjectSpace.memsize_of_all
を使った- メモリの断片化の影響なども含めて OS からみたメモリ使用量もみたかったので ps の rss の値をみるようにした
# 存在確認くん(バッチ処理。コードはイメージです) require 'objspace' GC.start logger.info "[BEFORE] memsize_of_all: #{ObjectSpace.memsize_of_all / 1000} KB, rss: #{`ps -o rss= #{Process.pid}`.chomp} KB" i = 0 Video.find_each do |video| video.delete! unless video.existing? logger.info "[#{format('% 6d', i)}] memsize_of_all: #{ObjectSpace.memsize_of_all / 1000} KB, rss: #{`ps -o rss= #{Process.pid}`.chomp} KB" if (i % 50).zero? i += 1 end logger.info "[AFTER] memsize_of_all: #{ObjectSpace.memsize_of_all / 1000} KB, rss: #{`ps -o rss= #{Process.pid}`.chomp} KB"
結果
memsize_of_all の値はときおり減少しているが、ps の rss の値のほうは怒濤の勢いで増え続けている!!!
I, [2015-04-08T19:01:54.578874 #54321] INFO -- : [BEFORE] memsize_of_all: 62464 KB, rss: 114772 KB I, [2015-04-08T19:01:56.485631 #54321] INFO -- : [ 0] memsize_of_all: 72261 KB, rss: 123000 KB I, [2015-04-08T19:03:27.527892 #54321] INFO -- : [ 50] memsize_of_all: 84207 KB, rss: 152252 KB I, [2015-04-08T19:04:54.920088 #54321] INFO -- : [ 100] memsize_of_all: 70339 KB, rss: 231368 KB I, [2015-04-08T19:06:56.643725 #54321] INFO -- : [ 150] memsize_of_all: 79597 KB, rss: 321488 KB I, [2015-04-08T19:09:54.851015 #54321] INFO -- : [ 200] memsize_of_all: 83443 KB, rss: 415800 KB I, [2015-04-08T19:12:08.230988 #54321] INFO -- : [ 250] memsize_of_all: 86268 KB, rss: 491844 KB I, [2015-04-08T19:29:33.526412 #54321] INFO -- : [ 300] memsize_of_all: 87477 KB, rss: 553316 KB I, [2015-04-08T19:32:18.692708 #54321] INFO -- : [ 350] memsize_of_all: 87328 KB, rss: 640488 KB ...
この結果から推測したこと:
- memsize_of_all はときおり減少しているので、ガーベジコレクションは実行されている
- rss の値が増え続けているのは、Video についてのイテレータの中で、ガーベジコレクションされていないオブジェクトが増え続けている
コードのメモ化に関する注意
rss の値が増え続けているのは、Video についてのイテレータの中で、ガーベジコレクションされていないオブジェクトが増え続けている
と推測したので、その "ガーベジコレクションされていないオブジェクト" について明示的にクリアする必要があると考えた。で、このあたりについて下記の本が非常に参考になったので長めに引用する。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (5件) を見る
メモ化を使ったときに浮上する微妙な問題に注意を注ぐことにしよう。まず、メソッドの結果をどれくらいの間キャッシングすべきだろうか。current_user の例では、メモ化はウェブ要求ごとにリセットされる。しかし、ほかの使い方では、結果はかなり長い間残る。プログラムが終了するまで残る場合さえあるだろう。メソッドをどれくらいの間メモ化するか、キャッシング変数に nil をセットしてメモ化をリセットするメソッドを用意すべきかどうかを検討すべきだ。
コードのメモ化をリセット
キャッシング変数に nil をセットしてメモ化をリセットするメソッドを用意すべきかどうかを検討すべきだ。
というアドバイスに基づいてメモ化をリセットしてみた(コードは変更した箇所のみ抜粋)
Video.find_each do |video| video.delete! unless video.existing? video.clear_cache end
class Video < ActiveRecord::Base def doc # 動画配信元ページの HTML をキャッシュ @doc ||= Nokogiri::HTML(open(source_url)) end def clear_cache @doc = nil end end
バッチ全体のメモリ使用量の推移(After)
コードのメモ化をリセットするようにして再度メモリ使用量の推移を調べてみた。ps の rss の値が増え続けていない!やった!!!
I, [2015-04-08T19:38:08.907721 #54519] INFO -- : [BEFORE] memsize_of_all: 62462 KB, rss: 113076 KB I, [2015-04-08T19:38:10.339837 #54519] INFO -- : [ 0] memsize_of_all: 72307 KB, rss: 121076 KB I, [2015-04-08T19:39:29.367451 #54519] INFO -- : [ 50] memsize_of_all: 80730 KB, rss: 141600 KB I, [2015-04-08T19:41:02.980132 #54519] INFO -- : [ 100] memsize_of_all: 77585 KB, rss: 170548 KB I, [2015-04-08T19:43:09.312620 #54519] INFO -- : [ 150] memsize_of_all: 71928 KB, rss: 170996 KB I, [2015-04-08T19:46:12.389644 #54519] INFO -- : [ 200] memsize_of_all: 77397 KB, rss: 172648 KB I, [2015-04-08T19:48:44.221965 #54519] INFO -- : [ 250] memsize_of_all: 76182 KB, rss: 172820 KB I, [2015-04-08T19:50:38.114557 #54519] INFO -- : [ 300] memsize_of_all: 70948 KB, rss: 172820 KB I, [2015-04-08T19:53:23.965943 #54519] INFO -- : [ 350] memsize_of_all: 86286 KB, rss: 172820 KB ...
スパイクが消えて平和が訪れた munin の様子。
というわけで省メモリ化に成功し、存在確認くんが無事に働いてくれるようになったのでした。いぇい!
参考
今回の対応を終えても、Ruby のメモリ管理についてまだまだよく分かっていないところが多くて、下記の本も読んでみたけど、なかなか理解が難しい...
Rubyのしくみ -Ruby Under a Microscope-
- 作者: Pat Shaughnessy,島田浩二,角谷信太郎
- 出版社/メーカー: オーム社
- 発売日: 2014/11/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る