smellman's Broken Diary

クソみたいなもんです

Gunicornでsymbolic linkを貼り直しつつ停止なしでデプロイしたい

最近Pythonのプロダクトを扱っていたりします。
GunicornはRuby on Railsでよく使われているUnicornの影響を受けたと思われるプロダクトで、Gunicornは"Green Unicorn"という意味らしいです。

Unicornではよく知られてる手法としてkillによる再起動を使ってほぼ無停止でデプロイ後に再起動をかける手段があります。

Gunicornでも FAQ - How do I reload my application in Gunicorn? にあるように、以下のようなコマンドで再起動ができるそうです。

kill -HUP masterpid

しかしながら、デプロイ時にsymbolic linkを入れ替えるという手法を取った場合にうまく動きません。そもそも、これってソースコードの編集に対応してないんじゃないかなっていう感じです。(検証しているときに書き換えてこれで再起動を掛けても何も変わらなかった)

サーバ自体を停止して、起動するという方法も良いのですが、その方法だと停止なしにはならないし、Ruby on Railsでできてるのになんか負けた気がしたのでちゃんとした方法がないか調べました。

例えば、Ruby on Rails 3.x + Unicorn + capistranoではcapistranounicornで以下のような処理を書いています。
capistranoのdeploy.rbではこんな感じです。

namespace :deploy do
  task :start, :except => { :no_release => true } do
    run "cd #{current_path} ; BUNDLE_GEMFILE=#{current_path}/Gemfile bundle exec unicorn_rails -c config/unicorn.rb -D"
  end

  task :stop, :except => { :no_release => true } do
    run "kill -s QUIT `cat /tmp/#{application}_unicorn_production.pid`"
  end

  task :restart, :except => { :no_release => true } do
    run "kill -s USR2 `cat /tmp/#{application}_unicorn_production.pid`"
  end
end

もう一つunicorn.rbで以下のようなコードを使っています。

before_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  old_pid = "/tmp/#{application}_unicorn_production.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # the following is *required* for Rails + "preload_app true",
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end

(この例ではActiveRecordの処理がありますが、今回は使いません)

このような処理を再現すればいいのかと思い調べたところ、以下のスレッドにヒントが有りました。

HUP reloading does not get sys.path updates from .pth files.

このコメントで以下の3つのコマンドを打てばよいという結論が出ていました。

# Reexec a new master with new workers
/bin/kill -s USR2 `cat "$PID"`
# Graceful stop old workers
/bin/kill -s WINCH `cat "$PIDOLD"`
# Graceful stop old master 
/bin/kill -s QUIT `cat "$PIDOLD"`

これを応用してfabricで一気にデプロイできるようにしました。

前提条件として、

  1. /home/hoge/hoge_ve にvirtualenv環境を入れてる(僕の場合はchefで全部やってる)
  2. アプリ自体を /home/hoge/app/(timestamp) みたいにやって、deploy時に /home/hoge/app/hoge にsymbolic linkを貼ってる
  3. /home/hoge/app/.python-eggs は事前にディレクトリを作っておく(あまり意味がないかも?)
# 環境設定
def commonsrv():
    ....
    env.app_name = 'hoge'
    env.home = '~/'
    env.deploy_dir = env.home + 'app/'
    env.app_deploy_root = env.deploy_dir + env.app_name
    env.hoge_pid = '/tmp/hoge_gunicorn.pid'
    env.hoge_pid_old = env.hoge_pid + '.oldbin'

def hogeserv():
    '''hogeサーバーの設定'''
    commonsrv()
    env.user = 'hoge'
    env.hosts = ['127.0.0.1:13022']

def start_app():
    '''
    AP起動
    '''
    cmd = "PYTHON_EGG_CACHE=/home/hoge/app/.python-eggs PATH=$PATH:/home/hoge/hoge_ve/bin/ /home/hoge/hoge_ve/bin/gunicorn main:app -c /home/hoge/app/hoge/gunicorn.nosock.conf.py"
    with cd(env.app_deploy_root):
        run(cmd)

def stop_app():
    '''
    AP停止
    '''
    cmd = "kill -s QUIT `cat " + env.hoge_pid + "`"
    with cd(env.app_deploy_root):
        run(cmd)

def restart_app():
    '''
    AP再起動
    '''
    if exists(env.hoge_pid):
        # Reexec a new master with new workers
        cmd = "kill -s USR2 `cat " + env.hoge_pid + "`"
        run(cmd)
        # Graceful stop old workers
        cmd = "kill -s WINCH `cat " + env.hoge_pid_old + "`"
        run(cmd)
        # Graceful stop old master
        cmd = "kill -s QUIT `cat " + env.hoge_pid_old + "`"
        run(cmd)
    else:
        start_app()

def deploy():
    '''
    デプロイ
    '''
    ....(デプロイの処理とか)
    restart_app()

Gunicornのconfigには以下のように記述しておきます。

# -*- coding: utf-8 -*-
bind = '127.0.0.1:5000'
backlog = 2048
...
debug = False
spew  = False
preload_app = True
daemon = True
pidfile = '/tmp/hoge_gunicorn.pid'
user  = 'hoge'
group = 'hoge'
accesslog = '/var/log/gunicorn/hoge-access.log' # /var/log/gunicornを作成しておく
errorlog = '/var/log/gunicorn/hoge-error.log'
loglevel = 'info'
logconfig = None

デプロイはこんなコマンドです。

fab -f hoge.py hogeserv deploy

これで、すでにサーバが起動していたら切り替わるという寸法です。
再起動だけしたいときも、

fab -f hoge.py hogeserv restart_app

としています。

oldbinについてはUnicornとほぼ同様と見て良いでしょう。実際、Gunicornのコードを見ても入れ替えてる処理が見えます。

まだ、これでも問題があったりします。

一つは stop_app したあとに start_app がたまに動かなくなります。何度も叩いてると動くようになるので何かがロックしてるのかもしれない。
それでも、運用時にはそんなに問題にはならないレベルになっています。

もう一つは Gunicorn で unix socket を使った場合はこの手法だとバグがあって使えないみたいです。
USR2 + QUIT deletes unix socket when QUIT on old master
これでかなりの時間ハマりましたorz

ちなみに、上記のkillを使う手法をベースとしてGunicorn向けのラッパーもあったりします。

Rainbow Saddle

こちらも試してみたのですが、daemonとして動かないため利用するのは止めました。あとちょっと不安定っぽい。