Разработка на коленке

"тут должна быть красивая цитата о программировании"

Настройка python-хостинга для pylons-приложений на основе nginx + uwsgi на ubuntu

2011-05-27 12:12

Настраиваю сервер ubuntu для запуска на нём wsgi-приложений, написанных с использованием pylons.

Я не админ, я только учусь!

Мне нужно было настроить сервер, на котором бы крутилось pylons-приложение. Поскольку я проверял разные конфигурации по памяти, процессору и версиям пакетов, мне нужно было сделать всё возможное, чтобы упростить тестирование и повторное разворачивание сервера на виртуальных машинах. К самому серверу были следующие требования:

  1. Статику должен отдавать шустрый сервер (nginx | lighttpd)
  2. Приложение должно работать не под рутом, чтобы было не очень страшно за безопасность.
  3. Процессы нужно мониторить и перезапускать в случае падения.
  4. Обо всех непрятностях сообщать админу (мне то есть).
  5. Для удобства тестирования весь процесс установки должен быть автоматизирован, чтобы действия ограничивались запуском нескольких скриптов.

В результате решил, что:

  1. В качестве фронт-енд сервера будет nginx, потому что lighttpd + flup ложится при большой нагрузке из-за утечек памяти.
  2. В качестве wsgi-сервера взял uwsgi, потому что во время тестов этот сервер показал хорошую скорость.
  3. nginx и uwsgi ставим из исходников.

Что нужно сделать:

  1. Подготовить конфигурационные файлы для nginx, uwsgi, monit, pylons-приложения.
  2. Подготовить файл, который запускается под рутом, добавляет пользователя, устанавливает пакеты.
  3. Подготовить файл, который запускается под пользователем и компилирует uwsgi.

Что получаем в итоге:

  1. Набор конфигурационных файлов.
  2. Два скрипта, которые нужны для разворачивания сервера.

Далее идут файлы из расчёта, что приложение было создано командой:

paster create -t pylons tinycode

Настройка nginx

Файл настроек - nginx.conf:

worker_processes  1;

events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  0;

    server {
        listen       80;
        server_name  tinycode.ru;
    charset utf-8;
    root /home/tinyuser/tinycode.ru/tinycode/public;

    location ~*/(pdf|css|js|png|jpg|gif|jpeg|bmp|JPG)/ {
        root   /home/tinyuser/tinycode.ru/tinycode/public;
        expires     max;
        add_header  Cache-Control "public";
        break;
    }

    location / {
        uwsgi_pass  0.0.0.0:3993;
        include     uwsgi_params;
        uwsgi_param SCRIPT_NAME "";
    }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

Теперь нужен скрипт для запуска nginx, этот скрипт я честно упёр с одного сайта, о чём сообщают копирайты в коментах. Исходный скрипт расчитан на дефолтную установку nginx, для работы с собранным вручную сервером подправил пару путей, в остальном скрипт тот же. Назовём его nginx:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: nginx init.d dash script for Ubuntu <=9.10.
# Description:       nginx init.d dash script for Ubuntu <=9.10.
### END INIT INFO
#------------------------------------------------------------------------------
# nginx - this Debian Almquist shell (dash) script, starts and stops the nginx
#         daemon for ubuntu 9.10 and lesser version numbered releases.
#
# description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
#               proxy and IMAP/POP3 proxy server.  This \
#           script will manage the initiation of the \
#           server and it's process state.
#
# processname: nginx
# config:      /usr/local/nginx/conf/nginx.conf
# pidfile:     /acronymlabs/server/nginx.pid
# Provides:    nginx
#
# Author:  Jason Giedymin
#          <jason.giedymin AT gmail.com>.
#
# Version: 2.0 02-NOV-2009 jason.giedymin AT gmail.com
# Notes: nginx init.d dash script for Ubuntu <=9.10.
#
# This script's project home is:
#   http://code.google.com/p/nginx-init-ubuntu/
#
#------------------------------------------------------------------------------
#                               MIT X11 License
#------------------------------------------------------------------------------
#
# Copyright (c) 2009 Jason Giedymin, http://Amuxbit.com formerly
#                                http://AcronymLabs.com
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
#                               Functions
#------------------------------------------------------------------------------
. /lib/lsb/init-functions

#------------------------------------------------------------------------------
#                               Consts
#------------------------------------------------------------------------------
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/local/nginx/sbin/nginx

PS="nginx"
PIDNAME="nginx"                             #lets you do $PS-slave
PIDFILE=$PIDNAME.pid                    #pid file
PIDSPATH=/usr/local/nginx/logs

DESCRIPTION="Nginx Server..."

RUNAS=root                              #user to run as

SCRIPT_OK=0                             #ala error codes
SCRIPT_ERROR=1                          #ala error codes
TRUE=1                                  #boolean
FALSE=0                                 #boolean

lockfile=/var/lock/subsys/nginx
NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"

#------------------------------------------------------------------------------
#                               Simple Tests
#------------------------------------------------------------------------------

#test if nginx is a file and executable
test -x $DAEMON || exit 0

# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then
        . /etc/default/nginx
fi

#set exit condition
#set -e

#------------------------------------------------------------------------------
#                               Functions
#------------------------------------------------------------------------------

setFilePerms(){

        if [ -f $PIDSPATH/$PIDFILE ]; then
                chmod 400 $PIDSPATH/$PIDFILE
        fi
}

configtest() {
    $DAEMON -t -c $NGINX_CONF_FILE
}

getPSCount() {
    return `pgrep -f $PS | wc -l`
}

isRunning() {
        if [ $1 ]; then
                pidof_daemon $1
                PID=$?

                if [ $PID -gt 0 ]; then
                        return 1
                else
                        return 0
                fi
        else
                pidof_daemon
                PID=$?

                if [ $PID -gt 0 ]; then
                        return 1
                else
                        return 0
                fi
        fi
}

#courtesy of php-fpm
wait_for_pid () {
        try=0

        while test $try -lt 35 ; do

                case "$1" in
                        'created')
                        if [ -f "$2" ] ; then
                                try=''
                                break
                        fi
                        ;;

                        'removed')
                        if [ ! -f "$2" ] ; then
                                try=''
                                break
                        fi
                        ;;
                esac

                #echo -n .
                try=`expr $try + 1`
                sleep 1
        done
}

