Pull to refresh

Разворачиваем nginx + mod_wsgi на сервере

Python *
Здрасти. Долго-долго я присматривался к замечательному фреймворку django, читал книгу, изучал статьи, пробовал писать hello world'ы (со встроенным в джангу сервером это было легко и приятно). А вчера я попробовал настроить от начала до конца боевой сервер, и как оказалось, это не так просто, и мне даже показалось, что будь я моложе и неопытнее, я бы плюнул на это дело. Вот я и решил поделиться с читателями полной инструкцией, снабдив её некоторыми рассуждениями и конфигами. Статья расчитана на начинающих, но будет интересно всем, обещаю.


Почему wsgi и nginx?


На это есть несколько причин. Во первых, решение должно быть быстрым, кушать мало памяти и быть устойчивым. Nginx создавался чтобы быть таким. К тому же по тестам производительности, именно связка nginx + wsgi дает меньший расход памяти, и высокую отказоустойчивость под большими нагрузками. Во вторых, известно, что чем более простая система, тем она надежнее. Если мы сравниваем wsgi с fastcgi, про последний можно сказать, что это отдельный сервер, на котором крутится само приложение. Кто-то должен следать за тем, чтобы этот сервер не падал. wsgi же представляет из себя вызов python скриптов из приложения на С (коим является nginx), поэтому один раз настроив интерфейс мы уменьшаем количество сущностей вдвое и дальше общаемся только с веб-сервером.

Кроме того, у меня есть предположение, что wsgi будет в целом быстрее для конечного пользователя, правда они основаны только на теоретическом рассуждении: допустим что к нашему серверу подконектилось одновременно 50 пользователей и запросили одинаково тяжелые страницы. В случае fastcgi nginx сделает сразу 50 запросов к fastcgi серверу и будет ждать ответа. Чисто теоретически одинаковые запросы, пришедшие одновременно, наплодят кучу запущенных приложений, будут равноценно конкурировать за процессорное время и выполнятся все в один момент (если предположить что число запущенных fastcgi приложений ничем не ограничено). Запросы, пришедшие к wsgi, nginx будет обрабатывать сам, и так как максимальное количество одновременно выполняемых запросов равно количеству воркеров, они будут становиться в очередь и первые ответы пользователи получат практически сразу. Но последние придут за то же время, что и в случае fastcgi, тут уж никуда не деться.

От теории к практике


Самая скучная часть, будем собирать nginx. Собирать его нужно потому, что другого способа подключить mod_wsgi просто нет, так что стяните с сайта Игоря Сысоева последнюю стабильную версию (сейчас 0.7.60) и распакуйте куда нибудь.
Официальная страница mod_wsgi, как я понял, здесь. И именно здесь нас постигает главное разочарование — проект не обновлялся больше года. Немного придется поправить исходники для совместимости с текущей версией nginx, но ничего, главное что всё должно завестись. Качаем последнюю версию модуля и тоже распаковываем куданибудь неподалеку от инджиникса.

Что конкретно нужно править:
1) В самом архиве с mod_wsgi в папке patches лежит патч nginx-0.6.x.patch, который патчит конфиг и ngx_http_wsgi_module.c На кончиг можно не обращать внимание, он нам не понадобится.
2) Так как когда был последний апдейт mod_wsgi, текущей версии инджиникса еще не было, то официального патча не хватит. Поиски по другим ошибкам компиляции навели меня только на этот сайт. Японского языка я не знаю, но этого и не потребовалось, прямо на странице лежат необходимые патчи. Правда номера строк в этом патче у меня не совпали с таковыми в исходниках, и мне пришлось вручную править:

В файл src/ngx_http_wsgi_module.c объявляем гденибудь ближе к началу файла структуру:

static ngx_path_init_t ngx_http_wsgi_temp_path = {
    ngx_string(NGX_HTTP_WSGI_TEMP_PATH), { 1, 2, 0 }
};


