воскресенье, 24 августа 2014 г.

Настройка nginx, php5-fpm и uwsgi

До недавнего времени я долгое время пользовался веб-сервером Lighttpd, который меня во всём устраивал. Но сейчас я стал пользоваться nginx, хотя сам nginx на такой выбор непосредственно не повлиял. Но, обо всём по порядку.

1. Менеджер процессов php5-fpm

В начале года я заинтересовался менеджером процессов php5-fpm для обслуживания приложений на PHP. Он обладает большим количеством приятных преимуществ, основанных на так называемых пулах. Пул - это группа процессов, выделенная для обработки запросов, поступающих на определённый порт или Unix-сокет. Для каждого пула действует своя политика управления процессами:
  • static - строго постоянное количество процессов-обработчиков,
  • dynamic - переменное количество обработчиков, для которых указывается минимальное и максимальное количество процессов, а также количество процессов-обработчиков "на подхвате", которые держатся готовыми на случай внезапного наплыва нагрузки, чтобы не терять время на порождение новых процессов-обработчиков,
  • ondemand - режим, при котором обработчики порождаются только при поступлении запросов и завершаются спустя указанный период простоя.
Каждый пул может быть запущен от имени отдельного пользователя, так что можно легко изолировать несколько приложений друг от друга. Кроме того, даже если пулы работают от имени одного и того же пользователя, разделение приложений по пулам позволяет предотвратить ситуацию, когда высоконагруженное приложение постоянно держит занятыми процессы-обработчики, не давая таким образом нормально работать лёгким интерактивным приложениям.

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

Благодаря своим особенностям php5-fpm очень хорошо подходит для виртуального хостинга, когда на одном сервере может обслуживаться множество приложений разных пользователей. Думаю, что это уже понятно.

Однако, php5-fpm можно хорошо использовать и совместно с Lighttpd. Lighttpd даже в чём-то проще. Например, при совместной работе php5-fpm и Lighttpd мне не приходилось настраивать размеры буферов для чтения заголовков ответов, что может понадобиться сделать при использовании nginx и какого-то приложения, устанавливающего большое количество cookie. Если вам требуется запускать только приложения, написанные на PHP, то связка Lighttpd и php5-fpm может оказаться оптимальным выбором, особенно если вы уже знакомы с Lighttpd.

2. Сервер uwsgi

Позже мне потребовалось запускать веб-приложения, написанные на веб-фреймворках на языках Python и Perl. В этих веб-фреймворках приложение в конечном итоге представляет собой объект, поддерживающий интерфейсы WSGI или PSGI соответственно. Я уже пользовался веб-серверами, которые реализуют HTTP-интерфейс для приложений WSGI и PSGI - это были flup и starman. Оба веб-сервера мне не понравились своей требовательностью к ресурсам, поскольку они написаны на соответствующих интерпретируемых языках, а также необходимостью под каждое веб-приложение создавать отдельный скрипт инициализации. Этих проблем был лишён найденный сервер uwsgi, который написан на Си, имеет удобный сценарий инициализации и позволяет запускать веб-приложения с интерфейсом WSGI и PSGI. С его помощью можно даже запускать приложения Rack, написанные на Ruby. Один недостаток - снаружи он предоставляет не HTTP-интерфейс, а всё тот же WSGI.

Совместно с uwsgi умеет работать nginx, так что выбор такой связки был предопределён. Осталось решить последний вопрос - как запускать классические CGI-приложения? Поначалу я думал воспользоваться сервером spawn-fcgi или fcgiwrap, но потом нашёл CGI-плагин для uwsgi, так что для запуска любого веб-приложения оказалось достаточно лишь php5-fpm и uwsgi, которыми ещё и очень удобно управлять. Несмотря на то, что в uwsgi имеется возможность обслуживать запросы к приложениям на PHP, я всё же предпочёл использовать для PHP сервер php5-fpm, обладающий большим количеством настроек, позволяющих получить больший контроль над PHP-приложениями.

