最近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ではcapistranoとunicornで以下のような処理を書いています。
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
end
end
end
after_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end
end
(この例ではActiveRecordの処理がありますが、今回は使いません)
このような処理を再現すればいいのかと思い調べたところ、以下のスレッドにヒントが有りました。
HUP reloading does not get sys.path updates from .pth files.
このコメントで以下の3つのコマンドを打てばよいという結論が出ていました。
/bin/kill -s USR2 `cat "$PID"`
/bin/kill -s WINCH `cat "$PIDOLD"`
/bin/kill -s QUIT `cat "$PIDOLD"`
これを応用してfabricで一気にデプロイできるようにしました。
前提条件として、
- /home/hoge/hoge_ve にvirtualenv環境を入れてる(僕の場合はchefで全部やってる)
- アプリ自体を /home/hoge/app/(timestamp) みたいにやって、deploy時に /home/hoge/app/hoge にsymbolic linkを貼ってる
- /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):
cmd = "kill -s USR2 `cat " + env.hoge_pid + "`"
run(cmd)
cmd = "kill -s WINCH `cat " + env.hoge_pid_old + "`"
run(cmd)
cmd = "kill -s QUIT `cat " + env.hoge_pid_old + "`"
run(cmd)
else:
start_app()
def deploy():
'''
デプロイ
'''
....(デプロイの処理とか)
restart_app()
Gunicornのconfigには以下のように記述しておきます。
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'
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として動かないため利用するのは止めました。あとちょっと不安定っぽい。