Платформы для запуска Perl веб-приложений

Тест производительности некоторых серверов
В статье рассматриваются платформы/библиотеки для запуска веб-приложений на языке Perl. Рассматриваются они именно как «серверы приложений» — в случае со standalone серверами не предъявляется требований гибкости обычного HTTP-сервера вроде nginx или Apache, безопасности, или производительности в обслуживании статических файлов, так как предполагается, что Perl-сервер будет всё равно «спрятан» за фронтенд наподобие nginx. Задача платформы — предоставить удобный программный интерфейс, такой, чтобы приложение могло получить запрос, предназначающийся ему, и сформировать на него адекватный ответ.

Все они делятся на две категории — библиотеки, дающие возможность запуска Standalone HTTP-сервера, и интерфейсы, требующие для работы внешнего HTTP-сервера.

Фреймворки для построения веб-приложений — всевозможные Mason, Catalyst и т. п. — не рассматриваются.

Do you want to try some new features? By joining the beta, you will get access to experimental features, at the risk of encountering bugs and issues.

Ок Нет, спасибо

Требующие внешнего HTTP-сервера

Сюда относятся, во-первых, все реализации, предназначенные для встраивания Perl-интерпретатора в тот или иной веб-сервер, а во-вторых, дополнительные протоколы взаимодействия HTTP-сервера с Perl-программами или демонами, такие, как CGI.

CGI, PCGI

Старый добрый Common Gateway Interface (CGI — статья в русскоязычной Википедии), впервые реализованный где-то в районе 1993 года в первом HTTP-сервере NCSA HTTPd 1.0. Идея — максимально простой интерфейс взаимодействия с программой — через переменные окружения, а также стандартные ввод и вывод (STDIN и STDOUT). Веб-сервер при каждом запросе, который нужно обработать CGI приложением, запускает новый процесс (это самое приложение), передавая некоторые заголовки и данные в переменных окружения, а адрес запроса и содержимое (например, загружаемого на сервер файла) через стандартный ввод, после чего читает ответ (заголовки и содержимое) со стандартного вывода приложения.

Плюсы:

  1. Универсальность — запуск CGI скриптов поддерживается почти везде.
  2. Запуск нового интерпретатора, помимо минуса (1), означает и плюс — свободу от свойственных долгоживущим Perl демонам утечек памяти.

Универсальность CGI в смысле возможности использования произвольного языка программирования для нас ни плюсом, ни минусом, очевидно, не является, так как рассматривается именно Perl.

На этом плюсы заканчиваются и начинаются сплошные минусы:

  1. Самый очевидный и повторяемый всеми аки мантра минус — слабая производительность из-за необходимости запуска нового процесса интерпретатора при каждом запросе. Причин две — во-первых, это постоянно создаваемые процессы, которые могут быть совсем не лёгкими — раз, и вообще ограничивать производительность ОС большим числом переключений при большой нагрузке; во-вторых, это постоянные компиляции и инициализации модулей, занимающие приличное время.
  2. Второй наиболее неприятный минус — очень неудобный API наиболее стандартного модуля CGI.pm. Например:
    • Нельзя нормально получить хеш со всеми параметрами, так как CGI->Vars работает криво: если в запросе передано несколько значений одного параметра (&a=1&a=2), то в Vars значением этого параметра будет конкатенация всех этих значений (то есть { a => '12' }). Хотя по логике вещей там должен быть массив [ '1', '2' ].
    • CGI->cookie() не дружит с UTF-ными ключами.
    • Следить за своевременным выводом заголовков и текста ответа нужно самому.
    • У различных функцих весьма странные интерфейсы — с ключами, начинающимися на «-» и так далее.
    • В CGI часто даже передаются не все заголовки запроса.
  3. Говнокод.
    • На код самого CGI.pm страшно смотреть. В принципе, уже одно отсутствие use strict говорит о раздолбайстве авторов.
    • В районе года 2000-ного авторам вдруг взбрело в голову, что век амперсанда (&) в качестве разделителя параметров в строках запроса подошёл к концу, и что теперь все будут использовать вместо него точку с запятой (;). Соответственно поведение и разбора, и генерации URL изменилось — причём, если в случае с генерацией всё легко возвращается на свои места заданием опции -oldstyle_urls, то разбор URL неизменно разбивает их и по «&», и по «;», что влечёт за собой различные неприятные эффекты.
    • Большинство модулей типа CGI::что-нибудь — дикая древность, исповедующая генерацию HTML кода без использования шаблонизаторов print()'ами, на содержимое которых повлиять без влезания в сами модули невозможно. XXI век на дворе, пора бы прекратить хотя бы это — ан нет, и в 2004, и даже в 2006 годах такие модули появлялись.

