Capistrano によるデプロイ時に Unicorn の再起動に失敗することがある問題への対処

概要

  • Unicorn を使って Railsアプリケーション を動かしている
  • アプリケーションのデプロイに Capistrano を使っている
  • Capistrano のデプロイ処理の最後で Unicorn の再起動を行っているが、たまに失敗する
  • その原因を調べ、対処した

環境

  • capistrano 2.5.19
  • unicorn 2.0.0
  • bundler 1.0.10

Unicorn の再起動に失敗する件

デプロイ後に Unicorn を再起動する際、そのデプロイで新たに Gemfile に追加された gem に含まれるファイルを require している箇所で LoadError が発生する。 Unicorn を停止して起動した場合はエラーは発生しない。 再起動の時だけ、しかもデプロイ時の再起動の時にだけエラーが発生する。

つまり、「新たな gem が追加されたアプリケーションのデプロイ時に Unicorn の再起動に失敗している」ということがわかった。

(ここまでたどり着くのに、デプロイ環境の構築等も含めて半日以上かかっている。。)

調査を進めると、 Unicorn の再起動時に、 Gemfile に新たに追加された gem を Bundler が読み込めていないことがわかった。 だから require している箇所で LoadError が発生する。 新しくなった Gemfile を Bundler がうまくロードできていないようだ。

Capistrano のデプロイ処理について

Capistrano は良くできたツールである。 デプロイ先のサーバーに以下のようなディレクトリ構造を作り、デプロイ等の処理を行う。

my_app
  ├── current -> /var/www/rails/my_app/releases/20110209064641
  ├── releases
  │     ├── 20110209064641
  │     ├── 20110209072816
  │     ├── 20110209074512
  │     ├── 20110209075449
  └── shared
        ├── bundle
        ├── cached-copy
        ├── log
        ├── pids
        └── system

current というのが実際に動作するアプリケーションのルートディレクトリで、実体はシンボリックリンクである。 デプロイの際、アプリケーションは releases ディレクトリ以下にタイムスタンプをディレクトリ名として配置され、それを指すようにシンボリックリンク current を作り直す。 つまり、デプロイ時に current の指し示すディレクトリが変わる。

Unicorn の挙動について

Unicorn はマスタープロセスとワーカープロセスによって動作する。 通常ワーカープロセスは複数存在し、マスタープロセスから仕事(クライアントからのリクエスト)をもらう。 Unicorn の再起動はマスタープロセスに USR2 というシグナルを送ることで行う。 USR2 というシグナルを受け取ると Unicorn は Ruby の exec メソッドを使ってマスタープロセスを再起動する。

Bundler の挙動について

Bundler はセットアップ時に環境変数 RUBYOPT に -rbundler/setup をセットする。 これにより、プログラム内で exec により ruby プログラムを起動した際は require 'bundler/setup' が始めに実行されるようになる。 require 'bundler/setup' は Bunder のロードとセットアップを同時に行う。 Bundler は、環境変数 BUNDLE_GEMFILE に値(Gemfile のパス)がセットされている場合はそれをもとにセットアップ処理を行う。

なぜ Gemfile に新たに追加された gem を Bundler が読み込めなかったのか

Rails アプリケーションの config/boot.rb に Bundler を初期化する処理が記述されており、アプリケーション起動時には真っ先にここが実行される。 その際、環境変数 BUNDLE_GEMFILE にアプリケーションのルートディレクトリに存在する Gemfile のパスがセットされる(例えば "/var/www/rails/my_app/releases/20110209064641/Gemfile" のような値がセットされる)。

Unicorn の再起動時に環境変数 RUBYOPT と BUNDLE_GEMFILE が exec を通して新マスタープロセスに引き継がれる。 つまり、最初に config/boot.rb で設定された BUNDLE_GEMFILE の値が Unicorn の再起動を経てもマスタープロセスにおいて残り続ける。 新マスタープロセスでは RUBYOPT (-rbundler/setup) により Bundler のセットアップ処理が最初に行われる。 この時使用される Gemfile は BUNDLE_GEMFILE に設定されているファイルである(つまり最初に Unicorn を起動したアプリケーションの Gemfile)。

このようにして、新しくデプロイしたアプリケーションの Gemfile (ファイルの場所が変わっている)が Unicorn 再起動時に Bundler によって読み込まれず、新たに追加した gem に含まれるファイルを require している箇所で LoadError が起きたのでした。

(ふぅ、疲れた。。)

図にするとこんな感じ

Screen shot 2011-02-15 at 3.54.40 PM

対処方法

  1. "/var/www/rails/my_app/releases/20110209064641/Gemfile" というようなリアルなパスではなく、シンボリックリンクを含んだ "/var/www/rails/my_app/releases/current/Gemfile" というようなパスを環境変数 BUNDLE_GEMFILE にセットした状態で Unicorn を起動する
  2. config/boot.rb では環境変数 BUNDLE_GEMFILE がセットされているときにそれを上書きしないようにする

1 については、 Capistrano のレシピを以下のように修正した

task :start do
  run "cd #{current_path} && bundle exec unicorn_rails -c config/unicorn.rb -E #{rails_env} -D"
end

task :start do
  run "cd #{current_path} && BUNDLE_GEMFILE=#{current_path}/Gemfile bundle exec unicorn_rails -c config/unicorn.rb -E #{rails_env} -D"
end

2 については config/boot.rb を以下のように修正した

gemfile = File.expand_path('../../Gemfile', __FILE__)

gemfile = ENV['BUNDLE_GEMFILE'] || File.expand_path('../../Gemfile', __FILE__)