タイトルの通り、gunicornなWebアプリをsupervisorで制御しつつhot deployできるようにしたメモです。
登場人物
問題
- gunicornのhot deployを利用する場合、直接はsupervisorの制御下に置けない
- gunicornはhot deployの仕組みを持っている
- 流れ
- gunicornのmasterプロセスにUSR2シグナルを送ると、新しいmasterプロセスを産む
- 旧masterと旧workerは生き続けているので、ほどよい頃合いで旧masterにQUITシグナルなどを送って死んでもらう
- 新masterの親が死んだので、init (PID 1) が新masterの親となる
- というわけで、supervisor視点だとgunicorn(旧master)が死んだのでまた立ち上げようとするが、新masterはinitを親として起動しているので競合しちゃう次第
結論
- supervisor + unicornherder + gunicorn
案1: start_server配下でgunicornを立ちあげ、hot deployはstart_serverの機能を使う
## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash
export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
exec start_server \
--port 127.0.0.1:1919 \
--signal-on-term=TERM \
--signal-on-hup=QUIT \
--kill-old-delay=10 \
-- \
/home/hirose31/oreno/script/start-gunicorn
## /home/hirose31/oreno/script/start-gunicorn
#!/bin/bash
if [[ -n "$SERVER_STARTER_PORT" ]]; then
GUNICORN_FD=
for l in $(echo $SERVER_STARTER_PORT | tr ';' ' '); do
GUNICORN_FD="$GUNICORN_FD $(echo $l | cut -d= -f2)"
done
export GUNICORN_FD=$(echo $GUNICORN_FD | tr ' ' ',')
fi
exec /usr/local/bin/envdir /home/hirose31/oreno/env \
gunicorn -w 3 --reuse-port --capture-output oreno:application
これで supervisorctl signal HUP oreno
で start_server に HUP を送ると、start_serverが新たにgunicornを起動して、10秒後に古いgunicornにQUITを送って殺してくれる。はず。
つまり、(gunicornの機能ではなく)start_serverの機能でhot deployが実現できる。はず。
start_server に対応するにはいくつかの要件が必要ですが、
Unicornの UNICORN_FD
と同様のものが gunicorn には GUNICORN_FD
という環境変数名で存在するので、gunicornもstart_serverに対応できる。はず。
そう。はず、なのであった!
gunicornの このpull request で、GUNICORN_FD
を評価するところのがこのような条件付きになり、
### gunicorn/arbiter.py: start
elif self.master_pid:
fds = []
for fd in os.environ.pop('GUNICORN_FD').split(','):
fds.append(int(fd))
初っ端の起動時は master_pid
は 0 なので GUNICORN_FD
は評価されないのであった!!
条件を elif os.environ.get('GUNICORN_FD', ''):
にすれば期待通り動くことは確認できたんだけど、できればgunicornに手を入れないで使いたいのでこの案はとりあえずボツに…
将来、外部由来の GUNICORN_FD
を評価するようになったら、gunicornは--config
オプションでpythonの文法で設定が書かれたファイルを指定することができるので、そこで SERVER_STARTER_PORT
を参照して GUNICORN_FD
をセットする処理を書いたり、pre_fork, post_fork hookを使って、Server::Starter で Unicorn を起動する場合の Slow Restart - sonots:blog と同じことも実現できるんじゃないかと思います。
余談ですが、start_server
の server-prog
の指定は、ラップしたスクリプトのみ、オプション指定無しにするのをお勧めします。
例えば start_server -- /my/app -p 8
で起動していて -p 16
に変えたいときは、start_server
自体の再起動が必要(つまりhot deployできない)になってしまいます。これがラップしていて start_server -- /my/app_wrapper
で起動している場合は、app_wrapper
の中を書き換えて start_server
に HUP を送ればOKになります。
start_server -- envdir ./env sh -c 'exec /my/app -p ${MAX_PROCESS:-8}'
と書けばHUPで引数の値を(envdir経由で)変えることは可能ですが、やや技巧的だしオプションの種類の増減には対応できないので、素直にラッパースクリプトを用意した方がよいでしょう。
案2: unicornherder配下でgunicornを立ちあげ、hot deployはgunicornの機能を使う
「gunicorn supervisor」でググるとみなさんお困りのようで、対応として rainbow-saddle と unicornherder がよく挙げられています。
rainbow-saddleは「cannot actively maintain it」だそうなので、unicornherderを試してみます。
## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash
export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
exec unicornherder -o 10 -p /var/run/oreno.pid -- \
-w 3 --reuse-port --capture-output oreno:application
unicornherderの流れはこんな感じです。
- gunicornを
--pid FILE --daemon
つきで起動する
- masterのPIDは所定のファイルに書き出される
- unicornherderの
--pidfile PATH
でPIDファイルを指定しないと、cwdにPIDファイルが作られるので注意
- gunicornはunicornherderの元を離れ、initの子プロセスとなる
- unicornherderはHUPを受けると、gunicornのmasterにUSR2を送る
- gunicornのhot deployが開始され、新masterが誕生する
- gunicornによってPIDファイルが更新される
- unicornherderはPIDファイルを2秒間隔でポーリングして、masterの変化を検知する
- masterの変化を検知すると、
- overlap (デフォルト120秒) の時間だけsleepする
- 後、旧masterを殺す
- masterにWINCH, QUITシグナルを送る
- QUITを受けたworkerは処理中であっても即、死ぬ
unicornherderはgunicornを子プロセスとしておかず、gunicornはinitの子プロセスとなる点に最初「オッ!」っと思いましたが、これは古き良きdaemonしぐさですからまぁ違和感はないです。
これで一応、問題は解決できたんですが、unicornherderはgunicornコマンドでのgunicornの起動しかサポートしていません。
gunicornの起動方法、Pythonの各種WAFとの連携方法は gunicornコマンド以外にもあります。例えば、Bottle だと bottle.run()
に server='gunicorn'
を与えると、gunicornが起動します。
その場合は、unicornherderは--gunicorn-bin
, -g
オプションでgunicornのフルパスを指定できるのでこれを使います。
## /home/hirose31/oreno/script/start
#!/bin/bash
export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
exec unicornherder -o 10 -p /var/run/oreno.pid \
-g /home/hirose31/oreno/script/oreno-gunicorn \
--
oreno-gunicorn
はお好みの方法でgunicornなプロセスを立ち上げる処理を書いたスクリプトです。
## /home/hirose31/oreno/script/oreno-gunicorn
#!/bin/bash
exec /home/hirose31/oreno/bin/oreno-python /home/hirose31/oreno/omaeno.py
注意点は以下の通りです。
- unicornherderの
-p
オプションで指定したPIDファイルと、gunicornが書き出すPIDファイルを一致させる
- gunicornはdaemonモードで起動するようにする
おわりに
unicornherderを使えば目的が達成できることがわかりましたが、個人的に安心と信頼と実績のある start_server が使いたい今日この頃です。