PCGI — те же яйца, только в профиль, чуть более аккуратно написанные и с защитой от слишком больших запросов. Все интерфейсные минусы сохраняются.

FastCGI / SCGI

Идея FastCGI — ликвидировать тормоза CGI, сохранив интерфейс. FastCGI-процесс обрабатывает не один запрос, а много — последовательно принимая их в цикле через Unix- или TCP-сокет. Таким образом, FastCGI-процессы, во-первых, могут быть запущены на другом физическом сервере, а во-вторых, становится возможно распределение запросов между несколькими долгоживущими процессами.

SCGI — фактически клон FastCGI, отличия лишь в формате передачи данных, который, как утверждают авторы, проще реализовать, но и возможностей у него меньше — например, STDERR не передаётся обратно HTTP-серверу. Поддерживается чуть менее широко: ngx_scgi_module, Apache, Lighttpd, Cherokee, Mathopd с неофициальным патчем.

Плюсы:

  1. Возможность запуска старых CGI-приложений в «ускоренном режиме» практически без дополнительных телодвижений. Хороший пример — awstats, представляющий из себя (внимание!) один CGI-скрипт на всё приложение, весом примерно 550 Кб. Один разбор такого скрипта занимает почти полсекунды… А под FastCGI это делается лишь однажды.
  2. Передача в скрипт любых HTTP-заголовков, какие душе угодны.
  3. Возможность самостоятельного управления пулом процессов или потоков.
  4. Возможность запуска FastCGI-скриптов на серверах, поддерживающих только CGI — в этом случае модуль FCGI обрабатывает 1 запрос и выходит.

Минусы:

  1. Склонность к утечкам памяти, особенно в случае использования большого числа устаревшего кода, рассчитанного на «умирание» скрипта после обработки каждого запроса в среде CGI.
  2. Взаимодействие по-прежнему ведётся через функции CGI, поэтому «интерфейсные» минусы CGI никуда не исчезают. Специальных обвязок для упрощения взаимодействия с сервером по протоколу FastCGI нет, кроме самой примитивной реализации — модуля FCGI.

mod_perl

mod_perl — модуль HTTP-сервера Apache, предназначенный для разработки модулей Apache на языке Perl. «Модулей» — потому, что API mod_perl’а почти полностью повторяет C’шное API апача. Поэтому же API сильно различается в 1-ой и 2-ой версиях. Хотя какая разница, всё равно про httpd 1.x и mod_perl 1.x уже никто не помнит.

Плюсы:

  1. Большая степень гибкости и возможности комбинирования с другими модулями Apache, в частности, засчёт наличия большого числа обработчиков разных стадий запроса. Своим Perl-модулем можно осуществлять не всю обработку запроса, а, например, только авторизацию.
  2. Весьма богатый и довольно удобный программный интерфейс, через который с Apache можно делать практически всё, что душа пожелает.
  3. Возможность с небольшими телодвижениями запускать CGI-приложения в среде mod_perl с помощью модуля Apache::Registry. Для примера использования доработанного модуля Registry можно посмотреть реализацию mod_perl.pl из Bugzilla.
  4. Модуль популярен. Есть множество наработок, многие из которых представляют собой весьма приятные продукты. Хороший пример — профилировщик NYTProf, разработанный именно для профилирования мод_перла.

