『Working with Unix Processes』の Appendix: How Resque Manages Processes を読みました
はじめに
Ending の続きです。
まとめ
なぜ Resque が job を実行するのに fork しているのかというと、ワーカーのプロセスが使うメモリ量の肥大化を防ぐためだ。job を実行する度に fork するのでメモリは使うデメリットはあるが、実行後のクリーンアップが保証されるメリットがある。肥大化の背景には Ruby において GCレベルでブロック単位の開放は滅多にされないことがある。
ジョブを実行しているコードのリーディング大会
本文中のコードは実際のコードを簡略化して抜き出したものです。実際のコードを読んでみたら複雑なことをやっていました。勉強会メンバーで本で得た知識をもとに読み解いてみました。
# fork をラップすることで、fork をサポートしていない # プラットフォームでも動作するようにしている。 # # *注意* # fork をサポートしてないプラットフォームではメモリ管理のメリットは失われる def fork(job,&block) # 二度目以降はムダな処理をしない return if @cant_fork run_hook :before_fork, job if will_fork? begin if Kernel.respond_to?(:fork) # MRI など fork をサポートしている場合 Kernel.fork &block if will_fork? else # IronRuby など fork をサポートしていない場合 raise NotImplementedError end rescue NotImplementedError # fork をサポートしていない場合はフラグを立て、 # block は実行せず終了 @cant_fork = true nil end end ... # job を実行する度に実行されるメソッド def work(interval = 5.0, &block) ... loop do ... if job = reserve(interval) ... # だいぶトリッキーなので注意 ※A if @child = fork(job) do # fork をサポートしているプラットフォームの子プロセスの場合 unregister_signal_handlers procline "Processing #{job.queue} since #{Time.now.to_i}" reconnect perform(job, &block) end # fork をサポートしているプラットフォームの親プロセスの場合 srand # Reseeding. see http://redmine.ruby-lang.org/issues/4338 procline "Forked #{@child} at #{Time.now.to_i}" begin Process.waitpid(@child) rescue SystemCallError nil end job.fail(DirtyExit.new($?.to_s)) if $?.signaled? else # fork がサポートされていないプラットフォームの場合 procline "Processing #{job.queue} since #{Time.now.to_i}" # ここで reconnect が走るケースはどういう時だろう? # オーバーライドした fork が nil を返すときは @cant_fork は # true になっているはず reconnect unless @cant_fork perform(job, &block) end done_working @child = nil else ... end end unregister_worker rescue Exception => exception unregister_worker(exception) end ... # Redis へのコネクションが親プロセスとシェアされてしまうことを避ける def reconnect ... redis.client.reconnect ... end
コードリーディングをしていて驚いたことを列挙します。
- そもそも fork がラップされていること
- IronRuby など fork を実装していないプラットフォームも考慮されていること
この本を読まなかったらここまで読み解くことはできませんでした。コードリーディング力の高まりを感じます^^ 読んでてコメントがあまりなくて苦労しましたw
なお if @child = fork(job) あたりのコードの解説は本文でとても詳しく書いてあります。
forkブロックと if文が組み合わさったときの挙動
※A の挙動を読み解くのにかなり苦労しました。わかりやすくするために次のコードを書いてみました。ファイル名は fork-if.rb です。
puts "#{Process.pid}: before if" if child = fork do puts "#{Process.pid}: in fork block" end puts "#{Process.pid}: in if [true]" else puts "#{Process.pid}: in if [false]" end puts "#{Process.pid}: after if"
実行します。
$ ruby fork-if.rb 2069: before if # parent 2069: in if [true] # parent 2069: after if # parent 2070: in fork block # child
else 文は実行されていないことが分かります。fork にブロックを渡すかどうかで戻り値が 2つ返ってくるかどうかの挙動が変わるようです。シンプルな fork のケースはこちらをご覧ください。
Resque では fork をオーバーライドすることで forkブロック・if文・else文の実行の 3つの分岐を実現しているのですね。すごいマジック!
気になる
srand ではどんなバグを回避しているのか?
そのうち調べる。
procline とは
procline という単語はどういう意味なのだろう? たぶん process line のことかなあ。
フレーズ
- bloat: 膨らむ
- go awry: 予想が外れる
- clean slate: 何かの比喩表現? 「まっさらな状態」というニュアンスかな
- spike: 一気に増える(釘が打ち付けられるイメージ。「【名】釘」「【動】釘で打ち付ける」という意味もある。お試しプログラムのことをスパイクと呼ぶけど、あれはどういうイメージから来ているのかなあと思った)
- use up: 使い切る