こちらの記事のように unicorn を Server::Starter 経由で起動できるようにして、daemontools の管理下におこうとしていてハマってたのでメモ。

おさらい

おさらいしておくと、config/unicorn.conf に次のようなコードを書く事で unicorn を Server::Starter 経由で起動できる。Server::Starter の環境変数から open した socket ファイルディスクリプタを受け取れて、unicorn のほうで使えるようになる。

if ENV.key?('SERVER_STARTER_PORT')
  fds = ENV['SERVER_STARTER_PORT'].split(';').map { |x|
    path_or_port, fd = x.split('=', 2)
    fd
  }
  ENV['UNICORN_FD'] = fds.join(',')
  ENV.delete('SERVER_STARTER_PORT')
else
  listen ENV['PORT'] || '10080'
end

unicorn はそれ自体でホットリスタートの機能 (SIGUSR2 => SIGQUIT のコンボ) を持っているけど、master プロセスがみなしごになって daemontools の管理下から外れてしまうので使わない。server starter の pid に SIGHUP を送って server starter の機能で unicorn プロセスを新しく起動してもらってホットリスタートする形になる。daemontools に svc -h する形になる。

新しい unicorn master が立ち上がったら Server Starter は --signal-on-hup オプションで指定したシグナルを古い unicorn master に勝手に送って殺してくれるので、unicorn のホットリスタート機能より使いやすい(unicorn の場合、手動で QUIT を送り直さなければならない)。--signal-on-hup=QUIT にしておくと、QUIT シグナルを送ってくれて、unicorn は QUIT では graceful shutdown (リクエストを捌いてから終了) してくれるので良い。

ということで、そうしようと思ったらハマったのでメモ。

事実1. bundle exec 経由で unicorn を起動するとファイルディスクリプタが子プロセス(unicornプロセス)に渡らない

$ start_server --port=10080 --signal-on-hup=QUIT -- \
    bundle exec unicorn -c config/unicorn.conf config.ru
