Skip to content

Instantly share code, notes, and snippets.

「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 プロセスも終了させる。

Server Starter と組み合わせた場合

--status-fileENV['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 個のプロセスが同時に立ち上がることになる。

@sonots
Copy link
Author

sonots commented Feb 17, 2015

ToDo: TTOU は graceful shutdown なのか? => そのようでした

@sonots
Copy link
Author

sonots commented Feb 17, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment