Платформы для запуска Perl веб-приложений
В статье рассматриваются платформы/библиотеки для запуска веб-приложений на языке Perl. Рассматриваются они именно как «серверы приложений» — в случае со standalone серверами не предъявляется требований гибкости обычного HTTP-сервера вроде nginx или Apache, безопасности, или производительности в обслуживании статических файлов, так как предполагается, что Perl-сервер будет всё равно «спрятан» за фронтенд наподобие nginx. Задача платформы — предоставить удобный программный интерфейс, такой, чтобы приложение могло получить запрос, предназначающийся ему, и сформировать на него адекватный ответ.
Все они делятся на две категории — библиотеки, дающие возможность запуска Standalone HTTP-сервера, и интерфейсы, требующие для работы внешнего HTTP-сервера.
Фреймворки для построения веб-приложений — всевозможные Mason, Catalyst и т. п. — не рассматриваются.
Содержание
Требующие внешнего HTTP-сервера
Сюда относятся, во-первых, все реализации, предназначенные для встраивания Perl-интерпретатора в тот или иной веб-сервер, а во-вторых, дополнительные протоколы взаимодействия HTTP-сервера с Perl-программами или демонами, такие, как CGI.
CGI
Старый добрый Common Gateway Interface (CGI — статья в русскоязычной Википедии), впервые реализованный где-то в районе 1993 года в первом HTTP-сервере NCSA HTTPd 1.0. Идея — максимально простой интерфейс взаимодействия с программой — через переменные окружения, а также стандартные ввод и вывод (STDIN и STDOUT). Веб-сервер при каждом запросе, который нужно обработать CGI приложением, запускает новый процесс (это самое приложение), передавая некоторые заголовки и данные в переменных окружения, а адрес запроса и содержимое (например, загружаемого на сервер файла) через стандартный ввод, после чего читает ответ (заголовки и содержимое) со стандартного вывода приложения.
Плюсы:
- Универсальность — скрипт, написанный с использованием CGI.pm, будет работать на любой платформе, так как CGI поддерживают все (почти все?).
- Простые вещи реализовать на CGI просто. Чем-то эта идеология похожа на идеологию Perl в целом — простые задачи он делает простыми, а сложные — возможными.
- Большое количество наработок, то есть, модулей для работы в CGI среде. В частности, к счастью разработчиков, есть модуль PCGI.pm, являющий собой вменяемую PHP-подобную реализацию CGI-протокола.
- Запуск нового интерпретатора, помимо минуса (1), означает и плюс — свободу от свойственных долгоживущим Perl демонам утечек памяти.
А вот универсальность CGI в смысле возможности использования произвольного языка программирования для нас ни плюсом, ни минусом, очевидно, не является, так как рассматривается именно Perl.
Минусы:
- Самый очевидный и повторяемый всеми аки мантра минус — слабая производительность из-за необходимости запуска нового процесса интерпретатора при каждом запросе. Причин две — во-первых, это постоянно создаваемые процессы, которые могут быть совсем не лёгкими — раз, и вообще ограничивать производительность ОС большим числом переключений при большой нагрузке; во-вторых, это постоянные компиляции и инициализации модулей, занимающие приличное время.
- Кривой и устаревший код многих модулей для работы с разными функциями CGI. Примеры:
- CGI.pm написан без использования
use strict
. Это не страшно, но показатель раздолбайства авторов. - В районе года 2000-ного авторам CGI.pm вдруг взбрело в голову, что век амперсанда (&) в качестве разделителя параметров в строках запроса подошёл к концу, и что теперь все будут использовать вместо него точку с запятой (;). Соответственно поведение и разбора, и генерации URL изменилось — причём, если в случае с генерацией всё легко возвращается на свои места заданием опции -oldstyle_urls, то разбор URL неизменно разбивает их и по «&», и по «;», что влечёт за собой различные неприятные эффекты.
- Многие модули CGI::xx исповедуют генерацию HTML кода без использования шаблонизаторов, обычными print()'ами, на содержимое которых повлиять без влезания в сами модули невозможно. XXI век на дворе, пора бы прекратить хотя бы это — ан нет, и в 2004, и даже в 2006 годах такие модули появлялись.
- CGI.pm написан без использования
- Идеология CGI хоть чем-то и похожа на Перл, но фактически CGI больше рассчитан на простые задачи, нежели чем на сложные.
- Слабые возможности взаимодействия с HTTP-сервером — например, обычно в CGI даже не передаются все заголовки запроса. Кроме того, вывод ответа на STDOUT неудобен, если различные параметры задаются в различных местах программы.
FastCGI / SCGI
Идея FastCGI — ликвидировать недостатки CGI, сохранив интерфейс. Главный недостаток CGI — необходимость перезапуска приложения, поэтому его и ликвидировали в первую очередь: FastCGI-процесс обрабатывает не один запрос, а много — последовательно принимая их в цикле через Unix- или TCP-сокет. Таким образом, FastCGI-процессы, во-первых, могут быть запущены на другом физическом сервере, а во-вторых, становится возможно распределение запросов между несколькими процессами.
SCGI — практически это клон FastCGI, отличия лишь в формате передачи данных, который, как утверждают авторы, проще реализовать, но и возможностей у него меньше — например, STDERR не передаётся обратно HTTP-серверу. Поддерживается несколько менее широко: ngx_scgi_module, Apache, Lighttpd, Cherokee, Mathopd с неофициальным патчем.
Плюсы:
- Возможность запуска старых CGI-приложений в «ускоренном режиме» практически без дополнительных телодвижений. Хороший пример — awstats, представляющий из себя (внимание!) один CGI-скрипт на всё приложение, весом примерно 550 Кб. Один разбор такого скрипта занимает почти полсекунды… А под FastCGI это делается лишь однажды.
- Передача в скрипт любых HTTP-заголовков, какие душе угодны.
- Возможность самостоятельного управления пулом процессов или потоков.
Минусы:
- Склонность к утечкам памяти, особенно в случае использования большого числа устаревшего кода, рассчитанного на «умирание» скрипта после обработки каждого запроса в среде CGI.
- Взаимодействие по-прежнему ведётся через функции CGI, поэтому «интерфейсные» минусы CGI никуда не исчезают. Специальных обвязок для упрощения взаимодействия с сервером по протоколу FastCGI нет, кроме самой примитивной реализации — модуля FCGI. Идеология, видимо, такова — а зачем, раз и так есть CGI.pm и компания?
mod_perl
mod_perl — модуль HTTP-сервера Apache, предназначенный для веб-разработки внутри Apache на языке Perl. Существуют как версии для Apache 1.x, так и для 2.x. Внутреннее устройство mod_perl’а полностью повторяет C API апача.
Плюсы:
- Очень большая степень гибкости и возможности комбинирования с другими модулями Apache, в частности, засчёт наличия большого числа обработчиков разных стадий запроса. Гибкость означает, что можно не только просто отправлять ответ на запрос из своего Perl-модуля, но можно и осуществлять авторизацию или ещё что-нибудь. Любопытный пример использования: SVNPropCheck.
- Весьма богатый и достаточно удобный программный интерфейс, через который с Apache можно делать практически всё, что душа пожелает.
- Возможность с небольшими телодвижениями запускать CGI-приложения в среде mod_perl с помощью модуля Apache::Registry. Для примера использования доработанного модуля Registry можно посмотреть реализацию mod_perl.pl из Bugzilla 3.x.
- Модуль популярен. Есть множество наработок, многие из которых представляют собой весьма и весьма приятные продукты. Хороший пример — профилировщик NYTProf, разработанный именно для профилирования мод_перла.
Минусы:
- Чрезмерная завязка на внутреннее устройство веб-сервера Apache. Фактически — когда вы разрабатываете на mod_perl’е, вы разрабатываете полноценный модуль Apache. Аналогично — приложения, написанные под mod_perl, не запустятся больше нигде.
- В mod_perl2 для различных функций существует по нескольку где-то конкурирующих, где-то дополняющих друг друга, а где-то сходных по функционалу, но разных по интерфейсу библиотек. Частично это диктуется совместимостью с mod_perl1. Примеры — куки: Apache2::Cookie, APR::Request::Cookie, запрос: Apache2::RequestRec, Apache2::RequestUtil, Apache2::Request, APR::Request. Пока я допёр, что для того, чтобы получить в пользование
$r->dir_config()
, нужно сделатьuse Apache2::RequestUtil
… Это не так страшно, но некоторую путаницу всё-таки вносит. - Склонность к утечкам памяти. mod_perl течёт всегда, хоть ты его режь. Решение, правда, тоже несложное — MaxRequestsPerChild.
- Серьёзное увеличение размеров детёнышей процесса Apache.
- Проблемы с перезагрузкой модулей в процессе обслуживания без перезапуска сервера. Оговорка: это проблема Perl’а в целом, не только mod_perl’а. Но под mod_perl она проявляется сильнее: иногда при перезагрузке модулей ему прямо-таки «сносит крышу», он забывает про все константы и валит Internal Server Error на все запросы. Ну и второе — авторы mod_perl могли бы и предусмотреть быстрый «сброс» интерпретаторов по сигналу.
- В «многопользовательской» среде, точнее, в среде с множеством различных веб-приложений, мод_перл создаёт проблемы по причине отсутствия изоляции загруженного кода модулей между приложениями. Решение для этого — PerlOptions +Parent, но оно подходит только для случая с небольшим количеством приложений, так как в противном случае детёныши Apache вырастают до неприличных размеров по причине работы в них нескольких пулов Perl-интерпретаторов вместо одного.
- Время от времени в mod_perl всплывают совершенно неуловимые глюки, особенно в необычных режимах вроде taint, и при использовании с некоторыми модулями или движками Apache. «Потому что Perl и mod_perl — это как бэ немного разные языки» (c). Например:
- При использовании mpm_itk, 2-го мод_перла и PerlOptions +Parent (дающей отдельный пул интерпретаторов виртхосту) глобальные переменные в пакетах (как my, так и our) перестают сохранять свои значения между запросами.
- При включённом taint mode и тоже в отдельном интерпретаторе, в составе Bugzilla 3.x проявляется следующий мистический баг:
- На входе строки
$oldstr
и$newstr
, обе не taint’ченные, в$newstr
есть запятые, в$oldstr
нет. Пишем два идентичных по семантике фрагмента кода:- Если написать
$oldstr =~ s/[,\s]+/ /g; $newstr =~ s/[,\s]+/ /g;
, то$newstr
почему-то становится tainted. - Если же написать
s/[,\s]+/ /g for $oldstr, $newstr;
, то обе, как и положено, остаются не tainted.
- Если написать
- Баг воспроизводится только в составе Bugzilla и только под мод_перлом, из контекста выдернуть его не получается.
- Некоторые затрудения в автоматизированных отладке и профилировании приложений в среде mod_perl из-за неочевидности программ исполнения. Автоматизированные инструменты эта неочевидность «смущает».
- Отсутствие mod_perl на подавляющем большинстве веб-хостингов. Потому что для админов серверов с кучами хомячков это — головная боль, в многопользовательской среде влекущая проблемы как с безопасностью, так и с надёжностью и производительностью. Всё по причине уже описанных минусов.
- Как вы думаете, почему так широко распространился язык PHP? Именно по причине простоты обслуживания.
ngx_http_perl_module
Идея — вызов Perl-кода из nginx.
Ссылка на документацию: http://sysoev.ru/nginx/docs/http/ngx_http_perl_module.html.
Плюсы:
- Возможность вызова Perl-кода из SSI, что делает nginx почти шаблонизатором.
- Простота, благодаря которой количество источников мистических ошибок, а также неуловимых утечек памяти сводится почти к нулю.
Минусы:
- Идея размещения приложения внутри nginx несколько лишает его легковесности. Также появляются некоторые проблемы с масштабируемостью в случае простоев (ожиданий ответов от СУБД и т. п.).
- Жёсткая завязка на использование внутри nginx. Больше нигде приложения, написанные под nginx, не заработают.
- Поразительное качество: невозможно получить список всех HTTP-заголовков, присутствующих в запросе, а можно лишь считывать их по одному. Остаётся только применять патч к src/http/modules/perl/nginx.xs.
- Отсутствие поддержки CGI среды внутри ngx_http_perl_module, наподобие mod_perl апача.
- Версии ngx_http_perl_module до 0.6.22 имели следующие особенности:
- Значения, возвращаемые методами объекта запроса $r, во-первых, хранились в памяти, выделяемой не perl’ом, а nginx’ом из собственных пулов, что в большинстве случаев позволяло уменьшить число операций копирования, а во-вторых, не завершались нулевым байтом. В некоторых ситуациях это приводит к ошибкам, например, при попытке использования таких значений в численном контексте, или использования незавершённых нулём строк в именах файлов и тому подобном:
- (FreeBSD)
-
nginx in realloc(): warning: pointer to wrong page
-
Out of memory!
-
Callback called exit.
-
- (Linux)
-
*** glibc detected *** realloc(): invalid pointer: ... ***
-
Out of memory!
-
Callback called exit.
-
- Обход этих особенностей простой — нужно просто скопировать возвращённое значение в скаляр. Например, заменить на
my $i = $r->variable('counter') + 1;
my $i = $r->variable('counter'); $i++;
Standalone
Альтернативой встраиванию интерпретатора в процесс HTTP-сервера является запуск собственного HTTP-сервера, рассчитанного на расположение за обратным прокси (reverse proxy) — mod_proxy Апача, nginx'ом, или Squid'ом — в качестве фронтенда. Так делают, например, Jetty (Java) и Zope (Python). Это работает точно так же, как обработка запросов внутри сервера, только запросы отправляются другому HTTP-серверу.
У такого подхода — использования HTTP вместо встраивания интерпретатора в сервер, или вариаций на тему CGI — есть несколько приятных преимуществ. Фронтенд может балансировать и распределять по нескольким серверам нагрузку одновременно с передачей запроса, причём для этого существует множество готовых качественных инструментов. Работа администраторов упрощается, потому что конфигурация фронтенда для передачи запроса разным приложениям идентична, а сами приложения могут быть запущены под любым системным пользователем, на другом сервере, в jail’е, виртуальной машине, за аппаратным firewall’ом или внутри какой-нибудь другой системы безопасности. А при отладке разработчик может взаимодействовать напрямую со своим приложением без необходимости запускать отдельный HTTP-сервер.
Кроме того, в этом случае приложение свободно от накладных расходов веб-сервера — для того же mod_perl’а генерация страниц без кэширования (но и без особых изысканий) за 1.5 мс почти «фантастика».
LWP (HTTP::Daemon)
LWP (libwww-perl) — библиотека для создания как клиентов, так и серверов, полностью совместимых со спецификацией HTTP/1.1, на чистом Perl’е.
В качестве единственной готовой, пусть и исключительно простой, платформы, исповедующей идеологию получения запросов в форме HTTP::Request и ответа HTTP::Response’ами можно рассмотреть HTTP::Server::Brick, построенный на основе HTTP::Daemon.
Плюсы:
- Очень логичный и правильный программный интерфейс — HTTP::Request, HTTP::Response, HTTP::Body и т. п. HTTP::Body, кстати, использует Catalyst.
Минусы:
- Главный минус в том, что LWP в качестве основы HTTP-сервера не использует, видимо, практически никто. Посему, сколь логичным бы ни выглядел сервер, получающий на вход HTTP::Request и отвечающий HTTP::Response’ами, реально готовых наработок для такого подхода фактически нет. Например, нет ничего похожего на удобный Apache2::Request, с готовыми функциями для доступа к запросу, разобранному на URI, параметры POST, заголовки и куки.
HTTP::Server::Simple
HTTP::Server::Simple — простая реализация HTTP-сервера на Perl.
Плюсы:
- Интерфейс — CGI. В режиме nph — «Non-Parsed Headers» — то есть на STDOUT нужно выводить просто HTTP-ответ. Это устраняет по сути все интерфейсные ограничения CGI и сохраняет совместимость с CGI скриптами.
- Можно использовать стандартные модули Net::Server::xx для выбора поведения сервера — например, можно использовать как TCP, как и UNIX сокеты, можно создать prefork (Net::Server::PreFork или Net::Server::PreForkSimple) или мультиплексирующий однопоточный сервер (Net::Server::Multiplex).
- Существует некоторое количество модулей для расширения данного сервера — в частности, для запуска приложений на некоторых фреймворках через HTTP::Server::Simple, например, для Mason.
- Модуль достаточно широко используется при тестировании веб-приложений, в качестве легковесного тестового сервера: Test::HTTP::Server::Simple.
Минусы:
- Интерфейс CGI всё-таки требует по крайней мере выбора нужных модулей для разбора запросов и объединения всего этого функционала в своём приложении.
- Странная реализация parse_request() и parse_headers(), по меньшей мере, вплоть до версии 0.40. Лечится просто — переопределением соответствующей функции, но это ведь тоже лишние действия! :)
- Читает из стандартного ввода запрос и заголовки она по 1 символу функцией sysread(), что весьма негативно сказывается на производительности.
- В качестве разделителя строк, согласно всем стандартам, при обмене данными по сети, должно выступать сочетание CR-LF в платформо-независимом варианте: «\015\012». Тем не менее, функции HTTP::Server::Simple используют просто «\n», что тоже работает, но не является идеально переносимым вариантом.
Заключение
С точки зрения веб-разработчика, не принимающего участие в разработке основного «ядра» системы (демона и/или обработчиков), всё вышеописанное сводится к следующим различным интерфейсам:
Если стремиться к наиболее «красивому» и логичному интерфейсу, то:
Всё существующее фигня.) С моей точки зрения, по идеологии наиболее близок к идеалу подход LWP и HTTP::Server::Brick — на входе объект «запрос», на выходе объект «ответ». Но этот подход не очень популярен, поэтому, используя его, вы обрекаете себя на разработку и поддержку своей реализации — никто не говорит, что это плохо, но… не mainstream.
Данный подход был выбран личной мной в платформе Sway Solstice. Исходные коды можно увидеть здесь — они включают в себя реализацию HTTP::Request::Incoming — подкласса HTTP::Request с некоторыми удобными функциями разбора запроса, и абстракции HTTP::Request для mod_perl 1 и 2, CGI и nginx.
Если хочется простоты в стиле PHP, то:
Можно обратиться к PCGI.
А если выбирать наиболее универсальный и «mainstream» (широко поддерживаемый) интерфейс, то:
Это, конечно же, CGI. Что и объясняет популярность FastCGI — эквивалентного наиболее простому интерфейсу, обёрнутому в «ускоритель». Если хочется — использовать HTTP::Server::Simple тоже можно — он хоть и простой, но очень удобный. Отдавать с помощью него же статику я, правда, не стал бы… :)
Также следует помнить о всё ещё очень высокой популярности mod_perl. Ни универсальностью, ни надёжностью от него не пахнет, это нужно понимать, но всё-таки, во-первых, такой гибкости в обработке запросов невозможно добиться больше ни на одной платформе, а во-вторых, поддерживается он достаточно широко и поэтому вполне подходит для использования.