lib/unicorn/http_server.rb:784:in `for_fd': not a socket file descriptor (ArgumentError)

該当行のコードを開くと Socket.for_fd で死んでいる。4 番の fd が socket file descriptor ではない、と言われて死んでいる。なので fd を確認してみる。

親である Server Starter

$ ls -l /proc/24663/fd
lrwx------ 1 sonots sonots 64  9月 17 23:44 0 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 17 23:44 1 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 17 23:44 2 -> /dev/pts/2
lr-x------ 1 sonots sonots 64  9月 17 23:44 3 -> start_server
lrwx------ 1 sonots sonots 64  9月 17 23:44 4 -> socket:[1725527471]

子である unicorn のプロセス

$ ls -l /proc/24664/fd
lrwx------ 1 sonots sonots 64  9月 17 23:45 0 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 17 23:45 1 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 17 23:44 2 -> /dev/pts/2
lr-x------ 1 sonots sonots 64  9月 17 23:45 3 -> pipe:[1725527518]
l-wx------ 1 sonots sonots 64  9月 17 23:45 4 -> pipe:[1725527518]
lr-x------ 1 sonots sonots 64  9月 17 23:45 5 -> pipe:[1725527519]
l-wx------ 1 sonots sonots 64  9月 17 23:45 6 -> pipe:[1725527519]

確かに unicorn の4番が socket fd じゃない!!

普通に fork していれば fd は子プロセスに引き継がれるはずで何かがおかしいので、 bundler のコードを読む。

lib/bundler/cli/exec.rb

 14       begin
 15         if RUBY_VERSION >= "2.0"
 16           @args << { :close_others => !options.keep_file_descriptors? }
 17         elsif options.keep_file_descriptors?
 18           Bundler.ui.warn "Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec."
 19         end
 20
 21         # Run
 22         Kernel.exec(*args)

なにやら keep_file_descriptors という怪しい名前が見つかったので調べてみると、どうやら bundle exec でファイルディスクリプタを引き継ぎたい場合は --keep-file-descriptors オプションを指定する必要があるらしい。ref. http://bundler.io/v1.7/bundle_exec.html

そうしないと、Kernel.exec に close_others オプションが渡って、ファイルディスクリプタが閉じられる。 ref. http://docs.ruby-lang.org/ja/2.1.0/class/Kernel.html

解決策: bundle exec --keep-file-descriptors を使う

start_server --port=10080 --signal-on-hup=QUIT -- \
  bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru
$ ls -l /proc/26090/fd
lrwx------ 1 sonots sonots 64  9月 18 00:12 0 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 18 00:12 1 -> /dev/pts/2
lrwx------ 1 sonots sonots 64  9月 18 00:11 2 -> /dev/pts/2
lr-x------ 1 sonots sonots 64  9月 18 00:12 3 -> pipe:[1725540588]
lrwx------ 1 sonots sonots 64  9月 18 00:12 4 -> socket:[1725540548]
l-wx------ 1 sonots sonots 64  9月 18 00:12 5 -> pipe:[1725540588]
lr-x------ 1 sonots sonots 64  9月 18 00:12 6 -> pipe:[1725540589]
l-wx------ 1 sonots sonots 64  9月 18 00:12 7 -> pipe:[1725540589

4番が socket fd になって起動できた。

補足:bunlde exec start_server ... は良くない
他にも bundle exec start_server ... と起動することも出来そうだが、これは実は良くない。

bundle exec start_server --port=10080 --signal-on-hup=QUIT -- \
  unicorn -c config/unicorn.conf config.ru

bundle exec がやっていることは実質環境変数を設定しているだけなので、 このように実行しても unicorn にもその環境変数は渡ってアプリを起動することができる。そして、start_server, unicorn 間は bundle exec を挟んでいないので fd が閉じられることもない。

のだが、このように起動すると bundler を新しくすることができない。start_server に SIGHUP を送っても古い bundler のまま (RUBYLIB 環境変数が古いまま)となってしまい問題となる。

事実2. foreman 経由で unicorn を起動するとファイルディスクリプタが子プロセス(unicornプロセス)に渡らない

理由は先ほどと同様で、foreman が悪さをしているのでファイルディスクリプタが渡らない.

lib/foreman/process.rb

 68       wrapped_command = "#{Foreman.runner} -d '#{cwd}' -p -- #{command}"
 69       Process.spawn env, wrapped_command, :out => output, :err => output

ここで spawn に :close_others => false を渡せればなんとかやりようがあるのだが、foreman はサポートしていない。

パッチをあてれば対応できるが、確かに単純にここでオプションを渡すと全ての Procfile 内のプロセスに影響が出てしまうので悩ましい。微妙すぎて pull req を送れない。

解決策: foreman を使わないw

事実3. unicorn が立ち上がりきる前に QUIT シグナルを送ってしまう

start_server --port=10080 --signal-on-hup=QUIT -- \
  bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru

として起動し、start_server に HUP シグナルを送ると、新しい unicorn プロセスがたちあがり切っていないのに、スタートしたものとみなして QUIT を送ってしまう。

unicorn のログ。spawned の前に reaped が来てしまっている。 

received HUP (num_old_workers=0)
spawning a new worker (num_old_workers=0)
starting new worker 31688
new worker is now running, sending QUIT to old workers:31611
sleep 0 secs
killing old workers
I, [2014-09-25T15:27:00.799636 #31688]  INFO -- : inherited addr=0.0.0.0:10080 fd=4
I, [2014-09-25T15:27:00.799905 #31688]  INFO -- : Refreshing Gem list
I, [2014-09-25T15:27:00.832733 #31611]  INFO -- : reaped #<Process::Status: pid 31638 exit 0> worker=0
I, [2014-09-25T15:27:00.832901 #31611]  INFO -- : reaped #<Process::Status: pid 31641 exit 0> worker=1
I, [2014-09-25T15:27:00.833078 #31611]  INFO -- : master complete
old worker 31611 died, status:0
I, [2014-09-25T15:27:04.144681 #31762]  INFO -- : worker=0 spawned pid=31762
I, [2014-09-25T15:27:04.145343 #31762]  INFO -- : worker=0 ready
I, [2014-09-25T15:27:05.148449 #31765]  INFO -- : worker=1 spawned pid=31765
I, [2014-09-25T15:27:05.149028 #31765]  INFO -- : worker=1 ready
I, [2014-09-25T15:27:07.157861 #31688]  INFO -- : master process ready

解決策: --kill-old-delay オプションを使う

--kill-old-delay オプションを使うと、立ち上げはじめて指定秒数待ってから新しいプロセスにデータを dispatch してくれるようになる。待ってから古いプロセスに QUIT を送ってくれるようになる。うちでは 10 にしている。

start_server --port=10080 --signal-on-hup=QUIT --kill-old-delay=10 \
  bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru

事実4. デプロイしたのに古いディレクトリを追いかけてしまう

例えば capistrano でデプロイすると current シンボリックリンクの指す実体が新しい releases ディレクトリになるが、 次のように先に cd してしまうと、古いディレクトリに chdir してしまった後になるため、 HUP シグナルを送っても、カレントディレクトリが変わらず、古いディレクトリのコードを読み込んでアプリを起動してしまう。

cd $HOME/app/current
start_server --port=10080 --signal-on-hup=QUIT --kill-old-delay=10 \
  bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru

解決策: --dir オプションを使う

server starter には --dir オプションという、コマンドを再実行する前に chdir してくれるものがあるのでそれを使うと解決できる。

start_server --port=10080 --signal-on-hup=QUIT \
--kill-old-delay=10 --dir=$HOME/app/current -- \ bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru 


(追加) 事実5. 一時的にプロセス数が二倍になってしまう

See Server::Starter で Unicorn を起動する場合の Slow Restart 対応