nginxとgunicornとsupervisorを連携させる

私はVPSと自宅LAN内でDjangoで作った個人的なwebサービスをいくつか運用しています。
現在の運用環境はapache+mod_wsgiですが、ネットで色々調べていると、nginxとgunicornとsupervisorを組み合わせるのが旬(?)のようなので、その方法について色々調べたことを書きたいと思います。

具体的には、nginxがリバースプロキシ兼Webサーバとして、キャッシュと静的ファイルの配信を担当し、gunicornがバックエンドとして動的なページ生成を担当し、supervisorがサーバプロセスの監視を行う、という構成です。

環境としては、CentOS 5.X を想定しています。

nginxのインストール

CentOSにnginxをイントールする方法はこちらのページに詳しく書かれているので、それを参考にしました。configureの引数のうち、追加した方が良さそうなものがあったので、それだけ掲載しておきます。

./configure \
--prefix=/usr/local \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--user=nginx \
--group=nginx \
--with-http_stub_status_module \
--with-http_ssl_module \
--with-http_gzip_static_module \
--http-log-path=/var/log/nginx/access.log \
--http-client-body-temp-path=/var/tmp/nginx/client/ \
--http-proxy-temp-path=/var/tmp/nginx/proxy/ \
--http-fastcgi-temp-path=/var/tmp/nginx/fcgi/ \
--http-uwsgi-temp-path=/var/tmp/nginx/uwsgi/ \
--http-scgi-temp-path=/var/tmp/nginx/scgi/

gunicornのインストール

gunicornはeasy_installを使って一発でインストールできます。また、setproctitleというライブラリをインストールしておけば、psコマンドやtopコマンドで見えるプロセス名を任意の名前に設定できるようになります。

sudo easy_install gunicorn
sudo easy_install setproctitle

supervisorのインストール

supervisorもeasy_installを使ってインストールすることができます。

sudo easy_install supervisor

supervisorをインストールするとecho_supervisord_confというコマンドが使用できるようになります。
このコマンドを実行すると、supervisorの設定ファイルの雛形が標準出力に出力されるので、リダイレクトして保存し、編集した後、/etc/supervisord.confに保存します。

echo_supervisord_conf > supervisord_conf
vim supervisord_conf
sudo cp supervisord_conf /etc/supervisord_conf

ついでに、supervisord自体の起動スクリプトを見よう見まねで作成したので、掲載しておきます。

#!/bin/sh
#
# supervisord - this script starts and stops the supervisord daemon
#
# chkconfig:   - 90 10
# description:  Supervisor is a client/server system that allows \
#               its users to monitor and control a number of \
#               processes on UNIX-like operating systems.
# processname:  supervisord
# config:       /etc/supervisord.conf
# pidfile:      /tmp/supervisord.pid

# Source function library.
. /etc/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

RETVAL=0
supervisord="/usr/local/bin/supervisord"
prog=$(basename $supervisord)
pidfile=/tmp/supervisord.pid
lockfile=/var/lock/subsys/supervisord

start () {
    echo -n $"Starting $prog: "
    daemon $supervisord --pidfile $pidfile
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && touch $lockfile
    return $RETVAL
}
stop () {
    echo -n $"Stopping $prog: "
    killproc -p $pidfile $supervisord -QUIT
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && rm -f $lockfile
    return $RETVAL
}
restart () {
    stop
    sleep 1
    start
}
reload () {
    echo -n $"Reloading $prog: "
    killproc -p $pidfile $supervisord -HUP
    RETVAL=$?
    echo
}

case "$1" in
    start)
	start
	;;
    stop)
	stop
	;;
    reload)
	reload
	;;
    restart)
	restart
	;;    
    status)
	status -p ${pidfile} supervisord
	RETVAL=$?
	;;
    *)
	echo $"Usage: $0 {start|stop|status|restart|reload}"
        RETVAL=2
	;;
esac

exit $RETVAL

このファイルを/etc/rc.d/init.d/supervisordに保存して、以下のコマンドを実行すると、マシン起動時にsupervisordが自動的に起動されます。

sudo chmod 755 /etc/rc.d/init.d/supervisord
sudo /sbin/chkconfig --add supervisord
sudo /sbin/chkconfig supervisord on

gunicornの設定

gunicornの設定例を以下に示します。(memoはアプリケーション名)

bind = 'unix:/tmp/gunicorn_memo.sock'

backlog = 2048
workers = 1
worker_class = 'sync'
worker_connections = 1000
max_requests = 0
timeout = 30
keepalive = 2

