読者です 読者をやめる 読者になる 読者になる

Unicorn の graceful restart と環境変数

Unicorn の graceful restart は無停止でのデプロイを可能にして非常に便利だが、fork を用いて実装されている都合で古いプロセスから新しいプロセスに環境変数が引き継がれるため、そのことに起因するトラブルがいくつかある。

dotenv の設定が書き変わらない

設定情報を dotenv で管理している人も多いと思うけど、環境変数を使っているので罠がある。

例えば最初に .env に MEMCACHE_SERVERS=memcache-server-001:11211 と書いてあったとする。 このとき Unicorn を起動すると、dotenv によって MEMCACHE_SERVERS=memcache-server-001:11211環境変数に追加される。

その後、接続先として memcache-server-002:11211 を追加したくなって .env を編集して MEMCACHE_SERVERS=memcache-server-001:11211,memcache-server-002:11211 に変える。 ここで Unicorn を graceful restart すると、古い MEMCACHE_SERVERS の値は次のプロセスにも引き継がれ、dotenv はデフォルトでは既にある環境変数を上書きしないため、新しいプロセスでも MEMCACHE_SERVERS の値は古いままで、memcache-server-002:11211 が追加されない。

Bundler のバージョンが上がらない

Bundler環境変数 RUBYLIBRUBYOPT を指定することで、Ruby の起動時に Bundler を読み込ませセットアップを行うようなしくみになっている。 RUBYLIB には初回起動時の Bundler の libdir が入っており、Unicorn を graceful restart してもこの環境変数が引き継がれるため、同じく引き継がれた RUBYOPT=-rbundler/setup によりずっと初回起動時の Bundler の libdir から bundler/setup がロードされ続ける。

解決策

どちらも before_exec でなんとかすることができそう。

dotenv については Dotenv.overload を呼ぶことで .env の内容で環境変数を上書きできる。 なので、before_exec でこれを呼ぶことによって新しい設定が使われるようにできる。 もし .env 以外の方法でセットしている環境変数があった場合、そこでセットされたものよりも .env が優先されるようになってしまうけど、まぁ .env 使ってるときにそういう環境変数は無いと思う。

before_exec do |server|
  Dotenv.overload
end

Bundler については RUBYLIB を最新の Bundler を指すように書き換えてやればいいわけで、ややトリッキーな方法だが以下のような before_exec を書くことでできそう。 もしデフォルトで RUBYLIB, RUBYOPT, GEM_HOME が存在するような環境なら、env.delete のかわりにその値をセットするようにすればいいはず。

before_exec do |server|
  env = ENV.to_hash
  %w[RUBYLIB RUBYOPT GEM_HOME].each do |key|
    env.delete(key)
  end
  rubylib = IO.popen([env, 'bundle', 'exec', 'ruby', '-e', 'puts ENV["RUBYLIB"]', unsetenv_others: true], &:read).chomp
  ENV['RUBYLIB'] = rubylib
end

追記

RUBYLIB を出力するときにわざわざ ruby を使う必要は無いという指摘があった。たしかに bundle exec env でよさそう。

before_exec do |server|
  env = ENV.to_hash
  %w[RUBYLIB RUBYOPT GEM_HOME].each do |key|
    env.delete(key)
  end
  rubylib = IO.popen([env, 'bundle', 'exec', 'env', unsetenv_others: true], &:read).slice(/^RUBYLIB=(.+)$/, 1).chomp
  ENV['RUBYLIB'] = rubylib
end