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

    Здрасти. Долго-долго я присматривался к замечательному фреймворку 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-соединения, они не должны так себя вести. Но даже этот минус не такой страшный, потому что, как я уже говорил, инджиникс замечательный сервер. Он разделяет запросы на тяжелые и легкие, и если даже у вас в очереди окажется много тяжелых запросов, то при освобождении хотябы одного воркера, он сначала выплюнет все легкие запросы, и только потом примется за тяжелый. Иными словами, легкие запросы всегда приоритетнее тяжелых, в каком бы порядке они не поступали.
    Поделиться публикацией
    Комментарии 24
      0
      спасибо за статью. попробую обязательно. однако лично я ставлю под сомнение целесообразность установки на боевую машину модуль который не обновлялся более года. неужели за этот год не выпустили ничего аналогичного?
      • НЛО прилетело и опубликовало эту надпись здесь
        0
        А чем не устраивает стандартный способ — mod_wsgi к апачу + когда понадобится — ngnix в качестве фронт-энда\для статики? Оверхед в случае апача небольшой, если вообще есть (по сути — это 1 лишний родительский процесс и все).
        Ведь треды-процессы mod_wsgi апачевского — независимые, и апача самого в себе не содержат.

        А тут непонятно, как обходить эту самую «ложку дегтя».
          0
          Апач много памяти сам по себе жрет, на всяких VDS это может очень сказаться.
            0
            Сейчас глянул к себе на VDS — процесс апача, который обслуживает сайт, к которому никто не ходит, занимает 6 метров RES-памяти. Это, собственно, все лишние расходы на 1 сайт. Я ничего хитро не компилировал и т.д., просто оставил только нужные модули (mod_wsgi, mod_fastcgi, еще по мелочи).
              +1
              Сколько точно?
              0
              Сколько-то памяти лишней апачь все равно скушает, настройки требует, лишняя сущность как ни как.
              А насчет ложки дегтя — при любой настройке есть 2 варианта — либо мы не ограничиваем количество приложений и при нагрузке они плодятся, пожирая память и уводя систему в даун. Либо мы ограничиваем количество количество приложений и они встают в очередь. Только тут мы получаем в нагрузку неприятный эффект — небольшую задержку пока не освободиться хотябы один воркер. Да только в любом случае следующую порцию статики инджиникс отдаст прежде чем возмется за следующую динамику.
                +3
                1. Ну насчет настройки, с одной стороны, все верно, лишняя сущность, но с другой — чего там в апаче настраивать-то, по сравнению с предложенным способом)

                2. Теперь насчет памяти. В случае с процессами-воркерами ngnix, как я понимаю (поправьте, если не так), в каждый процесс будет грузиться как минимум интерпретатор питона. Или нет? Апачевский mod_wsgi работает с shared-версией питоновских библиотек, в итоге каждый его процесс представляет из себя 250 килобайт собственно кода mod_wsgi + загруженное приложение (сайт на джанге, например). Я так себе представляю. Так что и потребление памяти еще посчитать стоит, где больше.

                3. Насчет ложки дегтя. Если на VDS том же крутятся несколько сайтов, то выгоднее всем ограничить число плодящихся процессов, а изначально запускать их меньше, чем это макс. ограничение, так что сайты смогут переживать пиковые нагрузки с одной стороны, и не кушать много памяти без надобности с другой. Апачевский mod_wsgi в этом плане довольно гибок — может использовать как апачевские модели порождения процессов и тредов, так и свои. Если ngnix'овые воркеры тоже умеют запускаться по необходимости, то все отлично. А если нет, то вот что выходит: сам сайт на джанге ест в большинстве случаев памяти больше, чем выражение (размер процесса апача — размер процесса ngnix), поэтому даже одна лишняя копия сайта в памяти может убить все преимущество ngnix.

                4. Еще раз, насчет памяти, которую скушает апач. Это скорее штука психологическая. Ведь все знают, что апач жрет кучу памяти. Но лучше знать цифры. Если критично 6 (или сколько там) мегабайт на 1 сайт (не на 1 процесс, на сайт) — то да, есть смысл искать что-то другое. Как мне кажется, это критично только если хостится куча сайтов на одном VDS или сервере (кстати, тут см. п3, про ложку дегтя). А для одного сайта с высокой нагрузкой это не критично совершенно, тут важнее надежность и гибкость апачевского mod_wsgi, а так же то, что накладные расходы на процесс/тред минимальны.

                Все это ни в коем случае не умаляет важность и полезность статьи, полезно знать, как настроить mod_wsgi ngnix-овский, просто важно обозначить, зачем и когда это нужно.
                  0
                  Про психологический фактор, это вы верно сказали.

                  В «похожих публикациях» есть две статьи, которые я очень внимательно изучал до моих экспериментов. В первой автор рассказывает, как в условиях VDS, экономя память, сначала отказался от mod_python в пользу apache+mod_wsgi, а во второй, как отказался от apache в пользу проксирования сетнд-элон сервера на питоне. Хотя да, я сам еще не пробовал mod_wsgi для апача, возможно его и можно оттюнинговать. Теперь обязательно этим займусь.
                    +2
                    Прочитал обе статьи.

                    Там неясно, с какими параметрами запускалось все в обоих случаях, так что прямое сравнение бессмысленно проводить. Похоже, в mod_wsgi было 15 тредов на сайт, а с fapsw — 1 процесс на сайт, естественно сайт в первом случае занимал раз в 15 больше памяти.

                    Запустили бы mod_wsgi с одним процессом и 1 тредом, а статику отдавали бы ngnix'ом — было бы сравнение mod_wsgi и fapsw на одних настройках. И неизвестно, что бы оказалось лучше. В любом случае, разница была бы крошечная, думаю.

                    А что касается производительности — упирается все обычно уж точно не в способ организации wsgi-взаимодействия. Все эти тесты про 10000 запросов в секунду vs 1000 запросов в секунду имеют значение только для статики, в случае приложения, которое что-то делает, коннектится к базе той же, это просто абсолютно незаметная мелочь.

                    Миф о раздутости апача, думаю, во многом происходит от mod_php. Когда в каждый процесс апача, отдающий статику, встраивается такая бандура и быстро сжирает всю память, а потом запускаешь все на ngnix+fastcgi и радуешься жизни, осадочек-то остается.
              0
              Интересно, но пока только добавил в закладки. Сам использую fastcgi, который запускается по средством супервизора(ИМХО удобно рулить бэкэндами, когда их много)
                +1
                Я б не доверил вам боевой сервер.
                  +3
                  Да, надо ж объяснять почему, а то заминусуют…

                  Вот так вот, с бухты-барахты, теоретически пораскинув мозгами, даже не попытавшись исследовать проблему, решаем развернуть именно nginx и именно с mod_wsgi. Нету сборки под нашу систему — ничего страшного. Лёгким движением ./configure && make && make install мы превращаем любой дистрибутив в слакварь. Как обновлять сервер, как следить за целостностью пакетов — даже предположений нету. Пофиг, это же Б.О.Е.В.О.Й. сервер, зачем нам эти выдумки?

                  Рассуждения про очередь и tcp-коннекты… Я даже не знаю, как это комментировать…

                  > username ALL=NOPASSWD:/bin/kill -HUP *

                  Вот за эту строчку, я бы выдернул руки.

                  P.S. Пля, чё за глюки с комментами…
                    +2
                    Есть 2 типа серверов: тестовый и боевой. Боевой не означает, что он работает в стойке яндекса под нагрузкой 100 хитов в секунду. Просто он не тестовый.

                    Я отлично понимаю, что есть люди, которые лучше меня разбираются в чем угодно. Возможно через неделю я осознаю, что mod_wsgi + nginx вообще не вариант (возможно и нет). Однако же, я достаточно много материала прочел о различных способах запуска web-приложений на питоне и оптимальным мне пока показался именно этот вариант. Но к своему удивлению я не нашел каких-то объемных материалов по этой теме, а проблем, оказалась, уйма.

                    Насчет строки в sudoers я согласен, сейчас уберу из статьи.
                    –2
                    Официальная страница mod_wsgi, как я понял, здесь. И именно здесь нас постигает главное разочарование — проект не обновлялся больше года.

                    Официальная страница на code.google.com/p/modwsgi, и проект активно разрабатывается.
                      +2
                      mod_wsgi для apache и для nginx — это два совершенно разных проекта.
                        +2
                        Да, что-то я не подумал.
                        0
                        это для апача
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Статья полезная, но к Питону как языку программирования имеет весьма косвенное отношение.

                          Есть же блоги Django, Nginx, Web-разработка. Там она будет более логично смотреться.
                            0
                            Спасибо за статью. Успешно поднял.
                            Уточню, что для компиляции nginx версии 0.7.60 и выше в файле ngx_http_wsgi_handler.c необходимо
                            заменить строку:
                            rc = ngx_http_discard_body(r );
                            на:
                            rc = ngx_http_discard_request_body(r );
                            Перед ней еще комент соответствующий /* XXX not sure */ :)
                              0
                              Отличная статья.
                              Единственное, но: При нескольких приложениях запущенными под разыми server_name на одном сервере вылазил странный глюк, запускалось только то приложение к которому производился запрос, оно и отвечало на остальных именах.
                              Исправилось переносом include wsgi_params; из location в секцию http.
                                0
                                Если кто-то это ещё будет читать как и я сейчас, то пусть знают, что mod_wsgi для nginx таки недавно обновился и был переименован ngx_http_wsgi_module
                                  0
                                  опечатка " Кто-то должен следать за тем "

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое