scramble cadenza

技術ネタのガラクタ置き場

unicorn + capistrano 構成で、古いリリースの実行パスを参照し続けてしまう問題

イントロ

今更だけどハマったので書いておく。

I, [2017-11-15T17:45:38.423986 #7537]  INFO -- : executing ["/var/www/app/releases/20171115083608/vendor/bundle/ruby/2.4.0/bin/unicorn", "-c", "/var/www/ser
val/current/config/unicorn.rb", "-E", "production", "-D", {15=>#<Kgio::UNIXServer:fd 15>}] (in /var/www/app/releases/20171115084509)
I, [2017-11-15T17:45:38.424276 #7537]  INFO -- : [before_exec] path: /var/www/app/current/config
/var/www/app/shared/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/lib/unicorn/http_server.rb:457:in `exec': No such file or directory - /var/www/app/releas
es/20171115083608/vendor/bundle/ruby/2.4.0/bin/unicorn (Errno::ENOENT)
        from /var/www/app/shared/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/lib/unicorn/http_server.rb:457:in `block in reexec'
        from /var/www/app/shared/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/lib/unicorn/http_server.rb:441:in `fork'
        from /var/www/app/shared/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/lib/unicorn/http_server.rb:441:in `reexec'
        from /var/www/app/shared/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/lib/unicorn/http_server.rb:306:in `join'
        from /var/www/app/current/vendor/bundle/ruby/2.4.0/gems/unicorn-5.3.1/bin/unicorn:126:in `<top (required)>'
        from /var/www/app/releases/20171115083608/vendor/bundle/ruby/2.4.0/bin/unicorn:23:in `load'
        from /var/www/app/releases/20171115083608/vendor/bundle/ruby/2.4.0/bin/unicorn:23:in `<main>'
E, [2017-11-15T17:45:38.500772 #5794] ERROR -- : reaped #<Process::Status: pid 7537 exit 1> exec()-ed

unicorn + capistrano を使っている環境下で、unicornUSR2 シグナルを送って再起動すると、 新しく立ち上がった unicorn のプロセスは、自身の実行パスが古いままになっている。

これは unicorn の master プロセスが USR2 シグナルを受け取ると、古い unicorn プロセスが、新しい master プロセスを fork するため。(Signal handling)

ところがこのまま capistrano でデプロイしていくと、維持する世代数(keep_releases)を越えてしまい、起動時のディレクトリは破棄されてしまう。
けど、unicorn 自体は初回起動時のパスで再起動しようとするから、そんなファイルねーよと言われてしまう。

環境

rails + unicorn + capistrano というオーソドックスな構成。
capistrano3-unicornunicorn の restart を行っている。

  • ruby (2.4.1)
  • unicorn (5.3.1)
  • capistrano (3.10.0)
  • capistrano-bundler (1.3.0)
  • capistrano3-unicorn (0.2.1)

対応

  • bundle_binstubs で shared ディレクトリ以下に、unicorn の実行ファイルを配置する
    • unicorn の実行パスを固定するため
  • Unicorn::HttpServer::START_CTX[0] で、上記の固定した実行パスを指定する
    • capistrano3-unicorn では、普通に bundle exec unicorn コマンドを実行しているだけ
    • bundle exec unicorn で呼ばれた unicorn は、/var/www/app/releases/xxxxxx/vendor/bundle/ruby/2.4.0/bin/unicorn で動く
      • log/unicorn.stderr.log にログが吐かれている通り
    • この unicorn の起動パスが固定されれば良いので、binstubs されたパスを指定して unicorn を起動するように、修正する
# config/deploy.rb
set :bundle_binstubs, -> { shared_path.join('bin') }
# config/unicorn.rb
app_path = '/var/www/app'
Unicorn::HttpServer::START_CTX[0] = File.join(app_path, 'shared/bin/unicorn')

おまけ

Unicorn::HttpServer::START_CTX ってなんだよ、というと unicorn の中で、プロセスを fork する時のコマンド実行に使われている定数っぽい。

https://github.com/defunkt/unicorn/blob/v5.3.1/lib/unicorn/http_server.rb#L441-L458

  # reexecutes the START_CTX with a new binary
  def reexec
    ...
    @reexec_pid = fork do
      listener_fds = listener_sockets
      ENV['UNICORN_FD'] = listener_fds.keys.join(',')
      Dir.chdir(START_CTX[:cwd])
      cmd = [ START_CTX[0] ].concat(START_CTX[:argv])

      # avoid leaking FDs we don't know about, but let before_exec
      # unset FD_CLOEXEC, if anything else in the app eventually
      # relies on FD inheritence.
      close_sockets_on_exec(listener_fds)

      # exec(command, hash) works in at least 1.9.1+, but will only be
      # required in 1.9.4/2.0.0 at earliest.
      cmd << listener_fds
      logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
      before_exec.call(self)
      exec(*cmd)
    end
    proc_name 'master (old)'
  end

https://github.com/defunkt/unicorn/blob/v5.3.1/lib/unicorn/http_server.rb#L32-L49

  # :startdoc:
  # We populate this at startup so we can figure out how to reexecute
  # and upgrade the currently running instance of Unicorn
  # This Hash is considered a stable interface and changing its contents
  # will allow you to switch between different installations of Unicorn
  # or even different installations of the same applications without
  # downtime.  Keys of this constant Hash are described as follows:
  #
  # * 0 - the path to the unicorn executable
  # * :argv - a deep copy of the ARGV array the executable originally saw
  # * :cwd - the working directory of the application, this is where
  # you originally started Unicorn.
  #
  # To change your unicorn executable to a different path without downtime,
  # you can set the following in your Unicorn config file, HUP and then
  # continue with the traditional USR2 + QUIT upgrade steps:
  #
  #   Unicorn::HttpServer::START_CTX[0] = "/home/bofh/2.3.0/bin/unicorn"

今回は実行パスが毎回変わるパティーンに相当するのかな。
この変更をダウンタイム無しで行うためには、HUP シグナルを送って、reload させてから、USR2 + QUIT シグナルを送れよ、と書かれている。 (USR2 だけで古いプロセスが死ぬように設定している場合は、USR2 だけで良さそうに思える)

結果

# /var/www/app/shared/log/unicorn.stderr.log
I, [2017-11-16T18:17:40.000683 #10785]  INFO -- : unlinking existing socket=/var/www/app/current/tmp/sockets/unicorn.sock
I, [2017-11-16T18:17:40.001511 #10785]  INFO -- : listening on addr=/var/www/app/current/tmp/sockets/unicorn.sock fd=15
I, [2017-11-16T18:17:40.005369 #10835]  INFO -- : worker=0 ready
I, [2017-11-16T18:17:40.006112 #10785]  INFO -- : master process ready
I, [2017-11-16T18:17:40.010895 #10838]  INFO -- : worker=1 ready
I, [2017-11-16T18:17:40.011886 #10840]  INFO -- : worker=2 ready
I, [2017-11-16T18:26:31.321077 #12323]  INFO -- : executing ["/var/www/app/shared/bin/unicorn", "-c", "/var/www/app/current/config/unicorn.rb", "-E", "st
aging", "-D", {15=>#<Kgio::UNIXServer:/var/www/app/current/tmp/sockets/unicorn.sock>}] (in /var/www/app/releases/20171116092542)
I, [2017-11-16T18:26:31.646072 #12323]  INFO -- : inherited addr=/var/www/app/current/tmp/sockets/unicorn.sock fd=15
I, [2017-11-16T18:26:31.646396 #12323]  INFO -- : Refreshing Gem list

これで何度 deploy しても成功するようになりました。
しかしこんなやり方でいいのだろうか?

一先ずこれで capistrano を使っていても、USR2 シグナルで restart できるようになった。