status(){
    isRunning
    isAlive=$?

    if [ "${isAlive}" -eq $TRUE ]; then
                echo "$PIDNAME found running with processes:  `pidof $PS`"
        else
                echo "$PIDNAME is NOT running."
        fi


}

removePIDFile(){
    if [ $1 ]; then
                if [ -f $1 ]; then
                    rm -f $1
            fi
        else
        #Do default removal
        if [ -f $PIDSPATH/$PIDFILE ]; then
                    rm -f $PIDSPATH/$PIDFILE
            fi
        fi
}

start() {
        log_daemon_msg "Starting $DESCRIPTION"

    isRunning
    isAlive=$?

        if [ "${isAlive}" -eq $TRUE ]; then
                log_end_msg $SCRIPT_ERROR
        else
                start-stop-daemon --start --quiet --chuid $RUNAS --pidfile $PIDSPATH/$PIDFILE --exec $DAEMON \
                -- -c $NGINX_CONF_FILE
                setFilePerms
                log_end_msg $SCRIPT_OK
        fi
}

stop() {
    log_daemon_msg "Stopping $DESCRIPTION"

    isRunning
    isAlive=$?
        if [ "${isAlive}" -eq $TRUE ]; then
                start-stop-daemon --stop --quiet --pidfile $PIDSPATH/$PIDFILE

        wait_for_pid 'removed' $PIDSPATH/$PIDFILE

                if [ -n "$try" ] ; then
                        log_end_msg $SCRIPT_ERROR
                else
                        removePIDFile
                    log_end_msg $SCRIPT_OK
                fi

        else
                log_end_msg $SCRIPT_ERROR
        fi
}

reload() {
    configtest || return $?

    log_daemon_msg "Reloading (via HUP) $DESCRIPTION"

        isRunning
        if [ $? -eq $TRUE ]; then
        `killall -HUP $PS` #to be safe

                log_end_msg $SCRIPT_OK
        else
                log_end_msg $SCRIPT_ERROR
        fi
}

