В этой статье я постараюсь максимально широко изложить схемы работы веб-серверов. Это поможет выбрать сервер или решать, какая архитектура быстрее, не основываясь на часто необъективных бенчмарках.
В общем - статья представляет собой глобальный обзор "что бывает". Без циферок.
Статья написана на основе опыта работы с серверами:
- Apache, Lighttpd, Nginx (на C)
- Tomcat, Jetty (на Java)
- Twisted (Python)
- Erlang OTP (язык Erlang)
- и операционными системами Linux, FreeBSD
Тем не менее, принципы достаточно общие, поэтому должны распространяться в каком-то виде на OS Windows, Solaris, и на большое количество других веб-серверов.
Цель веб-сервера
Цель веб-сервера проста - обслуживать одновременно большое количество клиентов, максимально эффективно используя hardware.
Как это сделать - в этом основная заморочка и предмет статьи ;)
Работа с соединениями
С чего начинается обработка запроса? Очевидно - с приема соединения от пользователя.
Для этого в разных OS используются разные системные вызовы. Наиболее известный и медленный на большом количестве соединений - select. Более эффективные - poll, kpoll, epoll.
Современные веб-серверы постепенно отказываются от select.
Оптимизации ОС
Еще до приема соединения возможны оптимизации на уровне ядра ОС. Например, ядро ОС, получив соединение, может не беспокоить веб-сервер, пока не произошло одно из событий.
- пока не пришли данные (dataready)
- пока не пришел целиком HTTP-запрос (httpready)
На момент написания статьи оба способа поддерживаются во FreeBSD (ACCEPT_FILTER_HTTP, ACCEPT_FITER_DATA), и только первый - в Linux (TCP_DEFER_ACCEPT).
Эти оптимизации позволяют серверу меньше времени простаивать в ожидании данных, повышая таким образом общую эффективность работы.
Соединение принято
Итак, соединение принято. Теперь на плечи сервера ложится основная задача - обработать запрос и отослать ответ посетителю. Будем здесь рассматривать только динамические запросы, существенно более сложные, чем отдача картинок.
Во всех серверах используется асинхронный подход.
Он заключается в том, что обработка запроса спихивается куда-нибудь "налево" - отдается на выполнение вспомогательному процессу/потоку, а сервер продолжает работать и принимать-отдавать на выполнение все новые соединения.
В зависимости от реализации - процесс-помощник ("worker") может пересылать результат обратно серверу целиком (для последующей отдачи клиенту), может передавать серверу только дескриптор результата (без копирования), или может отдавать результат клиенту сам.
Основные стратегии работы с worker'ами
Работа с воркерами состоит из нескольких элементов, которые можно по-разному комбинировать и получать разный результат.
Тип worker'а
Основных типов два - это процесс и поток. Для улучшения производительности иногда используют оба типа одновременно, порождая несколько процессов и кучу потоков в каждом.
Процесс
Различные worker'ы могут быть процессами. В этом случае они не взаимодействуют между собой, и данные различных worker'а полностью независимы друг от друга.
Поток
Потоки, в отличие от процессов, имеют общие, разделяемые структуры данных. В коде worker'а должна быть реализована синхронизация доступа, чтобы одновременная запись одной и той же структуры не привела к хаосу.
Адресное пространство
Каждый процесс, в том числе и сервер, обладает своим адресным пространством, которое использует для разделения данных.
Внутри сервера
При работе внутри сервера - worker имеет доступ к данным сервера. Он может поменять любые структуры и делать разные гадости, особенно если написан с ошибками.
Плюсом является отсутствие пересылки данных из одного адресного пространства в другое.
Снаружи сервера
Worker может быть запущен вообще независимо от сервера и принимать данные на обработку по специальному протоколу (например FastCGI).
Конечно, этот вариант - самый безопасный для сервера. Но требует дополнительной работы по пересылке запроса - результата между сервером и worker'ом.
Рождение worker'ов
Чтобы обрабатывать много соединений одновременно - нужно иметь достаточное количество рабочих.
Основных стратегий - две.
Статика
Количество рабочих может быть жестко фиксированно. Например, 20 рабочих процессов всего. Если же все рабочие заняты и приходит 21й запрос - сервер выдает код Temporary Unavailable - "временно недоступен".
Динамика
Для более гибкого управления ресурсами - рабочие могут порождаться динамически, в зависимости от загрузки. Алгоритм порождения рабочих может быть параметризован, например (Apache pre-fork), так:
- Минимальное количество свободных рабочих = 5
- Максимальное количество свободных рабочих = 20
- Всего рабочих не более = 30
- Начальное количество рабочих = 10
Чистка между запросами
Рабочие могут либо заново инициализовать себя между запросами, либо - просто обрабатывать запросы один за другим.
Чистый
Перед каждым запросом очищается от того, что было раньше, чистит внутренние переменные и пр.
В результате нет проблем и ошибок, связанных с использованием переменных, оставшихся от старого запроса.
Персистентный
Никакой очистки состояния. В результате - экономия ресурсов.
Разбор типичных конфигураций
Посмотрим, как эти комбинации работают на примере различных серверов.
Apache (pre-fork MPM) + mod_php
Для обработки динамических запросов используется модуль php, работающий в контексте сервера.
- Процесс
- Внутри сервера
- Динамика
- Чистый
Apache (worker MPM) + mod_php
Для обработки динамических запросов используется модуль php, работающий в контексте сервера.
При этом, так как php работает в адресном пространстве сервера, разделяемые потоками данные периодически портятся, поэтому связка нестабильна и не рекомендована. Это происходит из-за ошибок в mod_php, который включает в себя ядро PHP и различные php-модули.
Ошибка в модуле, благодаря одному адресному пространству, может повалить весь сервер.
- Поток
- Внутри сервера
- Динамика
- Чистый
Apache (event mpm) + mod_php
Event MPM - это стратегия работы с worker'ами, которую использует только Apache. Все - точно так же, как с обычными потоками, но с небольшим дополнением для обработки Keep-Alive
Установка Keep-Alive служит для того, чтобы клиент мог прислать много запросов в одном соединении. Например, получить веб-страницу и 20 картинок.
Обычно, worker заканчивает обработку запроса - и ждет какое-то время (keep-alive time), не последуют ли в этом соединении дополнительные запросы. То есть, просто висит в памяти.
Event MPM создает дополнительный поток, который берет на себя ожидание всех Keep-Alive запросов, освобождая рабочего для других полезных дел. В результате, общее количество worker'ов значительно сокращается, т.к никто теперь не ждет клиентов, а все работают.
- Поток
- Внутри сервера
- Динамика
- Чистый
Apache + mod_perl
Особенность связки Apache с mod_perl - в возможности вызывать Perl-процедуры по ходу обработки запроса апачем.
Благодаря тому, что mod_perl работает в одном адресном пространстве с сервером - он может регистрировать свои процедуры через Apache hooks,
на разных стадиях работы сервера.
Например, можно работать на той же стадии, что и mod_rewrite, переписывая урл в хуке PerlTransHandler.
Следующий пример описывает rewrite с /example на /passed, но на перле.
# в конфиге апача при включенном mod_perl
PerlModule MyPackage::Example
PerlTransHandler MyPackage::Example
# в файле MyPackage/Example.pm
package MyPackage::Example
use Apache::Constants qw(DECLINED);
use strict;
sub handler {
my $r = shift;
$r->uri('/passed') if $r->uri == '/example'
return DECLINED;
}
1;
К сожалению, mod_perl - весьма тяжелый сам по себе, поэтому использование его лишь реврайтов - весьма накладно.
В отличие от mod_php, перловый модуль персистентен, т.е не инициализует себя заново каждый раз. Это удобно, т.к освобождает от необходимости загружать заново большую пачку модулей перед каждым запросом.
- Процесс/поток - зависит от MPM
- Внутри сервера
- Динамика
- Персистентный
Twisted
Этот асинхронный сервер написан на Python. Его особенность - в том, что программист веб-приложения сам создает дополнительных рабочих и дает им задания.
# пример кода на сервере twisted
# долгая функция, обработка запроса
def do_something_big(data):
....
# в процессе обработки запроса
d = deferToThread(do_something_big, "параметры")
# привязать каллбеки на результат do_something_big
d.addCallback(handleOK)
# .. и на ошибку при выполнении do_something_big
d.addErrback(handleError)
Здесь программист, получив запрос, использует вызов deferToThread для создания отдельного потока, которому поручено выполнить функцию do_something_big. При успешном окончании do_something_big, будет выполнена функция handleOK, при ошибке - handleError.
А текущий поток в это время продолжит обычную обработку соединений.
Все происходит в едином адресном пространстве, поэтому все рабочие могут разделять, например, один и тот же массив с пользователями. Поэтому на Twisted легко писать многопользовательские приложения типа чата.
- Поток
- Внутри сервера
- Динамика
- Персистентный
Tomcat, Servlets
Сервлеты - классический пример поточных веб-приложений. Единый Java-код приложения запускается во множестве потоков. Синхронизация обязательна и должна выполняться программистом.
- Поток
- Внутри сервера
- Динамика
- Персистентный
FastCGI
FastCGI - интерфейс общения web-сервера с внешними worker'ами, которые обычно запущены как процессы. Сервер в специальном (не HTTP) формате передает переменные окружения, заголовки и тело запроса, а worker - возвращает ответ.
Есть два способа порождения таких worker'ов.
- Интегрированный с сервером
- Отдельный от сервера
В первом случае сервер сам создает внешние рабочие процессы и управляет их числом.
Во втором случае - для порождения рабочих процессов используется отдельный "spawner", второй, дополнительный сервер, который умеет общаться только по FastCGI-протоколу и управлять рабочими. Обычно spawner порождает рабочих в виде процессов, а не потоков. Динамика/Статика - определяется настройками spawner'а, а Чистый/Персистентный - характеристиками рабочего процесса.
Пути работы с FastCGI
С FastCGI можно работать двумя путями. Первый способ - самый простой, его использует Apache.
получить запрос -> отдать на обработку в FastCGI -> подождать ответа -> отдать ответ клиенту.
Второй способ используют сервера типа lighttpd/nginx/litespeed/и т.п.
получить запрос -> отдать на обработку в FastCGI -> обработать других клиентов -> отдать ответ клиенту, когда придет.
Отмеченное отличие позволяет Lighttpd + fastcgi работать эффективнее, чем это делает Apache, т.к пока процесс Apache ждет - Lighttpd успевает обслужить другие соединения.
Режимы работы FastCGI
У FastCGI есть два режима работы.
- Responder - обычный режим, когда FastCGI принимает запрос и переменные, и возвращает ответ
- Authorizer - режим, когда FastCGI в качестве ответа разрешает или запрещает доступ. Удобно для контроля за закрытыми
статическими файлами
Оба режима поддерживаются не во всех серверах. Например, в сервере Lighttpd - поддерживаются оба.
FastCGI PHP vs PERL
PHP-интерпретатор каждый раз очищает себя перед обработкой скрипта, а Perl - просто обрабатывает запросы один за другим в цикле вида:
подключить модули;
while (пришел запрос) {
обработать его;
print answer;
}
Поэтому Perl-FastCGI гораздо эффективнее там, где большУю часть времени выполнения занимают include вспомогательных модулей.
Резюме
В статье рассмотрена общая структура обработки запросов и виды worker'ов.
Кроме того, заглянули в Apache Event MPM и способы работы с FastCGI, посмотрели сервлеты и Twisted.
Надеюсь, этот обзор послужит отправной точкой для выбора серверной архитектуры Вашего веб-приложения.
|