『Working with Unix Processes』の Appendix: How Unicorn Reaps Worker Processes を読みました
はじめに
How Resque Manages Processes の続きです。
主旨
Unicorn が親プロセスを終了する際に、どうやって親なし子プロセスができないように制御しているかを読み解く。
コードリーディング大会
勉強会メンバーでコードを読んでみました。読んだコードはこちら。コメントに理解や疑問などを書きました。
def stop(graceful = true) self.listeners = [] limit = Time.now + timeout # なるべくワーカーが残らないように(関連※A)、 # ワーカーが全て終了するかタイムアウトになるまで、 # ワーカーを終了させる処理を続ける。 until WORKERS.empty? || Time.now > limit kill_each_worker(graceful ? :QUIT : :TERM) sleep(0.1) # これはなんで必要なの? reap_all_workers end kill_each_worker(:KILL) end ... def reap_all_workers # 全てのワーカーが終了するまで終了処理を繰り返す begin # 任意の子プロセスをノンブロッキングで wait # *注意* 終了していない子プロセスが存在する場合は nil が返る wpid, status = Process.waitpid2(-1, Process::WNOHANG) # 子プロセスが無くなったら終了処理から抜ける。 # # reap_all_workers を呼ぶ前にワーカーである子プロセス全てに QUITシグナルは # 送られている。そのため、子プロセスは終了しているはず。 # だが、もし子プロセスが何らかの原因で終了していない場合は return される(関連※A) wpid or return if reexec_pid == wpid # ダウンタイム0 で再起動できる機能と関係のあるコード logger.error "reaped #{status.inspect} exec()-ed" self.reexec_pid = 0 self.reexec_pid = 0 self.pid = pid.chomp('.oldbin') if pid proc_name 'master' else # wait したワーカーを終了 worker = WORKERS.delete(wpid) and worker.close rescue nil # ログ出力 m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" status.success? ? logger.info(m) : logger.error(m) end rescue Errno::ECHILD # 子プロセスが存在しない場合=全てのワーカーが終了している場合 break end while true end ... # 実行に長時間かかっているワーカーを終了する def murder_lazy_workers ... end
本文では reap_all_workers の解説がさらに詳しく書かれています。ぜひ読んでみてください。
感想
Unicorn を使って Gogengo! というサービスを動かしているのですが、Unicorn の仕組みを学んだのは初めてでした。とても面白かったです^^ reap_all_workersメソッド の挙動を読んだときに「子プロセスが QUITシグナルを送っても終了しなかった場合は残ってしまうのではないか?」という疑問がありました(本文には stopメソッドの解説はありませんでした)。stopメソッドを読んでみるとタイムアウトするまで reap_all_workers をリトライしていたので、残るのを防止しているのか、とわかりました。
本文を読むことで理解が深まり、疑問が出てきて、オープンソースを読み進めるとさらに理解が深まる。このような良い循環ができるのが本書の魅力の一つだと思います。勉強になる!