「Server::Starterに対応するとはどういうことか」の補足 の続き。
Unicorn の新しいプロセス(master + worker) が全て新規に立ち上がるのを見届けてから古いプロセスを殺す方法だと、1時的にプロセス数が全体の2倍になり、最悪の場合メモリ使用量が平常時の2倍になりえる (CoW が効くのでよっぽどの状況でなければ2倍にはならないはずだ)
これを防ぐためには、新しい worker プロセスを1つ起動できたタイミングで古い worker プロセスを停止し、最大でも N 個のプロセスしか立ち上がらないようにする制御を行う。また、新しいプロセスも一気に立ち上げるのではなく徐々に立ち上げるとより安心だ。
Unicorn を単体で SIGUSR2 + SIGQUIT のコンボでホットスタートを行っている場合は、unicorn.conf.rb のサンプルに書いてあるやり方でスローリスタートできる。
cf. https://github.com/defunkt/unicorn/blob/master/examples/unicorn.conf.rb
pid "/path/to/pid/file/to/generate.pid"
before_fork do |server, worker|
# This allows a new master process to incrementally
# phase out the old master process with SIGTTOU to avoid a
# thundering herd (especially in the "preload_app false" case)
# when doing a transparent upgrade. The last worker spawned
# will then kill off the old master process with a SIGQUIT.
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
# Throttle the master from forking too quickly by sleeping. Due
# to the implementation of standard Unix signal handlers, this
# helps (but does not completely) prevent identical, repeated signals
# from being lost when the receiving process is busy.
sleep 1
end
Unicorn は SIGUSR2 を受け取ると新しい unicorn master プロセスを起動しつつ、古い master プロセスの pid を .oldbin ファイルに書き込む。
worker が1つ起動される度に、旧 unicorn master に TTOU シグナルを送る。Unicorn は TTOU シグナルを受け取ると worker の数を decrement し、graceful に落とす。
worker が全て立ち上がったら 旧 unicorn master に QUIT シグナルを送り、master プロセスも終了させる。
--status-file
と ENV['SERVER_STARTER_GENERATION']
を利用する。また、--signal-on-hup=CONT
と設定する。
start_server --status-file=/path/to/app/log/unicorn.stat \
--port=10080 --signal-on-hup=CONT --dir=/path/to/app -- \
bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru
status file に世代id と server starter が fork したプロセス (unicorn master) の pid が : 区切りで書き込まれている。
1:10658
server starter に HUP シグナルを送ると一時的に次のように新旧世代が存在するようになり、
1:26237
2:12104
旧世代が殺されると、新世代のみになる
2:12104
このファイルを unicorn.conf から読み込んで旧世代のプロセスに TTOU を送るようにして対応する。
status_file = '/path/to/app/log/unicorn.stat'
before_fork do |server, worker|
# Throttle the master from forking too quickly by sleeping. Due
# to the implementation of standard Unix signal handlers, this
# helps (but does not completely) prevent identical, repeated signals
# from being lost when the receiving process is busy.
sleep 1
end
after_fork do |server, worker|
# This allows a new master process to incrementally
# phase out the old master process with SIGTTOU to avoid a
# thundering herd (especially in the "preload_app false" case)
# when doing a transparent upgrade. The last worker spawned
# will then kill off the old master process with a SIGQUIT.
begin
pids = File.readlines(status_file).map {|_| _.chomp.split(':').map(&:to_i) }.to_h
old_gen = ENV['SERVER_STARTER_GENERATION'].to_i - 1
if old_pid = pids[old_gen]
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, old_pid)
end
rescue Errno::ENOENT, Errno::ESRCH => e
$stderr.puts "#{e.class} #{e.message}"
end
end
最後に unicorn.conf に 旧 unicorn master に QUIT シグナルを送ってもらうので、server starter では --signal-on-hup=CONT
として、無害な CONT シグナルを送ってもらう。--kill-old-delay=10
で時間を調節する必要もなくなった。
なお、before_fork
だと1時的にプロセス数が 1 減る(起動前に旧 worker を減らしてしまう)ので、after_fork
に移動している。
結果、最大で N+1 個のプロセスが同時に立ち上がることになる。
ToDo: TTOU は graceful shutdown なのか? => そのようでした