quietupgrade() {
    log_daemon_msg "Peforming Quiet Upgrade $DESCRIPTION"

        isRunning
        isAlive=$?
        if [ "${isAlive}" -eq $TRUE ]; then
        kill -USR2 `cat $PIDSPATH/$PIDFILE`
        kill -WINCH `cat $PIDSPATH/$PIDFILE.oldbin`

        isRunning
        isAlive=$?
        if [ "${isAlive}" -eq $TRUE ]; then
            kill -QUIT `cat $PIDSPATH/$PIDFILE.oldbin`
            wait_for_pid 'removed' $PIDSPATH/$PIDFILE.oldbin
                        removePIDFile $PIDSPATH/$PIDFILE.oldbin

            log_end_msg $SCRIPT_OK
        else
            log_end_msg $SCRIPT_ERROR

            log_daemon_msg "ERROR! Reverting back to original $DESCRIPTION"

            kill -HUP `cat $PIDSPATH/$PIDFILE`
            kill -TERM `cat $PIDSPATH/$PIDFILE.oldbin`
            kill -QUIT `cat $PIDSPATH/$PIDFILE.oldbin`

            wait_for_pid 'removed' $PIDSPATH/$PIDFILE.oldbin
                        removePIDFile $PIDSPATH/$PIDFILE.oldbin

            log_end_msg $SCRIPT_ok
        fi
        else
                log_end_msg $SCRIPT_ERROR
        fi
}

terminate() {
        log_daemon_msg "Force terminating (via KILL) $DESCRIPTION"

    PIDS=`pidof $PS` || true

    [ -e $PIDSPATH/$PIDFILE ] && PIDS2=`cat $PIDSPATH/$PIDFILE`

    for i in $PIDS; do
        if [ "$i" = "$PIDS2" ]; then
                kill $i
                        wait_for_pid 'removed' $PIDSPATH/$PIDFILE
            removePIDFile
        fi
    done

    log_end_msg $SCRIPT_OK
}

destroy() {
    log_daemon_msg "Force terminating and may include self (via KILLALL) $DESCRIPTION"
    killall $PS -q >> /dev/null 2>&1
    log_end_msg $SCRIPT_OK
}

pidof_daemon() {
    PIDS=`pidof $PS` || true

    [ -e $PIDSPATH/$PIDFILE ] && PIDS2=`cat $PIDSPATH/$PIDFILE`

    for i in $PIDS; do
        if [ "$i" = "$PIDS2" ]; then
            return 1
        fi
    done
    return 0
}

case "$1" in
start)
    start
        ;;
stop)
    stop
        ;;
restart|force-reload)
    stop
    sleep 1
    start
        ;;
reload)
    $1
    ;;
status)
    status
    ;;
configtest)
        $1
        ;;
quietupgrade)
    $1
    ;;
terminate)
    $1
    ;;
destroy)
    $1
    ;;
*)
    FULLPATH=/etc/init.d/$PS
    echo "Usage: $FULLPATH {start|stop|restart|force-reload|status|configtest|quietupgrade|terminate|destroy}"
    echo "       The 'destroy' command should only be used as a last resort."
    exit 1
    ;;
esac

exit 0

Настройка uwsgi

Скрипт, запускающий непосредственно сервер run-uwsgi.sh (этот скрипт запускается из под пользователя, не из под рута):

#!/bin/sh

cd /home/tinyuser/tinycode.ru

# run uwsgi server
/home/tinyuser/uwsgi/uwsgi -p 4 --paste config:/home/tinyuser/tinycode.ru/tinyuser.ini --socket :3993 > /dev/null 2>&1 &</code></pre>

Скрипт для запуска и остановки uwsgi:

#!/bin/sh

case $1 in
        start)
                echo `exec 2>&1 su tinyuser -c /home/tinyuser/run-uwsgi.sh`;
                ps -C uwsgi -o pid= | sed s/\ //g > /var/run/uwsgi.pid
                ;;
        stop)
                killall uwsgi ;;
        *)
                echo "usage: uwsgi {start|stop}" ;;
esac
exit 0

Настройка monit

Создадим конфиг для monit - monitrc:

## Start monit in the background (run as a daemon):

set daemon  300
with start delay 240

# admin settings
set mailserver localhost
set alert alert.mail@tinycode.ru

# check uwsgi
check process uwsgi with pidfile /var/run/uwsgi.pid
        start program "/etc/init.d/uwsgi start"
        stop program "/etc/init.d/uwsgi stop"