Именно таким образом я и пришёл к решению сменить привычный мне веб-сервер Lighttpd на связку nginx + php5-fpm + uwsgi. В 2008 я перешёл от использования Apache на Lighttpd, в 2014 я сменил Lighttpd на nginx. С тех пор понимание того, каким должен быть хороший веб-сервер кардинально изменилось. Если раньше это был комбайн, в котором было довольно тяжело ориентироваться, то теперь это несколько взаимодействующих программ, каждая из которых очень наглядна, быстро осваивается, радует удобством и скоростью работы.

3. Установка пакетов

Если вы планируете использовать nginx только совместно с php5-fpm, то установить потребуется только два пакета - легкий nginx и сам менеджер процессов:
# apt-get install nginx-lite php5-fpm
Если же вы планируете использовать uwsgi, то вам нужна полная версия nginx и uwsgi-сервер:
# apt-get install nginx-full uwsgi
Дополнительно, в зависимости от того, какие приложения вы собираетесь запускать под управлением uwsgi, вам может понадобиться один или несколько дополнительных пакетов. Для поддержки выполнения CGI-скриптов при помощи uwsgi установите пакет с соответствующим модулем:
# apt-get install uwsgi-plugin-cgi
Если вы планируете запускать Perl-приложения с интерфейсом PSGI, то вам понадобится модуль для поддержки соответствующего протокола:
# apt-get install uwsgi-plugin-psgi
Наконец, uwsgi можно использовать и для запуска WSGI-приложений, написанных на Python. Для этого нужен модуль, называющийся... python:
# apt-get install uwsgi-plugin-python
4. Заготовка файла конфигурации nginx

Создадим заготовку файла конфигурации /etc/nginx/sites-enabled/default:
server {
  listen 0.0.0.0:80;
  # server_name info.domain.tld;

  root /var/www;
  index index.html index.php index.pl;
}
В последующих разделах приводятся небольшие фрагменты, которые можно добавить внутрь секции server этой заготовки.

Здесь настраивается виртуальный сервер, принимающий подключения на TCP-порту 80 на любой настроенный на компьютере IP-адрес. Если необходимо выделить несколько виртуальных веб-серверов, работающих на разных IP-адресах, можно создать несколько секций, указав в каждой из них соответствующий IP-адрес.

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

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

5. CGI-приложения на PHP

Для выполнения приложений на PHP воспользуемся уже упомянутым сервером php-fpm. Создадим файл с описанием пула, который будет обрабатывать запросы к PHP-файлам. Для этого создадим файл /etc/php5/fpm/pool.d/default.conf или приведём имеющийся файл к следующему виду:
; Имя пула
[default]

; Рабочие процессы пула будут работать от имени указанного пользователя и группы
user = www-data
group = www-data

; Пул будет ожидать запросы на указанном Unix-сокете
listen = /var/run/php.sock

; Владелец Unix-сокета, его группа и права доступа к сокету
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Динамический менеджер рабочих процессов будет поддерживать от 10 до 30 процессов,
; из которых от 5 до 10 могут простаивать в ожидании поступления новых запросов.
; Если простаивающих процессов будет меньше 5 - будут порождены новые процессы,
; если простаивающих процессов окажется больше 10 - лишние будут завершены
pm = dynamic
pm.max_children = 30
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 10

; Тут можно ограничить количество запросов, последовательно обслуживаемых одним процессом
; После этого процесс будет завершён и запущен снова - это может помочь от утечек памяти
;pm.max_requests = 500

; Если обработка одного запроса длится дольше трёх минут - обработка запроса принудительно завершается
request_terminate_timeout = 180s

; В этом файле можно вести журнал обработанных запросов
;access.log = /var/log/php5-fpm.access.log

