gunicornをsupervisorで制御しつつhot deployできるようにする

タイトルの通り、gunicornなWebアプリをsupervisorで制御しつつhot deployできるようにしたメモです。

登場人物

問題

  • gunicornのhot deployを利用する場合、直接はsupervisorの制御下に置けない
    • gunicornはhot deployの仕組みを持っている
    • 流れ
      • gunicornのmasterプロセスにUSR2シグナルを送ると、新しいmasterプロセスを産む
        • つまり新masterの親プロセスは旧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 に対応するにはいくつかの要件が必要ですが、

UnicornUNICORN_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_serverserver-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-saddleunicornherder がよく挙げられています。

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する
      • この間は新旧workerが混在する
    • 後、旧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 が使いたい今日この頃です。