# check nginx
check process nginx with pidfile /var/run/nginx.pid
start program = "/etc/init.d/nginx start"
stop program  = "/etc/init.d/nginx stop"

check system localhost
if loadavg (1min) > 4 then alert
if loadavg (5min) > 2 then alert

И ещё один файл - monit:

# Defaults for monit initscript
# sourced by /etc/init.d/monit
# installed at /etc/default/monit by maintainer scripts
# Stefan Alfredsson <alfs@debian.org>

# You must set this variable to for monit to start
startup=1

# To change the intervals which monit should run,
# edit the configuration file /etc/monit/monitrc
# It can no longer be configured here.

Настройка pylons-приложения

У каждого свой конфиг, я запишу свои настройки в tinyuser.ini

Компиляция uwsgi

Для того, чтобы подготовить uwsgi нужен будет файл install-uwsgi.sh: .. code:

#!/bin/sh

#run this code under tinyuser

cd ~
wget http://projects.unbit.it/downloads/uwsgi-0.9.7.1.tar.gz
tar xzf uwsgi-0.9.7.1.tar.gz
mv uwsgi-0.9.7.1 uwsgi
cd uwsgi
make

Что получилось

В результате всех манипуляций должен получиться следующий набор файлов:

  1. nginx.conf
  2. nginx
  3. run-uwsgi.sh
  4. uwsgi
  5. monitrc
  6. monit
  7. tinyuser.ini
  8. install-uwsgi.sh

Осталось подготовить файл, который установит пакеты и пропишет демонов в автозагрузку.

install-server.sh

#!/bin/sh
# the default folder with pylons project = /home/tinyuser/tinycode.ru

RUN_USER=tinyuser
adduser $RUN_USER

apt-get update

#install soft
apt-get install -y vim python-setuptools

#install python libs
easy_install "Pylons == 0.9.7"
easy_install "SQLAlchemy >= 0.5"
easy_install "TurboMail == 3.0.3"
easy_install "AuthKit >= 0.4.3, <= 0.4.99"
easy_install uwsgi

apt-get install -y build-essential python-numpy python-scipy python-reportlab python-reportlab-accel python-renderpm libfreetype6 python-dev python-imaging libxml2-dev libpcre3-dev sqlite3 libpcre3-dev libssl-dev

# install nginx
mkdir soft
cd soft

wget http://sysoev.ru/nginx/nginx-0.9.6.tar.gz
tar xzf nginx-0.9.6.tar.gz
cd nginx-0.9.6
./configure && make && make install

cd ../../

cp nginx.conf /usr/local/nginx/conf/
cp nginx /etc/init.d/
/usr/sbin/update-rc.d -f nginx defaults

cp uwsgi /etc/init.d/
chmod 0755 /etc/init.d/uwsgi
/usr/sbin/update-rc.d -f uwsgi defaults

# install ftp
# apt-get install -y vsftpd

# install monit
apt-get install -y monit
cp monitrc /etc/monit/
chmod 0600 /etc/monit/monitrc
cp monit /etc/default/
chmod 0644 /etc/default/monit
/etc/init.d/monit restart


# copy files for uwsgi server to user dir
cp install-uwsgi.sh /home/$RUN_USER/
cp run-uwsgi.sh /home/$RUN_USER/
cp tinyuser.ini /home/$RUN_USER/

chown $RUN_USER /home/$RUN_USER/install-uwsgi.sh
chown $RUN_USER /home/$RUN_USER/run-uwsgi.sh
chown $RUN_USER /home/$RUN_USER/tinyuser.ini

echo "login under $RUN_SERVER and run install-uwsgi.sh"

Разворачиваем сервер:

  1. Под рутом запустить install-server.sh
  2. Под пользователем tinyuser запустить install-uwsgi.sh
  3. В папку /home/tinyuser/tinycode.ru скопировать файлы приложения. Это те файлы, которые лежат в папке tinycode после создания проекта командой paster create -t pylons tinycode
  4. Скопировать файл tinyuser.ini в /home/tinyuser/tinycode.ru
  5. Перезагружаем сервер.

Ещё была идея запихивать архив с проектом вместе с этими файлами и автоматически его распаковывать, но отказался от этой идеи, потому что проекты могут быть разными, могут заливаться разными людьми, а моя цель была в настройке именно сервера, а не в деплое приложения.

На этом всё, до новых встреч :)