スレッドではインスタンス変数を排他的に使わないとメモリを喰いまくるorz(←Rubyで使用メモリを減らすには(特にNet::HTTPライブラリを使う場合))

ガーベージコレクション(以下GC)すれば不要なオブジェクトが整理されてメモリが減る、と思い込んでいた[twitter:@riocampos]です。
環境はRuby 1.9.3です。

2014/8/19追記

google-picasa gemを扱うときにログインを終えたインスタンスインスタンス変数に入れ、スレッドでインスタンス変数を呼んでいたのですが、どうやらインスタンス変数を呼び出すときに排他処理をしていなかったためにメモリリーク(?)が生じていたようです。
mutexを使ってインスタンス変数の呼び出しを排他処理したところ、メモリ使用量が抑えられたようです。
やはりスレッドとインスタンス変数とは相性が悪いようです…^^;;
(追記ここまで)

いまやってること

あるサイトで定期的に更新される画像をRmagickで少し手を加えてPicasaに画像をアップする、という作業をherokuで動かしています。すると徐々にswapサイズが大きくなっていき、appの動きが徐々に悪くなる、そんな症状に悩んでおります。Picasaへのアップにはgoogle-picasa gemというのを使っています。
README - picasaonrails - Ruby wrapper for Picasa Web Album API - Google Project Hosting

メモリ対策その1

当初は、画像アップ毎にgoogle-picasaオブジェクトを作り、それをそのまま放置しておりました。ただ、google-picasaオブジェクトは画像オブジェクトを引数に取ってアップロードするので、同様のオブジェクトを毎回作る必要は無いな、と気付きました。のでインスタンス変数にgoogle-picasaオブジェクトを入れて、そのインスタンス変数で画像アップを行っています。これで使用メモリが若干減りました。

メモリ対策その2

RMagickで作った画像ファイルをTempfileへ保存した後にそのまま放置していたのですが、画像ファイルを保持している変数にnilを代入して破棄するようにしました。
(しかし、いま気付きましたが、nil入れた場合には変数の指し示す先がnilになるだけで、単に放置しているのと変わらないような…1×1の画像でも代入するか)

メモリ対策その3

定期的な画像アップロード作業を終えたらGC.startさせました。

GC.startしたらswapが増えた

GCしてるためか、メモリ増加傾向が抑えられました。しかし、以前は一定時間後にメモリとswapが安定したのですが、GC.startを入れてからは、swapがゆっくりゆっくりですが増加一方に…。そのせいでPicasaへのアップロードが出来なくなり、そして同一スレッドのほかの部分へも影響orz

調査

ほかのアプローチが無いか確認してみました。

すべての生存しているオブジェクトが消費しているメモリ使用量をバイト単位 で返します。

これをまずやってメモリの状況を確認すべきなのでしょうけど、いま後回しにしています。
正確な値が返ってきていると思えないので、あまり良くないかも。

補足で、ヒープの断片化の影響なども含めてOSからみた最終的なプロセスの物理メモリ使用量は

`ps -o rss= -p #{Process.pid}`.to_i