Минусы:

  1. Чрезмерная завязка на внутреннее устройство веб-сервера Apache. На mod_perl’е вы разрабатываете модуль Apache. Приложения, написанные под mod_perl, не запустятся больше нигде.
  2. В mod_perl2 для различных функций существует по нескольку где-то конкурирующих, где-то дополняющих друг друга, а где-то сходных по функционалу, но разных по интерфейсу библиотек. Частично это диктуется совместимостью с mod_perl1. Примеры — куки: Apache2::Cookie, APR::Request::Cookie, запрос: Apache2::RequestRec, Apache2::RequestUtil, Apache2::Request, APR::Request. Пока я в первый раз допёр, что для того, чтобы получить в пользование $r->dir_config(), нужно сделать use Apache2::RequestUtil Это не так страшно, но некоторую путаницу всё-таки вносит.
  3. Склонность к утечкам памяти. mod_perl течёт всегда, хоть ты его режь. Более-менее решается MaxRequestsPerChild.
  4. Проблемы с перезагрузкой модулей без перезапуска сервера. Apache2::Reload — кривой; перезагрузку можно реализовать ручками (см. https://github.com/vitalif/bugzilla4intranet), но и такой вариант всё равно иногда срывает апачу крышу, он забывает про все константы и валит Internal Server Error до ближайшего рестарта.
  5. Большое потребление памяти, ибо Perl сосуществует в одних и тех же процессах с остальными модулями Apache.
  6. Если нужно запустить несколько разных приложений или несколько экземпляров одного приложения, mod_perl создаёт проблемы по причине отсутствия изоляции загруженного кода модулей между приложениями. Решается это PerlOptions +Parent (в mod_perl 1.x не решалось), но в этом случае дочерние процессы Apache вырастают до совсем неприличных размеров по причине работы в каждом нескольких Perl-интерпретаторов вместо одного. Пул процессов-то общий.
  7. Время от времени в mod_perl всплывают совершенно удивительные и неуловимые глюки, особенно в необычных режимах вроде taint, или при использовании с некоторыми модулями или движками Apache. «Потому что Perl и mod_perl — это как бэ немного разные языки» (c). Например:
    • При использовании mpm_itk, 2-го мод_перла и PerlOptions +Parent (дающей отдельный пул интерпретаторов виртхосту) глобальные переменные в пакетах (как my, так и our) перестают сохранять свои значения между запросами.
    • При включённом taint mode и тоже в отдельном интерпретаторе, в составе Bugzilla проявляется следующий мистический баг:
    На входе строки $oldstr и $newstr, обе не taint’ченные, в $newstr есть запятые, в $oldstr нет. Пишем два идентичных по семантике фрагмента кода:
    • Если написать $oldstr =~ s/[,\s]+/ /g; $newstr =~ s/[,\s]+/ /g;, то $newstr почему-то становится tainted.
    • Если же написать s/[,\s]+/ /g for $oldstr, $newstr;, то обе, как и положено, остаются не tainted.
    Баг воспроизводится только в составе Bugzilla и только под мод_перлом, из контекста выдернуть его не получается.
    • Ещё пример в taint mode из Багзиллы:
    Где-то в глубине души модифицированного модуля Bugzilla::Field::Choice конкатенируются Bugzilla::Product->DB_COLUMNS->[0] и Bugzilla::Product->DB_TABLE. При проверке Scalar::Util::tainted в момент конкатенации сами по себе они оба безгрешны, однако, грех (флаг tainted) зарождается при их конкатенации. Вот и как это понимать? Адам и Ева сами по себе были безгрешны, но соединившись?..
  8. Некоторые затрудения в автоматизированных отладке и профилировании приложений в среде mod_perl из-за неочевидности команд запуска. Многие инструменты эта неочевидность «смущает».
  9. Отсутствие 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 апача.

Standalone

Альтернативой встраиванию интерпретатора в процесс HTTP-сервера является запуск собственного HTTP-сервера, рассчитанного на расположение за обратным прокси (reverse proxy) — mod_proxy Апача, nginx'ом, или Squid'ом — в качестве фронтенда. Так делают, например, Jetty (Java) и Zope (Python). Это работает точно так же, как обработка запросов внутри сервера, только запросы отправляются другому HTTP-серверу.

У такого подхода — использования HTTP вместо встраивания интерпретатора в сервер, или вариаций на тему CGI — есть несколько приятных преимуществ. Фронтенд может балансировать и распределять по нескольким серверам нагрузку одновременно с передачей запроса, причём для этого существует множество готовых качественных инструментов. Работа администраторов упрощается, потому что конфигурация фронтенда для передачи запроса разным приложениям идентична, а сами приложения могут быть запущены под любым системным пользователем, на другом сервере, в jail’е, виртуальной машине, за аппаратным firewall’ом или внутри какой-нибудь другой системы безопасности. При отладке разработчик может взаимодействовать напрямую со своим приложением без необходимости запускать отдельный HTTP-сервер. Потребление памяти сокращается засчёт изоляции каждого придожения в своём наборе процессов.

Кроме того, в этом случае приложение свободно от накладных расходов веб-сервера — для того же mod_perl’а генерация страниц без кэширования (но и без особых изысканий) за 1.5 мс почти «фантастика».

PSGI/Plack

Можно смело сказать, что PSGI/Plack — самый современный и удобный интерфейс для Perl веб-приложений.

PSGI — Perl-реализация питонячьего WSGI. «Низкоуровневый» интерфейс взаимодействия тривиален:

  • Приложение = функция.
  • На входе 1 хешреф «окружения», в который включаются, во-первых, параметры, аналогичные %ENV в CGI (REQUEST_URI, PATH_INFO и т. п.)…
  • …а во-вторых, некоторые PSGI-специфичные ключи, например, «input», содержащий входной поток.
  • На выходе массив из 3-х элементов: численный статус ответа, массив заголовков в виде [ Header => Value, Header => Value, … ] и текст ответа.

Однако без дополнительных обёрток такой интерфейс неудобен, поэтому есть Plack, причём имеющий вполне вменяемый интерфейс — например, Plack::Request->parameters возвращает в виде хеша все GET и POST параметры, Plack::Request->cookies — хеш кукисов и т. п. Это — в отличие от модулей типа CGI и PCGI, в которых параметры и куки нужно читать по отдельности через param() и cookie().

Плюсы:

  • Грамотный интерфейс без глобальных переменных.
  • Совместимость с кучей разных серверов: можно запускать хоть под CGI/FastCGI (с помощью Plack), хоть в Apache (mod_psgi), хоть в uWSGI, хоть в виде standalone демона (Starman, Twiggy, Corona и т.п, полный список на http://plackperl.org). Причём, standalone-реализации по отзывам вполне производительны.

Минусы:

  • Разве что отсутствие совместимости со «старыми» приложениями — то есть, нельзя просто обернуть CGI скрипт в функцию и превратить его таким образом в PSGI.
  • (Не проверено) возможно, в PSGI серверах отсутствует поддержка перезагрузки модулей.
  • В остальном, похоже, всё хорошо.

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. Собственно, есть и вариант для запуска PSGI/Plack приложений.
  • Модуль достаточно широко используется при тестировании веб-приложений, в качестве легковесного тестового сервера: Test::HTTP::Server::Simple.

Минусы:

  • Интерфейс CGI всё-таки требует по крайней мере выбора нужных модулей для разбора запросов и объединения всего этого функционала в своём приложении.
  • Странная реализация parse_request() и parse_headers(), по меньшей мере, вплоть до версии 0.40 (можно полечить переопределением соответствующей функции).
    • Читает из стандартного ввода запрос и заголовки она по 1 символу функцией sysread(), что весьма негативно сказывается на производительности.
    • В качестве разделителя строк, согласно всем стандартам, при обмене данными по сети, должно выступать сочетание CR-LF в платформо-независимом варианте: «\015\012». Тем не менее, функции HTTP::Server::Simple используют просто «\n», что тоже работает, но не является идеально переносимым вариантом.

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, заголовки и куки.

Заключение

«Финалистами» являются mod_perl 2.x, CGI и Plack. Остальное распространено слабо.

Победитель — однозначно Plack. Он наиболее современный, простой и продуманный.

CGI — всё ещё жив, но безнадёжно устарел. Старые приложения, на нём написанные, нужно либо превращать в FastCGI, либо превращать в HTTP::Server::Simple (достаточно удобно и сделано в https://github.com/vitalif/bugzilla-4intranet), либо, на крайний случай, просто запускать через FCGI::Daemon.

mod_perl — тоже популярен, хотя ни универсальностью, ни надёжностью от него не пахнет. «Perl и mod_perl — это разные языки».