; Тут можно задать настройки, которые обычно указывают в файле php.ini
; Разница в том, что эти настройки будут действовать не глобально, а только внутри пула
; Настройки php_admin нельзя поменять изнутри самого PHP-приложения
php_value[data.timezone] = Asia/Yekaterinburg
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php5-fpm.errors.log
;php_admin_value[memory_limit] = 128M
php_admin_value[mysql.connect_timeout] = 1
php5-fpm умеет перечитывать файлы конфигурации, соответствующим образом меняя состав пулов и их настройки, не трогая те пулы, настройки которых не поменялись:
# /etc/init.d/php5-fpm reload
Однако, если вам кажется, что настройки не вступили в силу, можно перезапустить php5-fpm целиком, чтобы он гарантированно прочитал и применил новые настройки пулов, полностью перезапустив их процессы:
# /etc/init.d/php5-fpm restart
Теперь нужно добавить в заготовку файл, например, со следующим содержимым:
location ~ ^/php/(base|index|logout|query)\.php$ {
  fastcgi_pass unix:/var/run/php.sock;
  include fastcgi_params;
}

location /php/static/ {
  alias /usr/local/share/php/static/;
}
В первой секции location перечисляются файлы, запросы к которым будут обрабатываться созданным нами пулом процессов php5-fpm. Во второй секции location указан путь к статическим файлам, отдачей которых будет заниматься сам nginx.

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

Осталось перезапустить nginx, чтобы его новые настройки вступили в силу:
# /etc/init.d/nginx restart
nginx тоже умеет перезагружать обновлённую конфигурацию, однако рабочие процессы могут некоторое время продолжать работать со старой конфигурацией, пока не будут вытеснены новыми. Если это не критично, то перезагрузить его можно так:
# /etc/init.d/nginx reload
6. PHP-приложения на фреймворке CodeIgniter

Так получилось, что мой коллега на работе в своих приложениях на PHP использует веб-фреймворк CodeIgniter, который несколько необычно для PHP обрабатывает запросы. Все запросы обрабатываются одной страницей, которая уже сама учитывает таблицу маршрутов или непосредственно вызывает необходимые методы из классов контроллеров.

Прежде чем приступить к настройке nginx, сначала настроим само приложение - в файле application/config/config.php нужно прописать следующие настройки:
$config['base_url'] = '';
$config['index_page'] = '';
$config['uri_protocol'] = 'REQUEST_URI';
Первые две настройки не заданы, т.к. будет использоваться автоматическое их определение самим фреймворком.

Теперь пришла очередь nginx. Для обработки запросов к приложению будем использовать пул php5-fpm, настройка которого была описана в предыдущем разделе. В нашу заготовку файла конфигурации nginx /etc/nginx/sites-enabled/default.conf добавим следующие настройки:
location /ci/ {
  fastcgi_pass unix:/var/run/php.sock;
  include fastcgi_params;

  fastcgi_buffer_size 64k;
  fastcgi_buffers 8 64k;

  fastcgi_param SCRIPT_FILENAME /usr/local/share/ci/index.php;
  fastcgi_param SCRIPT_NAME /ci/index.php;

  fastcgi_read_timeout 120s;
}

location /ci/data/ {
  alias /usr/local/share/ci/data/;
}
В первой секции переопределяются некоторые настройки, значения по умолчанию которых нас не устраивают. Важно переопределять их после включения файла с настройками по умолчанию - иначе он сам переопределит заданные нами настройки.

Размеры буферов переопределены, поскольку CodeIgniter интенсивно использует cookie - заголовок запроса или ответа может получиться очень большим и не поместиться в буфер. В этом случае приложение может иногда работать, а иногда - не работать, в зависимости от того, какой объём имеют cookie.

Следующие опции указывают, какой файл будет обрабатывать запросы и помогут фреймворку автоматически определить значения опций base_url и index_page.

Наконец, последняя настройка указывает nginx'у, что ждать ответа от php5-fpm не стоит больше двух минут. В пуле php5-fpm определён таймаут в 3 минуты, так что в данном случае nginx отключится от php5-fpm первым, не дождавшись срабатывания таймаута в процессе-обработчике.

Не забудьте перезагрузить или перезапустить nginx:
# /etc/init.d/nginx restart
7. CGI-приложения на Perl

Запросы к CGI-приложениям может обслуживать uwsgi с модулем uwsgi-plugin-cgi. В uwsgi нет понятия пулов и нет единственного мастер-процесса, который бы шефствовал над остальными процессами. Вместо этого можно определить несколько раздельных файлов конфигурации, у каждого из которых будет собственный мастер-процесс. Централизованное управление мастер-процессами на данный момент осуществляется при помощи единого init-скрипта.