Там же первый вызов функции ngx_garbage_collector_temp_handler заменяем на NULL.

Вызов функции

ngx_conf_merge_path_value(conf->temp_path,
        prev->temp_path,
        NGX_HTTP_WSGI_TEMP_PATH, 1, 2, 0,
        ngx_garbage_collector_temp_handler, cf);

меняем на
ngx_conf_merge_path_value(cf, &conf->temp_path,
        prev->temp_path,
        &ngx_http_wsgi_temp_path);


Забегая вперед, скажу, что в последствии придется еще вернуться к исходникам mod_wsgi и кое что в них поправить, но в принципе они уже сейчас готовы к сборке.

Перейдите в каталог с исходниками инджиникса и запустите
./configure --add-module=/path/to/mod_wsgi/

Здесь у меня случился небольшой затык. Дело в том, что с тех пор, как с инджиниксом стал поставляться модуль http_cache, он стал требовать openSSL. Я долго искал пакет openssl-dev и уже даже хотел собирать без этого модуля, но потом пакет нашелся и назывался он просто ssl-dev. Кстати, в описании ошибки есть ошибка, параметр, отключающий http_cache не «--without-http_cache», а «--without-http-cache».

Здесь я сделал make & make install.

Конфигурация nginx


Самое первое, что нужно исправить в nginx.conf — количество воркеров. Наверное вы слышали, что обычно оптимальное количество воркеров равно количеству ядер на машине. При использовании mod_wsgi дело немного меняется. Дело в том, что из-за того, что приложения работают в контексте этих самых воркеров, количество одновременно запущеных процессов равно количеству воркеров. При идеальных условиях количество воркеров по количеству ядер грузило бы процессор ровно на 100%. Но условия не идеальны и выполнение приложений иногда прерывается. Например при выполнении дисковых операций или запросе к базе данных, если сервер базы на другой машине. Так что мне кажется, оптимальное число воркеров равно удвоенному числу ядер.

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

location / {
        wsgi_pass /path/to/nginx/django.py;
}

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

location / {
        root           /path/to/www/;
        error_page 404 = @backend;
        log_not_found  off;
}
location = / {
        #Корень сервера
        wsgi_pass      /path/to/nginx/django.py;
        include        wsgi_params;
}
location @backend {
        wsgi_pass      /path/to/nginx/django.py;
        include        wsgi_params;
}


Здесь используется 2 файла, которые тоже нужно будет создать:

Отсюда берем wsgi_params:

wsgi_var REQUEST_METHOD $request_method;
wsgi_var QUERY_STRING $query_string;

wsgi_var CONTENT_TYPE $content_type;
wsgi_var CONTENT_LENGTH $content_length;

wsgi_var SERVER_NAME $server_name;
wsgi_var SERVER_PORT $server_port;

wsgi_var SERVER_PROTOCOL $server_protocol;

wsgi_var REQUEST_URI $request_uri;
wsgi_var DOCUMENT_URI $document_uri;
wsgi_var DOCUMENT_ROOT $document_root;

wsgi_var SERVER_SOFTWARE $nginx_version;

wsgi_var REMOTE_ADDR $remote_addr;
wsgi_var REMOTE_PORT $remote_port;
wsgi_var SERVER_ADDR $server_addr;

wsgi_var REMOTE_USER $remote_user;


А из официальной документации django.py:

import os, sys
# место, где лежит проект
sys.path.append('/path/to/')
# файл конфигурации проекта
os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()


Стоит отметить, что в DJANGO_SETTINGS_MODULE указывается не имя файла, а имя модуля, т.е. для приведенного примера реальное расположение файла конфигурации должно быть «/path/to/project/settings.py»

Большой облом


