最近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 # 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で一気にデプロイできるようにしました。
前提条件として、
- /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): # 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向けのラッパーもあったりします。
こちらも試してみたのですが、daemonとして動かないため利用するのは止めました。あとちょっと不安定っぽい。