В новых версиях uwsgi появился режим emperor, который работает полностью аналогично php5-fpm - с одним мастер-процессом. Более того - он умеет обнаруживать появление новых файлов приложений или изменения в имеющихся файлах, поддерживая состав и настройки рабочих процессов актуальными. Однако, в Debian Wheezy эта новая версия uwsgi ещё не попала, поэтому я опишу текущее положение дел.

Определим настройки приложения в файле /etc/uwsgi/apps-enabled/cgi.ini:
[uwsgi]

; Имена рабочих процессов приложения и мастер-процесса
procname = uwsgi-cgi
procname-master = uwsgi-cgi-master

; Используемый плагин и его настройки: корень с файлами, обрабатываемые расширения,
; максимальное время обработки запроса скриптом
plugins = cgi
cgi = /var/www
cgi-allowed-ext = .pl
cgi-timeout = 120

; Рабочих процессов будет четыре - можно будет одновременно обслуживать не более 4 запросов,
; а последующие запросы будут вставать в очередь до освобождения одного из рабочих процессов
processes = 4
Теперь можно перезапустить все приложения или индивидуально - только что созданное:
# /etc/init.d/uwsgi restart
# /etc/init.d/uwsgi start cgi
После запуска нового приложения uwsgi создаст Unix-сокет /var/run/uwsgi/app/cgi/socket, где cgi - имя приложения. Кроме того, будет создан журнал работы приложения в файле /var/log/uwsgi/app/cgi.log, где cgi - опять то же имя приложения.

Задаём настройки nginx, вписав в заготовку файла конфигурации ещё один фрагмент:
location = /pl/index.pl {
  uwsgi_pass unix:/var/run/uwsgi/app/cgi/socket;
  include uwsgi_params;

  # Модификатор для протокола CGI
  uwsgi_modifier1 9;
  # Скрипт, обрабатывающий запросы
  uwsgi_param SCRIPT_FILENAME /usr/local/share/pl/index.pl;
}

location /pl/static/ {
  alias /usr/local/share/pl/static/;
}
Значение опции uwsgi_modifier1 берётся из таблицы на странице The uwsgi Protocol. В данном случае используется протокол CGI, значение модификатора - 9.

Не забудьте перезагрузить или перезапустить nginx.

8. Perl с фреймворком Dancer

Для запуска PSGI-приложений под управлением uwsgi нужен файл, возвращающий объект PSGI. Для этого создадим файл приложения /usr/local/share/dancer/bin/app.psgi:
#!/usr/bin/perl

use Dancer;

...

setting apphandler => 'PSGI';

my $app = sub {
    my $env = shift;
    my $request = Dancer::Request->new(env => $env);
    Dancer->dance($request);
};
На месте многоточия располагается тело приложения, где определяются обработчики страниц.

Теперь создадим файл конфигурации веб-приложения /etc/uwsgi/apps-enabled/dancer.ini со следующим содержимым:
[uwsgi]

; Имена рабочих процессов приложения и мастер-процесса
procname = uwsgi-dancer
procname-master = uwsgi-dancer-master

; Настраиваем плагин и задаём количество рабочих процессов
chdir = /usr/local/share/dancer
plugin = psgi
psgi = /usr/local/share/dancer/bin/app.psgi
; Приложение используется редко - запустим два рабочих процесса
processes = 2
Запустим новое веб-приложение:
# /etc/init.d/uwsgi start dancer
В заготовку конфигурации nginx добавим следующий фрагмент:
location ~ ^/dancer/ {
  uwsgi_pass unix:/var/run/uwsgi/app/dancer/socket;
  include uwsgi_params;
  
  # Указываем модификатор для использования протокола PSGI
  uwsgi_modifier1 5;
}

location /dancer/static/ {
  alias /usr/local/share/dancer/static/;
}
Осталось перезапустить или перезагрузить nginx.

9. Python и приложение на Django в подкаталоге

При создании проекта Django генерируется файл wsgi.py, внутри которого создаётся WSGI-объект application. Для запуска Django-приложения через uwsgi создадим файл /etc/uwsgi/apps-enabled/dj1.ini с конфигурацией приложения:
[uwsgi]