Если я ничего не упустил, то все должно быть готово к запуску инджиникса. Запускаем браузер, идем на httр://localhost/ (или чего там у вас) и видим приветственную страницу джанги, если проект новый, как у меня, или главную страницу, если уже что-то написано. Честно говоря я уже думал, что самые большие трудности позади, но не тут то было. Как только я набрал httр://localhost/1 (или любой другой url не из корня) я получил пятисотую инджиникса. Понятно, что это страшнее, чем пятисотая джанги. В логах значилась запись, мол функции PyString_FromStringAndSize передан отрицательный второй аргумент (длина строки).
После разборок с исходниками пришел к выводу, что причина в том, что модуль пытается убрать из начала строки path_info имя локейшена в обработчик которого попал url. Т.е. в нашем случае все страницы попадают в location backend и именно 8 символов модуль пытается вычесть из path_info, в которой столько просто нет. В случае, когда столько символов в url находилось, мы получали урезанный с начала путь, т.е. вместо httр://localhost/123456789 получали httр://localhost89
В комментарии стоит TODO «we should check that clcf->name is in r->uri», т.е. «мы должны проверить, а есть ли вообще название этого локейшена в строке url.
В принципе, мне понятно, для чего так было сделано, например можно было определить

location /django {
        wsgi_pass …
}


и url вида httр://localhost/django/foo/bar/ транслировались бы для джанги в httр://localhost/foo/bar/. Но не понятно, почему автор не учел что url могут быть регекспами и вообще ссылками (как в нашем случае). Я решил, что такой глючный функционал мне не нужен, тем более что если бы он понадобился, его можно сделать в django, переписав urls.py c include.

В общем нужно исправить в ngx_wsgi_run.c строку 584:

if (clcf->name.len == 1 && clcf->name.data[0] == '/') {
if (1 || (clcf->name.len == 1 && clcf->name.data[0] == '/')) {

после чего еще раз сделать make & make install, предварительно погасив работающий инджиникс.

Перезапуск сервера


Python — это не php. Приложение, будучи единожды загруженным отделяется от своего исходника и ничего об его изменении не знает. Поэтому нам придется самим перезапускать время от времени инджиникс, при обновлении кода. К счастью, инджиникс очень хороший сервер, нам не придется писать скрипт, который бы убивал его и запускал снова, отключая всех клиентов. В нем предусмотрена возможность перезапустить воркеры мягко, без мгновенного отключения клиентов, которых они обслуживали. Сделать это можно, передав сигнал HUP головному процессу. Поэтому все, что нам нужно — это написать простенький скрипт, назвав его, скажем restart-server:

sudo kill -HUP `cat /usr/local/nginx/logs/nginx.pid`

Каждый раз при запуске этого скрипта, приложене будет перезапускаться. Кроме того, я бы очень рекомендовал повесить этот скрипт на крон, скажем раз в час. На производительности это сильно не отразиться, а вот памяти может сэкономить, если скрипты начнут течь.

Ложка дегтя


К сожалению, у получившегося сервера есть Ахилесова пята. Если вдруг так получится, что 10 человек решат загрузить какуюнибудь тормозную страничку одновременно (скажем генерация pdf или пережатие аваторки), и у вас будет только 10 воркеров, то ни один другой запрос (даже самый легкий, например картинка gif) не будет обработан, пока не освободиться хотя-бы один воркер. я к сожалению не тестировал другие способы связи, возможно такой же эффект будет при работе с fastcgi или http_pass, но мне кажется, раз там используются отдельные tcp-соединения, они не должны так себя вести. Но даже этот минус не такой страшный, потому что, как я уже говорил, инджиникс замечательный сервер. Он разделяет запросы на тяжелые и легкие, и если даже у вас в очереди окажется много тяжелых запросов, то при освобождении хотябы одного воркера, он сначала выплюнет все легкие запросы, и только потом примется за тяжелый. Иными словами, легкие запросы всегда приоритетнее тяжелых, в каком бы порядке они не поступали.
Tags:
Hubs:
Total votes 41: ↑38 and ↓3 +35
Views 28K
Comments Comments 24