背景
ジョブキューをさばくバッチ処理を開発していると、子プロセスを使って並列処理を実装したくなりました。
最大で10プロセスまで子プロセスを生成し10並列で処理できるように実装したは良いものの、
子プロセスがゾンビプロセス化してジョブキューをうまくさばくことができませんでした。
これまでなんとなくRubyでプロセスを扱ってきたので、これを機にいろいろ試してみました。
検証環境
- OS
- Debian GNU/Linux 9 (stretch)
- Dockerコンテナで検証しました。
- Ruby
- 2.6.3
検証
以下、やったことを書いていきます。
親プロセス
手始めに親プロセスの確認。
root@05b9ab6e9fa3:~# irb
irb(main):001:0>
irb(main):002:0> Process.pid
=> 534
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 1.2 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 3.7 0.8 90008 16596 pts/0 S+ 12:23 0:00 \_ irb
:
親のpidが「534」であることが確認できました。
子プロセス
次に子プロセスの確認。
Kernel.#fork
を使って、子プロセスを生成します。
irb(main):003:0> fork { sleep 60 }
=> 543
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.4 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.9 0.8 90148 16596 pts/0 S+ 12:23 0:00 \_ irb
root 543 0.0 0.5 90148 11396 pts/0 S+ 12:25 0:00 \_ irb
:
子プロセスが生成できました。
ゾンビ化する子プロセス
しばらく時間をおいてプロセスを確認すると、子プロセスが になり、STATが Z+ になっていました。
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.2 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.5 0.8 90148 16596 pts/0 S+ 12:23 0:00 \_ irb
root 543 0.0 0.0 0 0 pts/0 Z+ 12:25 0:00 \_ [irb] <defunct>
:
これは親プロセスが子プロセスを管理していないため、子プロセスがカーネルのキューに残り続けてしまっている状態のようです。
子プロセスをゾンビ状態から開放させてあげるためには、親プロセスにて Process.wait
を読んであげると良いらしいのでやってみました。
irb(main):004:0> Process.wait
=> 543
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.0 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.2 0.8 90148 16664 pts/0 S+ 12:23 0:00 \_ irb
:
子プロセスが無事に開放されましたね。
子プロセスをゾンビにさせない
子プロセスが終了するより先に、Process.#detach
を呼んでみます。
irb(main):007:0> fork { sleep 60 }
=> 555
irb(main):008:0> Process.detach(555)
=> #<Process::Waiter:0x000055ca51dad5b0 run>
irb(main):009:0>
子プロセスの処理が終了する前にプロセスの状況を確認してみると、先ほどとは変わらないですね。
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.0 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.1 0.8 157740 16724 pts/0 Sl+ 12:23 0:00 \_ irb
root 555 0.0 0.5 90148 11420 pts/0 S+ 12:32 0:00 \_ irb
:
子プロセスの処理が終了した後にプロセスの状況を確認してみると...
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.0 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.1 0.8 157740 16732 pts/0 S+ 12:23 0:00 \_ irb
:
おー。ゾンビにならず無事に(?)子プロセスが終了しています。
ゾンビプロセスにさせたくないときは、子プロセスを生成した後すぐにデタッチしてあげると良いみたいですね。
子プロセスが終了した後に、Process.#detach
を呼ぶとどうなるか
まずは子プロセスの生成。
irb(main):018:0> fork { sleep 60 }
=> 562
子プロセスがゾンビ化するのを待ちます。
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.0 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.0 0.8 157888 16836 pts/0 S+ 12:23 0:00 \_ irb
root 562 0.0 0.0 0 0 pts/0 Z+ 12:38 0:00 \_ [irb] <defunct>
:
親プロセスで Process.#detach
を呼び出します。
irb(main):020:0> Process.detach(562)
=> #<Process::Waiter:0x000055ca51dc8ec8 run>
irb(main):021:0>
プロセスの状態を確認すると
root@05b9ab6e9fa3:~# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 0.0 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 0.0 0.8 157888 16860 pts/0 S+ 12:23 0:00 \_ irb
:
子プロセスが消えてますね。
まあ、これはそうなるような気がしてました。
スレッドの状態を確認する
irb(main):023:0> fork { sleep 60 }
=> 577
root@05b9ab6e9fa3:~# ps aux -L
USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 529 0.0 1 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 534 0.0 1 0.8 157888 16860 pts/0 S+ 12:23 0:00 irb
root 577 577 0.0 1 0.5 157888 11636 pts/0 S+ 12:43 0:00 irb
子プロセスが終了する前に、Process.#detach
を呼びます。
irb(main):024:0> Process.detach(577)
=> #<Process::Waiter:0x000055ca51dd4b60 run>
スレッドを確認すると、
root@05b9ab6e9fa3:~# ps aux -L
USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 529 0.0 1 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 534 0.0 2 0.8 157888 16904 pts/0 Sl+ 12:23 0:00 irb
root 534 580 0.0 2 0.8 157888 16904 pts/0 Sl+ 12:43 0:00 irb
root 577 577 0.0 1 0.5 157888 11636 pts/0 S+ 12:43 0:00 irb
:
PIDが534のプロセスが2プロセスになってます。
ps
コマンドの -L
オプションで スレッドも表示するようにしているためです。
子プロセスが終了すると、
root@05b9ab6e9fa3:~# ps aux -L
USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
:
root 529 529 0.0 1 0.1 18188 3216 pts/0 Ss 12:23 0:00 /bin/bash
root 534 534 0.0 1 0.8 157888 16904 pts/0 S+ 12:23 0:00 irb
:
子プロセスが消え、親プロセスも1スレッドに戻っていることが確認できました。
この状態から、親プロセスで Process.#wait
を呼ぶと
irb(main):026:0> Process.wait
Traceback (most recent call last):
5: from /usr/local/bin/irb:23:in `<main>'
4: from /usr/local/bin/irb:23:in `load'
3: from /usr/local/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
2:
from (irb):26
1: from (irb):26:in `wait'
Errno::ECHILD (No child processes)
Errno::ECHILD
エラーが発生してますね。
子プロセスがいなくなっているのだから、当然ですね。
同じことを、Rubyのプログラムからも確認してみました。
irb(main):043:0> fork { sleep 60 }
=> 585
irb(main):044:0>
irb(main):045:0> Process.detach(585)
=> #<Process::Waiter:0x000055ca51b51230 run>
irb(main):046:0>
irb(main):047:0> Thread.list
=> [#<Thread:0x000055ca5174f2b8 run>, #<Process::Waiter:0x000055ca51b51230 sleep>]
irb(main):048:0>
irb(main):049:0>
# ここで子プロセスが終了。
irb(main):050:0> Thread.list
=> [#<Thread:0x000055ca5174f2b8 run>]
irb(main):051:0>
スレッドが2になり、子プロセスが終了するとスレッド数も1に戻っていることがわかります。
Process.#detach
と Process.#wait
を同時に呼ぶとどうなるか
irb(main):034:0> fork { sleep 60 }
=> 583
irb(main):035:0> Process.detach(583)
=> #<Process::Waiter:0x000055ca51dff518 run>
irb(main):036:0>
irb(main):037:0> Process.wait
Traceback (most recent call last):
5: from /usr/local/bin/irb:23:in `<main>'
4: from /usr/local/bin/irb:23:in `load'
3: from /usr/local/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
2: from (irb):37
1: from (irb):37:in `wait'
Errno::ECHILD (No child processes)
Process.wait
を読んだ直後は、待ち状態に入りました。
子プロセスの処理が終了したタイミングで Errno::ECHILD
エラーが発生しました。
Process.#detach
と Process.#wait
のどちらか一方を呼び出すようにするのが良さそうです。
結論
forkしたあと、すぐに Process.#detach
を呼び出し、スレッド数を監視することで子プロセス数の制御するのが良さそうな気がしました。
このようなユースケースではどうやってプロセス管理するのがいいんでしょうね。
他の方法でやってる方がいらっしゃったら教えてください😀