; Имена рабочих процессов приложения и мастер-процесса
procname = uwsgi-dj1
procname-master = uwsgi-dj1-master

; В пакете на самом деле два плагина - python26 и python27, выбираем плагин python27
plugin = python27
; Каталог проекта Django
chdir = /usr/local/share/dj1
; Используем файл wsgi.py, в котором определяется WSGI-приложение application
module = wsgi:application
; Запустим шесть рабочих процессов
processes = 6
Запустим рабочие процессы приложения:
# /etc/init.d/uwsgi start dj1
А в заготовку конфигурации nginx добавим такой фрагмент:
location /dj1/ {
  uwsgi_pass unix:/var/run/uwsgi/app/dj1/socket;
  include uwsgi_params;

  # Этот модификатор используется для протокола WSGI
  uwsgi_modifier1 30;
  # Имя скрипта будет вырезаться из URL перед маршрутизацией запроса
  uwsgi_param SCRIPT_NAME /dj1;
}

location /dj1/static/ {
  alias /usr/local/share/dj1/static/;
}
Осталось перезагрузить или перезапустить nginx.

10. Python и приложение на Django в корне

Создадим файл /etc/uwsgi/apps-enabled/dj2.ini с конфигурацией приложения (здесь всё аналогично предыдущему случаю):
[uwsgi]

; Имена рабочих процессов приложения и мастер-процесса
procname = uwsgi-dj2
procname-master = uwsgi-dj2-master

; В пакете на самом деле два плагина - python26 и python27, выбираем плагин python27
plugin = python27
; Каталог проекта Django
chdir = /usr/local/share/dj2
; Используем файл wsgi.py, в котором определяется WSGI-приложение application
module = wsgi:application
; Запустим шесть рабочих процессов
processes = 6
Запустим рабочие процессы приложения:
# /etc/init.d/uwsgi start dj2
В заготовку конфигурации nginx добавляем фрагмент:
# Здесь нужно перечислить все корневые страницы или подкаталоги со страницами приложения
location ~ ^/(admin|login|logout|app)(/|$) {
  uwsgi_pass unix:/var/run/uwsgi/app/dj2/socket;
  include uwsgi_params;
}

# Каталог со статическими файлами
location /static/ {
  alias /usr/local/share/dj2/static/;
}

# Каталог со статическими файлами для админ-панели Django
location /static/admin/ {
  alias /usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/;
}
Особенность в том, что модификатор в данном случае не используется, а в первой директиве location нужно перечислить все страницы, обработку которых нужно передавать в Django-приложение. Впрочем, если кроме этого приложения на сервере больше ничего нет и не будет, можно создать секцию для обработки корневого каталога.

Осталось перезагрузить или перезапустить nginx.

11. Python и веб-фреймворк Bottle

В случае с фреймворком Bottle всё аналогично Django. Разница лишь в том, что в Bottle нет генератора проекта, а потому создать объект WSGI-приложения придётся самостоятельно. Если же все маршруты вашего проекта определены в приложении по умолчанию, то достаточно в начало головного файла добавить импорт функции app из модуля фреймворка:
from bottle import app
И затем использовать эту функцию для создания экземпляра WSGI-объекта приложения. Для этого создадим файл /etc/uwsgi/apps-enabled/bottle.ini:
[uwsgi]

; Имена рабочих процессов приложения и мастер-процесса
procname = uwsgi-bottle
procname-master = uwsgi-bottle-master

; В пакете на самом деле два плагина - python26 и python27, выбираем плагин python27
plugin = python27
; Каталог приложения
chdir = /usr/local/share/bottle
; Используем файл main.py, в котором есть функция app, возвращающая экземпляр WSGI-приложения
module = main:app()
; Задаём количество рабочих процессов
processes = 2
Запустим рабочие процессы приложения:
# /etc/init.d/uwsgi start bottle
nginx настраивается аналогично одному из примеров для приложений Django, в зависимости от того, будет ли приложение доступно из каталога или из корня URL.

12. О безопасности

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