http://habrahabr.ru/post/266473/
Предисловие
Нижеследующий текст − результат практического опыта и самообразовательных порывов человека, не имеющего систематического образования ни в одной из областей, о которых он (то есть я) берётся рассуждать. Поэтому заумные рассуждения здесь будут перемежаться банальностями. Бейте меня за первые и игнорируйте вторые. Для кого-то и они могут стать откровением.
Я постараюсь описать идеальные варианты настройки тестового веб-севера, хотя понимаю, какой бардак на них обычно творится. Буду ориентироваться на ситуацию, когда деплоить приходится часто, то есть на сервере живёт проект в стадии активной разработки либо несколько проектов на разных стадиях. Проектами занимаются разные разработчики или команды, поэтому проекты нужно изолировать друг от друга. Но сервер внутренний, поэтому такая степень изоляции и автоматизации процессов администрирования, как на серверах под сдачу в аренду, не нужна.
Основной упор я буду делать на применение разных версий Python в качестве языка поддерживаемых веб-приложений. Хотя многие вещи наверняка будут справедливы и для других языков, например, Ruby или Perl.
Взаимодействие программы на Python с веб-сервером происходит по протоколу WSGI (читается как “wiskey”), описанном в
PEP 3333 и предшествовавшем ему документе,
PEP 333.
Однозвенная архитектура
Достоинством однозвенного подхода является простота настройки и скромное потребление памяти в ущерб гибкости и, в некоторых случаях, производительности.
Самым популярным веб-сервером, работающим в таких условиях, настоящим «швейцарским армейским ножом» современного веба является Apache HTTPd (это название часто сокращают до “Apache”). Буду иметь в виду текущие версии Apache: 2.2 и 2.4.
Для подключения WSGI-приложений к Apache напрямую, без участия серверов второго звена, используется плагин (в терминологии Apache − модуль)
mod_wsgi. WSGI − это продвинутый, Python-специфичный вариант протокола
CGI. Технически интерпретатор Python (точнее, CPython) встроен в mod_wsgi как внешняя библиотека: статически или динамически − это определяется при сборке модуля. Понятно, что одновременно в mod_python может быть встроена только одна версия CPython, как и Apache может адресовать только один модуль mod_wsgi в одной инсталляции. Отсюда вытекает ограничение: в однозвенной архитектуре с mod_wsgi
мы не можем использовать на сервере одновременно Python2 и Python3.
Существенным является для нас выбор модуля мультипроцессинга. Чтобы эффективно распределять нагрузку между разными вычислительными ядрами в мультиядерных (мультипроцессорных) компьютерных конфигурациях, веб-сервер должен уметь одновременно обрабатывать несколько запросов, инициируя запуск нескольких экземпляров веб-приложения. Есть две разновидности модулей мультипроцессинга, которые выполняют вышеописанную функцию разными способами.
prefork
Упрощённо выполнение CGI-запроса веб-сервером в UNIX-подобной среде выглядит так: получив запрос, сервер порождает процесс CGI-приложения, передавая ему параметры запроса в переменных среды, а тело − в стандартном потоке ввода. Самые первые оптимизации были вызваны тем, что порождение процесса происходит достаточно долго, что увеличивает время ответа на запрос. Чтобы не тратить время на запуск CGI-программы (в нашем случае − интерпретатора Python, встроенного в mod_wsgi), достаточно запустить его заранее и держать в подвешенном состоянии, не закрывая поток ввода, до того момента, пока на сервер не придёт запрос. Если сервер может выполнять одновременно несколько потоков кода, то имеет смысл запустить несколько обработчиков CGI и следить за тем, чтобы по мере их завершения запускались новые. Количество (размер пула) работающих и ждущих обработчиков можно конфигурировать. Обработав один запрос, процесс-обработчик может принять следующий. Если же запросов стало слишком мало, сервер убьёт лишние простаивающие процессы. Также сервер убивает процессы в профилактических целях, после того, как количество обработанных ими запросов достигнет определённого (также настраиваемого) предела.
Именно так и работают модули
mod_prefork и
mod_itk. Последний для нас наиболее интересен, так как способен обслуживать каждый виртуальный хост от имени пользователя, установленного в настройках этого виртуального хоста. Но об этом чуть позже.
worker
Второй способ «списать» расходы на порождение CGI-процесса, а заодно и снизить общую нагрузку на сервер − использовать один процесс для
одновременного обслуживания нескольких запросов, иными словами − в многопоточном (multiphreaded) режиме.
Описанную стратегию реализуют модули mpm_worker и mpm_event. Последний также разгружает основную нить обработки запроса от некоторых бухгалтерских действий и пока имеет статус экспериментального.
Worker долгое время считался нестабильным и небезопасным − не сам по себе, а из-за того, что многие другие модули Apache не были приспособлены к многопоточному режиму работы или имели ошибки в его реализации. Но со временем эти проблемы были решены, и сейчас worker достаточно безопасно использовать в деле.
Надо сказать, что благодаря такой особенности CPython, как GIL, mod_wsgi достаточно легко был приспособлен к многопоточной работе, однако эта же особенность и не позволяет получить полную выгоду от многопоточности. GIL гарантирует, что в рамках одного процесса все потоки Python-кода будут исполняться последовательно квант за квантом, что создаёт только иллюзию одновременного исполнения. Но это не относится к C-коду, запускающему (или запускаемому из) Python, а разбор параметров запроса, отображение адреса на WSGI-обработчик и т. д. − всё это делается в C-коде. Соответственно, определённый выигрыш производительности от использования mpm_worker сайты, написанные на Python, всё же имеют.
Двухзвенная архитектура
Двухзвенная архитектура предполагает совместное использование веб-серверов разных уровней: внешнего (front end) и внутреннего (back end).
Внешний сервер служит посредником между пользователем и внутренними серверами при помощи “reverse proxy”-подобных протоколов и плагинов. Кроме того, внешний сервер занимается тем, для чего в 1990 году и создавался протокол HTTP: отдаёт клиенту статические файлы (assets). Хотя в последнее время эта функция в продакшн-среде часто передаётся сервисам CDN.
В качестве внешнего сервера можно использовать и Apache, но больше подходят на эту роль легковесные nginx или, в условиях ограниченных ресурсов, lighttpd. В данной роли от сервера требуются только быстрая реакция на запросы и скромность в отношении потребляемых ресурсов.
Внутренний сервер обычно зависит от архитектуры и языка реализации приложения. Python-приложения часто используют
uWSGI,
Green Unicorn или собственные серверы на основе многопоточных сетевых фреймворков типа twisted или Tornado. Многие веб-фреймворки также включают в себя веб-серверы, более или менее пригодные к работе в боевых условиях. Например, встроенный в Django
веб-сервер предназначен исключительно для отладки, и должен заменяться в продакшне чем-то более серьёзным. Встроенный веб-сервер
CherryPy, наоборот, оптимизирован под боевую нагрузку.
Особые требования к тестовому серверу (серверу CI)
Проблема прав доступа к файлам
Архитектура сети в POSIX-системах предполагает, что к портам 1-1024 имеет доступ только привилегированные пользователи. Порты 80 (HTTP) и 443 (HTTPS) попадают под это ограничение, поэтому родительский процесс веб-сервера (внешнего, если речь идёт о двухзвенной архитектуре), всегда имеет root-права.
Но CGI-скрипту нежелательно давать излишне широкие права по соображениям безопасности. Поэтому при работе со скриптами и данными веб-сервер всегда понижает привилегии до уровня обычного пользователя. Такой пользователь в Apache задаётся переменными среды APACHE_RUN_USER и APACHE_RUN_GROUP. В Debian и производных дистрибутивах этот пользователь имеет имя ‘www-data’. Для ещё большего усиления безопасности системы пользователь www-data, как и другие сервисные пользователи, не имеет оболочки и не может входить в систему.
Если веб-приложение устанавливается на сервер один раз и в дальнейшем не будет изменяться (за исключением разве что файлов, загружаемых пользователем), то логично будет зайти на сервер под пользователем с высокими привилегиями, развернуть файлы в соответствующий каталог (‘/var/www’ в Debian) и поменять владельца этих файлов:
# chown -R www-data:www-data /var/www/
Понятно, что для сервера разработки этот способ неудобен и вызывает опасения из-за постоянного использования привилегированного аккаунта. Вопрос об идеальных правах доступа к каталогам и данным веб-сервера с подробным ответом на него можно найти
здесь. Статья попала в список каноничных вопросов на Serverfault, а это кое-что значит.
Суть статьи сводится к следующему:
- если есть один ответственный за развёртывание сайта, нужно сделать его пользователем-владельцем всех файлов и каталогов сайта, а группой-владельцем сделать ‘www-data’, коды доступа же установить в 750 для каталогов (770 для каталогов, в которые пользователи сайта смогут загружать файлы) и 640 для файлов (660 для файлов, записываемых сервером). umask в стартап-скриптах нужно установить в 027,
- если к сайту должны иметь доступ несколько человек, необходимо сделать владельцем пользователя root, группой-владельцем будет группа, в которой будут состоять все разработчики, а веб-сервер получит доступ к этим файлам за счёт младших трёх битов,
- либо установить владельцем данных www-data, а группой-владельцем − группу разработчиков, но тогда нужно будет после создания каждого файла менять его владельца. Другим отвечающим упоминается возможность установить на каталоги биты suid и guid, чтобы владельцем созданного объекта становился не создатель, а владелец каталога, в котором был создан объект. Такой нестандартный способ использования этих битов был перенесён в Linux из *BSD, и я даже не уверен, сработает ли он, если файлы будут создаваться веб-сервером,
- хотя в идеальном случае группе разработчиков стоит использовать средства автоматического развёртывания (Puppet, Chef) или CI-среду (Jenkins), чтобы свести ситуацию к одному ответственному пользователю, с той лишь разницей, что этим пользователем будет бот,
- в итоге делается вывод, что полной изоляции виртуальных хостов при помощи только установки прав доступа добиться невозможно: уязвимость одного сайта даст возможность злоумышленнику как минимум прочесть код всех веб-приложений на данном сервере.
Что не упоминается в дискуссии, так это зубодробительная сложность всех этих манипуляций с chmod и chown, и, как следствие, высокая вероятность человеческих ошибок, приводящих к возникновению уязвимостей.
Всё становится намного проще и безопасней, если воспользоваться средствами разделения привилегий, имеющимися в Apache, либо перейти на двухзвенную архитектуру.
Средства разделения привилегий Apache
mpm_itk
Модуль mpm_itk даёт нам возможность самым простым и естественным способом определить, под каким пользователем будет выполняться наше веб-приложение:
<VirtualHost *:80>
<IfModule mpm_itk_module>
AssignUserID johnd developers
</IfModule>
…
</VirtualHost>
mod_suexec
Если по каким-то причинам мы вынуждены использовать другой мультипроцессорный модуль, то нам на помощь придёт
mod_suexec. Его недостатком можно считать разве что относительную сложность в настройке, но это только в том случае, если мы хотим установить его из исходников. В дистрибутивах всё автоматизировано. А использование mod_suexec сводится опять-таки к одной директиве в конфигурации виртуального хоста:
<VirtualHost *:80>
<IfModule mod_suexec.c>
SuexecUserGroup johnd developers
</IfModule>
…
</VirtualHost>
Двухзвенная архитектура на тестовом сервере
Зачастую двухзвенная архитектура применяется для оптимизации производительности сервера. Разработчиков же больше волнует удобство развёртывания веб-приложения, нежели скорость его работы. Использование двухзвенной архитектуры на тестовом сервере также имеет определённые преимущества.
К преимуществам можно отнести то, что ответственность за установку и настройку внутреннего сервера несёт разработчик, он же определяет его оптимальные параметры и способ запуска. Внутренний сервер работает с привилегиями разработчика, и вопрос об оптимальной настройке прав доступа к программным файлам и каталогам проекта больше не стоит. Логи хоста также находятся в полном распоряжении разработчика. Веб-приложение может использовать ту версию Python, которая задана при помощи virtualenv, независимо от других проектов. Всё, о чём стоит позаботиться администратору сервера − каталоги со статикой и UNIX-сокет или непривилегированный порт, через который будут разговаривать внешний и внутренний серверы.
Разработчик также ответственен за автоматический запуск сервера и его бесперебойную работу. Если первое легко решается командой “crontab -e”, то второе можно обеспечить при помощи
supervisor.
Перезагрузка веб-сервера
При использовании Apache как единственного сервера стоит добавить в настройки директиву “
MaxRequestsPerChild 1”. С ней у разработчика не будет необходимости в перезагрузке сервера. На каждый запрос будет запускаться новая инстанция интерпретатора, следовательно, все изменения в коде сайта сразу же будут отражаться в его работе.
Разумеется, запрет на вторичное использование обработчика запроса снизит производительность сервера, но иначе придётся перезагружать сервер при каждом изменении кода. Есть два способа перезагрузить Apache: WSGI-специфический и общий.
- WSGI-специфический способ заключается в обновлении даты редактирования WSGI-скрипта. Это можно сделать командой touch. Этот метод работает только тогда, когда mod_wsgi настроен на работу в daemon mode. (В общем случае крайне желательно настроить mod_wsgi на работу в daemon mode, хотя по умолчанию этот режим выключен.)
- Общий метод − команда “apachectl restart” или аналогичная − требует прав root. Разумеется, можно обойтись соответствующей настройкой sudo, но использование sudo само по себе сложнее и опаснее, чем это может показаться. Поэтому такой способ использовать нежелательно.
Если используется двухзвенная архитектура, то в перезагрузке нуждается только внутренний сервер. Он работает от имени того же пользователя, от имени которого выполняется развёртывание, следовательно, никаких проблем с его перезагрузкой (или настройкой на работу без необходимости перезагрузки) не возникает.
Выводы
Двухзвенная архитектура веб-сервера является оптимальной для тестового сервера, используемого несколькими разработчиками или командами разработчиков внутри одного предприятия. Она освободит администратора сервера от большого количества рутины, разработчикам даст больше свободы в выборе средств реализации своих проектов, позволит более эффективно использовать аппаратные ресурсы. В то же время удобство разработки иногда может достигаться ценой понижения производительности. Но даже если организация настолько стеснена в средствах, что вынуждена располагать тестовые и боевые проекты на одном сервере, безопасность и достаточно хороший уровень производительности вполне достижимы и в этом случае.