で取得できます。(osx, linux
Rubyプログラム内で自身のメモリ使用率を計測できる方法はありますか? - QA@IT

リストによるプロセス選択

-p pidlist
PID で選択する。
プロセス ID 番号が pidlist にあるプロセスを選択する。 p, --pid と等しい。

出力フォーマットの制御

-o format
ユーザー定義フォーマット。
format は空白区切りまたはコンマ区切りリストの形式の 1 つの引き数である。 これにより各出力カラムを指定する方法を提供している。 「標準フォーマット指定子」のセクションで説明されている キーワードを認識する。 ヘッダは望みのものに変更できる (ps -o pid,ruser=RealUser -o comm=Command)。 全てのカラムヘッダが空の場合 (ps -o pid= -o comm=)、 ヘッダ行は出力されない。

標準フォーマット指定子

rss:常駐セットの大きさ。 タスクが使用しているスワップされていない物理メモリ (kB 単位)。 (別名 rssize, rsz)。
Man page of PS

"-o rss="の"="はヘッダ行を出力しないためのもの、んでサイズはKBで返ってくるみたいですね。
これも使ってみよう。

大量データ処理時におけるRubyメモリ使用量削減対策

大量データ処理時におけるRubyメモリ使用量削減対策(pdf)
Ustream.tv: ユーザー rubyw_conf09_A: RWC2012_1108_A-5, Recorded on 2012/11/08. カンファレンス
内容を要約すると

  • StringオブジェクトではなくTempfileオブジェクトを使う
  • 作成したStringオブジェクトはクリアする(サイズ0のStringオブジェクトにしてしまう)
  • インスタンスが保持するインスタンス変数を減らす

上記でのまとめ(よく分からない内容もあるので引用)

  • forkするアプリでは、親プロセスを軽くする
  • イテレータ処理する場合は、ブロック終了時に イテレータ変数をクリアしてあげる
  • クラス内部で保持する変数は少なくする
  • 破壊的メソッドを使用する(gsub!など)
  • 可能な限り Ruby1.9 を使用する

2012年であってもまだ1.8系が多かったことが分かります。

現状での最善手

使っているスクリプトでは、google-picasa gemのGoogle::Picasa::Picasa#post_photoメソッドを何度も呼ぶのですが、ここでのnet/httpライブラリの使い方が気になりました。

uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)

headers = {"Content-Type" => "image/jpeg",
  "Authorization" => "GoogleLogin auth=#{self.picasa_session.auth_key}",
  "Slug" => title, "Content-Transfer-Encoding" => "binary"}

response = http.post(uri.path, image_data, headers)
data = response.body

picasa.rb - picasaonrails - Ruby wrapper for Picasa Web Album API - Google Project Hosting

httpというオブジェクトを作りっぱなしなのではないか、という印象をうけました。
openしたらcloseしないと良くないのでは、と思ったので、ブロックを受けるNet::HTTP#startメソッドを使って書き替え(モンキーパッチ)しました。

uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)

headers = {
  "Content-Type" => "image/jpeg",
  "Authorization" => "GoogleLogin auth=#{self.picasa_session.auth_key}",
  "Slug" => title, 
  "Content-Transfer-Encoding" => "binary"
}

data = ""
http.start do |http_session|
  response = http_session.post(uri.path, image_data, headers)
  data = response.body
end

結果として、メモリ使用量の増加が抑えられたようです(まだ確認中)。

リファレンス

singleton method Net::HTTP.new

new(address, port = 80, proxy_addr = nil, proxy_port = nil, proxy_user=nil, proxy_pass=nil) -> Net::HTTP
新しい Net::HTTP オブジェクトを生成します。
このメソッドは TCP コネクションを張りません。
singleton method Net::HTTP.new

singleton method Net::HTTP.start

start(address, port = 80, proxy_addr = nil, proxy_port = nil, proxy_user=nil, proxy_pass=nil) -> Net::HTTP
start(address, port = 80, proxy_addr = nil, proxy_port = nil, proxy_user=nil, proxy_pass=nil) {|http| .... } -> object
新しい Net::HTTP オブジェクトを生成し、 TCP コネクション、 HTTP セッションを開始します。
ブロックを与えた場合には生成したオブジェクトをそのブロックに 渡し、ブロックが終わったときに接続を閉じます。このときは ブロックの値を返り値とします。
ブロックを与えなかった場合には生成したオブジェクトを渡します。 利用後にはこのオブジェクトを Net::HTTP#finish してください。
このメソッドは以下と同じです。

Net::HTTP.new(address, port, proxy_addr, proxy_port, proxy_user, proxy_pass).start(&block)

singleton method Net::HTTP.start

instance method Net::HTTP#start

start -> self
start {|http| .... } -> object
TCP コネクションを張り、HTTP セッションを開始します。 すでにセッションが開始していたら例外 IOError を発生します。
ブロックを与えた場合には自分自身をそのブロックに 渡し、ブロックが終わったときに接続を閉じます。このときは ブロックの値を返り値とします。
ブロックを与えなかった場合には自分自身を返します。 利用後にはこのオブジェクトを Net::HTTP#finish してください。
instance method Net::HTTP#start