Предисловие
Нижеследующий текст ? результат практического опыта и самообразовательных порывов человека, не имеющего систематического образования ни в одной из областей, о которых он (то есть я) берётся рассуждать. Поэтому заумные рассуждения здесь будут перемежаться банальностями. Бейте меня за первые и игнорируйте вторые. Для кого-то и они могут стать откровением.
Я постараюсь описать идеальные варианты настройки тестового веб-севера, хотя понимаю, какой бардак на них обычно творится. Буду ориентироваться на ситуацию, когда деплоить приходится часто, то есть на сервере живёт проект в стадии активной разработки либо несколько проектов на разных стадиях. Проектами занимаются разные разработчики или команды, поэтому проекты нужно изолировать друг от друга. Но сервер внутренний, поэтому такая степень изоляции и автоматизации процессов администрирования, как на серверах под сдачу в аренду, не нужна.
Основной упор я буду делать на применение разных версий 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 само по себе сложнее и опаснее, чем это может показаться. Поэтому такой способ использовать нежелательно.
Если используется двухзвенная архитектура, то в перезагрузке нуждается только внутренний сервер. Он работает от имени того же пользователя, от имени которого выполняется развёртывание, следовательно, никаких проблем с его перезагрузкой (или настройкой на работу без необходимости перезагрузки) не возникает.
Выводы
Двухзвенная архитектура веб-сервера является оптимальной для тестового сервера, используемого несколькими разработчиками или командами разработчиков внутри одного предприятия. Она освободит администратора сервера от большого количества рутины, разработчикам даст больше свободы в выборе средств реализации своих проектов, позволит более эффективно использовать аппаратные ресурсы. В то же время удобство разработки иногда может достигаться ценой понижения производительности. Но даже если организация настолько стеснена в средствах, что вынуждена располагать тестовые и боевые проекты на одном сервере, безопасность и достаточно хороший уровень производительности вполне достижимы и в этом случае.
Комментарии (27)
tisov
09.09.2015 10:40+2В качестве внешнего сервера можно использовать и Apache, но больше подходят на эту роль легковесные nginx
Тогда зачем вы рассматриваете монстра apache со стариком mod_wsgi?Tanner
09.09.2015 13:24Ну, я его особо и не рассматриваю в роли фронтенда. Хотя, с другой стороны, чем не фронтенд? Монструозность Apache сильно преувеличена, зато в его конфигурацию намного проще въехать, чем в конфиг того же nginx.
tisov
09.09.2015 13:50зато в его конфигурацию намного проще въехать, чем в конфиг того же nginx.
Не согласен, вы просто с ним не разобрались, что может быть проще?
пример конфигаupstream myappbackend { server 127.0.0.1:14001 max_fails=3 fail_timeout=1s; server 127.0.0.1:14002 max_fails=3 fail_timeout=1s; server 127.0.0.1:14003 max_fails=3 fail_timeout=1s; server 127.0.0.1:14004 max_fails=3 fail_timeout=1s; } server { listen 4.5.6.7:80; server_name example.com; access_log /var/log/nginx/myapp.log main; location / { proxy_set_header Host $host; proxy_set_header X-Real-Ip $remote_addr; proxy_pass http://myappbackend/; } }
Tanner
09.09.2015 14:34что может быть проще?
Вы правы, наверное. Дело привычки.
Насчёт монструозности ? там по ссылке автор приводит зачем-то конфигурацию железа, но вообще ничего не пишет про конфигурацию Apache и nginx. Скорее всего, он действительно сравнивает nginx с mpm-prefork, на что ему и попеняли в комментариях.
Вот, на мой взгляд, несколько более корректное сравнение. В нём, по крайней мере, говорится о том, какой модуль использовался. Стабильный mpm-event и nginx идут ноздря в ноздрю.
Фреймворк ? чаще всего Django.tisov
09.09.2015 16:48Ну на вкус и цвет… У меня apache ассоциируется только почему-то с php.
У tornado, например, есть замечательный балансировщик, который сам разбрасывает запросы по инстансам и ядрам, а как будет вести себя apache — мне очень интересно, если учесть, что apache обрабатывает каждый запрос в отдельном процессе/потоке, в отличие от nginx (поправьте если не прав).
Использование apache только как прокси для python-приложения скажется на производительности, — на пиках вы это увидите. В nginx вы эту проблему легко решите масштабированием (как простой пример, дополнительными upstream'ами на других серверах).
Tanner
09.09.2015 19:02если учесть, что apache обрабатывает каждый запрос в отдельном процессе/потоке
Уже нет. Как сейчас помню:
- в версии 2.0 код, отвечающий за запуск обработчиков запросов, выделили в отдельные модули (mpm_*),
- в 2.2 появился мультипотоковый обработчик mpm_worker,
- в 2.4 стабилизировался mpm_event, который на том графике внизу, рядом с nginx.
tisov
09.09.2015 19:25Ваша ссылочка на Performance от 2012 года, да и моя не надолго от вас ушла.
Свежих тестов не нашел, самому гонять — не до этого, да и уже не мой профиль.Так что останемся при своих, на днях еще буду шерстить, если что сюда отпишусь.
Для затравки http://habrahabr.ru/post/210950/
В любом случае, спасибо за стью, плюс.
JC_Piligrim
09.09.2015 16:57+3А не проще ли будет взять какой-нибудь Docker? Тогда и окружение будет стандартизированным везде, и на dev и на prod.
Tanner
09.09.2015 19:11Docker, как я понимаю, оперирует содержимым контейнеров LXC или OpenVZ? Возможно, стоит попробовать. Либо написать свои скрипты для деплоя в контейнеры.
Но беспокоит вопрос использования дискового пространства. Серверные диски желательно экономить.kvasdopil
10.09.2015 11:04Докер использует copy-on-write на файловых системах, можно развернуть 100 одинаковых контейнеров и места они будут занимать как один.
romangoward
Тестовый стенд не должен быть идеальным, он должен архитектурно соответствовать продакту. А маленький срачЪ в qa env — всегда соответствует большой «помойке» на проде.
Да и вообще, вы всю дорогу тёплое и мягкое путаете.
Если есть несколько независимых команд, то нужно поставть балансировщик, прописать upstream's, и для особо буйных команд сливать реквесты а) в контейнеры или б) на выделенные хосты, а уже там должны сидеть ваши *cgi и ci-agent, да билдить и управлять всей хурмой из vcs's.
И отдельным пунктом хотелось бы отметить, что virtualenv дальше компа разработчика уходить не должен. Надо стороннюю библиотеку на сервере — соберите пакет и используйте локальную репу для деплоя, а с setuptools, pip и gcc обращаться надо осторожно, а лучше вообще не дружить.
Tanner
Про контейнеры понимаю, но вот про
как-то первый раз слышу. И вообще, часто встречаю virtualenv в продакшне, и мне он там вполне нравится.
Не могли бы вы обосновать этот пункт поподробнее или ссылочкой кинуть?
chemistmail
А теперь добавьте туда ruby с его менеджером пакетов, nodejs, и еще пяток интерпретаторов, накатите по 10 — 15 пакетов в каждом, а потом попробуйте понять, что у вас лежит на сервере. И как его собрать заного. Все что запускается на сервере должно устанавливаться из пакетов, иначе вы получаете помойку, которая не может поддерживаться без вечно устаревающей документации и человека обладающего божественным знанием как его собрать.
Tanner
Не понимаю, чем знание
божественнее, чем, скажем
А если к этому добавить ещё и сборку в пакеты всех библиотек, которые есть в PyPy/Registry, но нет в репо целевой ОС (целевых ОС), управление частным репо, управление ключами… Оно действительно вам надо?
romangoward
Разница в том, что с dpkg вы используете бинарные пакеты, собранные один раз в правильном окружении, а pip install вызывает gcc и билдит модули при каждом деплое.
Кейсов тут несколько:
1. Если вы захардкорили версии зависимостей во избежании dependencies hell, то никто вам не гарантирует, что данный конкретный пакет будет доступен всегда. В один их тех самых доджливых вечеров в четверг, pip вам скажет, что нужного пакета нет.
2. Вы уверены, что все -devel файлы, которые используюся при компиляции — идентичны? И что unexpected behaviour на callback внешней функции в продакте не настигнет вас через 2 месяца после того самого дождливого четверга, а вы как на зло с семьей посредине средиземного моря на лайнере?
3. Компилятора на сервере быть не должно.
Да, поддерживать все это в приличном состоянии можно, стoит это неимоверных усилий, но моментально рассыпается под шаловливыми руками отдельно взятых личностей. Такие дела, и всё это уже сто раз проходили в связке perl + cpan.
Tanner
pip install может не вызывать gcc каждый раз, а ставить пакеты из wheel-кэша. Wheels можно собирать и на другом сервере.
Хотя я ещё не настолько параноик, чтобы удалять gcc с сервера. Я вообще не помню ни одной массово эксплуатируемой уязвимости с участием gcc.
ANtlord
А dpkg не скажет? Pip хотя бы может тянуть из репа, который может быть вашим, и расположен в том месте, за которое можете сами ручаться.
Компилятора на сервере быть не должно из-за того, что злоумышленник придет и скомпилирует что-нибудь? Можно и статические библиотеки принести.
chemistmail
А потом вам понадобится срочно еще какая-то либа на racket, для какого-то скрипта, немножко php что в пакетах не было, и еще что-нибудь, главное чтоб у него свой менеджер пакетов был, а может и инсталлятор в виде скрипта, в хомяк аль в opt положит мину.
В документацию записать забудут сей факт, человечек который это сделал станет не доступен.
И однажды вам понадобится развернуть сервис на новом железе (и очень срочно).
Развернете вы (debian | red-hat | sled), накатите пакеты менеджером, потом pip, потом bower, в общем все по доке.
Вот только не получится каменный цветочек. А начальство шумит, пальчиком грозится. И в думу вы погрузитесь, печальную. ))
И не вспоминайте про chef, puppet, salt, это по сути один из способов документирования ваших сервисов.
Tanner
Решение этой проблемы, если я вас правильно понял ? создать собственный deb-репозиторий и добиться того, чтобы проект можно было развернуть только при помощи этого репозитория, без использования иных пакетных менеджеров, кроме apt?
Спасибо, но я с тем же успехом упакую проект целиком, со всеми незадокументированными вовремя какашками, в wheel. Он гарантированно развернётся. Правда, обновления безопасности для компонентов я не получу. Но это в любом случае требует разработчика. Просто wheel-репозиторий требует Python-разработчика, а переупаковка всего проекта или библиотек, которых нет в системном репо или которые в нём устарели, в deb-пакеты требует Python-разработчика, умеющего dpkg-buildpackage и прочую чорную магию. Либо выделить отдельную человекоединицу для упаковки пакетов, а этого далеко не всякий бюджет выдержит. Проще обойтись толковым пайтонистом кмк.
Правда, тут ниже мне уже советуют использовать Docker. Я так понимаю, что установив проект со всеми его сюрпризами в контейнер LXC, можно при помощи Docker сохранить снимок этого контейнера и деплоить проект из этого снимка. Я не знаю подводных камней этого процесса, но выглядит всяко проще, чем создавать deb-пакет на каждый чих.
chemistmail
Не люблю магию, github.com/jordansissel/fpm, нарисуйте скрипты чтоб билдить все что вам нужно, привяжите их к бил серверу, артефакты автоматом на репозиторий. Добавьте puppet, salt, chef, bash, docker и тд для развертывания инфраструктуры целиком.
Как то так. Это в общем одна из задач системного администратора обеспечивающего поддержку разработки.
Менеджеров пакетов под разные языки куча, бывает что сервис работает, написан например на erlang, а разработчика нет. rebar, pecl, pip, lein, gem, cabal, stack и тд. С тем же pip я последний раз сталкивался 2 года назад, с rebar 4 года назад, lein вчера, gem пару месяцев назад, cabal — неделю назад. У каждого языка своя инфраструктура, вы их все тащите на прод? В общем это дело хозяйское, но мой опыт в администрировании склоняет меня в сторону пакетирования. Это потом сильно проще, и обеспечивает заменяемость людей.
Tanner
Спасибо за наводку, не знал про эту штуку. Надо будет иметь в виду. Наверное, я изменю своё мнение насчёт сборки OS-targeted пакетов.
chemistmail
туда же fpm-cookery.readthedocs.org/en/latest
aim
Про virtualenv — согласен. Но на дворе 2015, девелоперы привыкли, а у OPs появился докер. Это снимает проблему «чёрт знает что у нас требует хрен знает чего».