debug = False
spew  = False

preload_app = True
daemon = False
pidfile = '/var/run/gunicorn/memo.pid' # /var/run/gunicornを作成しておく
user  = 'memo_app'
group = 'nginx'
umask = 0002
# tmp_upload_dir = None

logfile = '/var/log/gunicorn/memo.log' # /var/log/gunicornを作成しておく
loglevel = 'info'
logconfig = None

proc_name = gunicorn_memo'

ワーカーの数は個人的なサービスを考えているので1としていますが、経験的にはCPUコア数×2+1が良いようです。nginxとsupervisorとの連携を考える上では、以下の点がポイントです。

  • nginxとはUNIXドメインソケットを使ってやり取りを行うことを考えているので、bindオプションの値をunix:〜にする。
  • ソケットはnginxとgunicornの両方のプロセスから読み書き可能でなければいけない。nginxとgunicornを別々のユーザで走らせたい場合は、gunicornのgroupをnginxとし、umask=0002(ユーザ及びグループが読み書き可能)とする。
  • supervisorでプロセスを監視するためにはデーモン化してはいけないので、daemon=Falseとする。

supervisorの設定

supervisorはHTTPサーバ機能を持っており、ウェブブラウザを介してプロセスの状態確認や起動/停止等を行うためのインターフェースが用意されています。
これをnginxを介して利用できるようにしたいと思います。
supervisorのHTTPサーバにははUNIXドメインソケットを使ったサーバとINETドメインソケットを使ったサーバの二種類ありますが、UNIXドメインソケットを使う方はnginxとの連携がうまくいかなかったので、INETドメインソケットを使ったサーバを使用します。

unix_http_serverのセクションをコメントアウトして、inet_http_serverを有効化します(パスワードは適宜設定します)。

;[unix_http_server]
;file=/tmp/supervisor.sock ; (the path to the socket file)
;chmod=0700                ; sockef file mode (default 0700)
;chown=nobody:nobody       ; socket file uid:gid owner
;username=user             ; (default is no username (open server))
;password=123              ; (default is no password (open server))

[inet_http_server]         ; inet (TCP) server disabled by default
port=127.0.0.1:9001        ; (ip_address:port specifier, *:port for all iface)
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

supervisorctlのセクションも合わせて変更します。

[supervisorctl]
;serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket

supervisorを使ってgunicornプロセスを起動するための設定例

[program:gunicorn_memo]
command=/usr/local/bin/gunicorn_django --config /opt/www/memo/gunicorn_conf.py /opt/www/memo/settings.py
directory=/opt/www/memo
user=root
autostart=true
autorestart=true
redirect_stderr=true
environment=PYTHON_EGG_CACHE=/opt/www/memo/.python-eggs

nginxの設定

supervisorのバーチャルホストの設定

upstream supervisor-backend {
  server 127.0.0.1:9001 fail_timeout=0;
}

server {
  listen       80;
  server_name  supervisor.local.saitodev.com;

  access_log  /var/log/nginx/supervisor.local.saitodev.com-access.log main;
  error_log   /var/log/nginx/supervisor.local.saitodev.com-error.log  info;

  location / {
    allow 127.0.0.1;
    allow 192.168.0.0/24;
    deny  all;
    proxy_set_header Host $host;
    proxy_pass http://supervisor-backend;
    break;
  }
}

proxy_set_headerの行が無いと、リダイレクト処理で失敗します。

gunicornのバーチャルホストの設定

upstream memo-backend {
  server unix:/tmp/gunicorn_memo.sock fail_timeout=0;
}

server {
    listen       80;
    server_name  memo.local.saitodev.com;

    access_log  /var/log/nginx/memo.local.saitodev.com-access.log main;
    error_log   /var/log/nginx/memo.local.saitodev.com-error.log  info;

    # djangoのadminの静的ファイルを配信する場合に必要
    location ~ /media/(.*)$ {
      alias /usr/local/lib/python2.7/site-packages/django/contrib/admin/media/$1;
      break;
    }

    location / {
      proxy_set_header Host $host;
      proxy_pass http://memo-backend;
      break;
    }
}

最後にコメント

今回の例では、nginxとgunicornを別ユーザで動かそうとしたため、結果的にsupervisordとgunicornのmasterがrootで動いてしまっています。
実際の運用では、nginxとgunicornとsupervisordを全て同一の非rootユーザで動かした方がセキュリティ的にましな気がしてきました。