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 を使っている環境下で、unicorn を USR2
シグナルを送って再起動すると、 新しく立ち上がった unicorn のプロセスは、自身の実行パスが古いままになっている。
これは unicorn の master プロセスが USR2
シグナルを受け取ると、古い unicorn プロセスが、新しい master プロセスを fork するため。(Signal handling)
ところがこのまま capistrano でデプロイしていくと、維持する世代数(keep_releases
)を越えてしまい、起動時のディレクトリは破棄されてしまう。
けど、unicorn 自体は初回起動時のパスで再起動しようとするから、そんなファイルねーよと言われてしまう。
環境
rails
+ unicorn
+ capistrano
というオーソドックスな構成。
capistrano3-unicorn
で unicorn の 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]
で、上記の固定した実行パスを指定する
# 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 できるようになった。