Однажды холодной сибирской зимой начальство решило согреть нашу команду горячей начальственной любовью и передало в наши руки еще один проект. С первого же взгляда обнаружилось несколько серьёзных проблем:
В общем, как настоящие русские парни, мы решили все сломать к хренам собачьим и построить заново, сделав красиво. Почесав репу решили, что apache+modperl — это для старпёров и не интересно, а fastcgi — путь слабых духом, поэтому все будем делать на nginx + его вечноэкспериментальный встроенный перл.
Позже я почувствовал себя на густозасеянном разного размера глаблями поле…
Буквально с первой же строчкой кода начались и первые проблемы. Начнем с того, что многие всеми любимые перловые модули были заточены под Apache и под nginx работать отказывались сколько их не уговаривай. Плюс ко всему форумы пестрили заголовками вроде «nginx+perl+mason — это невозможно», которые сразу насторожили. Как оказалось зря! mason завелся с полпинка и работал отлично, но это потом.
Модули затачивать под nginx жутко не хотелось(см. замечание про русских парней выше), поэтому было решено и их написать самим. Повыкидывав все
ненужное, мы поняли, что не хватает одного лишь Apache::Session, который как видно из имени, работает с сессиями только под вражескими серверами. Хранилище сессий с таким же как и у Apache::Session интерфейсом было написано за вечер и счастье казалось таким близким.
На следующее утро решил начать то о чем, собственно, топик — завод встроенного в nginx перла. Тут-то и началось веселье.
Мануал nginx говорит, что все довольно просто. Надо в конфиг вставить
И написать сам хендлер
Немного помусолив пути в конфиге, этот пример удалось запустить. Дальше заводим mason. Тут получили удар куда надо первыми граблями: обычно mason делают через HTML::Mason::ApacheHandler, который делает программисту хорошо. У нас этого не было, поэтому пришлось лезть в кишки mason'a и использовать HTML::Mason::Interp напрямую. Но он по умолчанию плюёт весь вывод в stdout, а нам надо через $r->print(). Покурив маны
увидели у конструктора HTML::Mason::Interp параметр out_method. Грабли появились вот какие: mason — довольно тяжелая штука и создавать его при каждом запросе страницы не комильфо, посему делаем один объект мейсона при инициализации(тут тоже грабельки, но об этом позже) и используем его потом при обработке всех запросов, но тогда придется сделать и $r глобальным!
Получилось как-то так:
Сразу же после этого выяснилось, что nginx не умеет передавать параметры из URL в переменные, а передаёт только сам URL — делай с ним всё, что тебе захочется. Ну ладно, подумал я, не велика потеря. URL распарсили, аккуратно все положили в переменные и поехали дальше.
В скором времени, конечно же, понадобилось использовать не GET, а POST запросы, а они в nginx обрабатываются совсем замечательно. Судя по мануалам, если приходит POST запрос, то его можно обработать, установив обработчик через $r->has_request_body(handler_ref).
Первый обработчик был такой:
Но после недолгого чтения самизнаетечего, это быстро заменилось на
Работало на ура… на пустой странице без передачи параметров и так далее. Чуть что — сразу в логе появлялось красивое worker process exited on signal 11. Вычитка манов не помогала. Дебагом было установлено, что падаем мы всегда по-разному — иногда при вызове $r->request_body, иногда при $r->request_body_file, иногда уже в DoRequest, а иногда уже после DoRequest. Через два дня(!!!) со слезами на глазах я в тысячный раз открыл мануал, глаз вцепился в одну единственную строчку, сердце забилось со страшной силой, а губы беззвучно повторяли все непечатные заклинания, которые мне только известны.
Все оказалось как всегда очень просто: когда мы в webhandler делаем $r = shift; ни в коем случае не стоит в других местах работать с этим же объектом! в HandlePOST тоже передается объект запроса, который может быть уже совсем не таким, как наш глобальный. Ну и получается классический такой сегфолт.
После добавления в самое начало HandlePOST строчки $r = shift; все отлично заработало без проблем.
Почти в самом конце пришлось немного повозиться с загрузкой файлов. Поскольку CGI мы не использовали, пришлось читать всякие там гуглы, в хэндлере понимать, что нам пришел файл, и что-нибудь с ним делать. По долгу службы RFC822, RFC2822, RFC2046-2049 давно прочитаны и выучены наизусть, поэтому мне сразу стало понятно, что файлы-то приходят как обычный MIME part. Парсить его руками я не рискнул, поэтому попробовал распарсить через MIME::Parser. Распарсила, зараза!
Еще одна особенность, о которой я обещал написать, но не нашел подходящего места:
При старте nginx запускает перл и грузит наш файл с обработчиком. Этот файл перлом честно выполняется(то есть проходят все use, require, выполняется непроцедурный код и так далее). Вроде ничего страшного, кроме того, что все это счастье делается из под root'а =). В общем-то, с mason'ом тут и возникла основная проблема: у него есть свой кэш, чтобы каждый раз не перекомпилировать шаблоны. Путь к кэшу указывается в конструкторе, но этот путь не полный и mason внутри него создает еще кучку служебных папок и файлов. Далее, когда приходит запрос от клиента, webhandler выполняется уже с правами, данными ему админом(то есть nginx:nginx), и записать в кэш скомпилированные шаблоны не может, ибо созданы папки были рутом.
Выхода виделось два: либо при создании объекта мейсона делать chmod-ы папок на обычного пользователя, либо создавать объект мейсона уже в webahndler(чего изначально очень хотелось избежать).
Из двух зол я выбрал второе и чуть ниже станет ясно как это было реализовано.
Через несколько недель был запуск и было все хорошо, а первый миллион удачных ответов nginx'a(без ездиного разрыва) были восприняты мною как полная победа. Добро победило!
В конце хотелось бы сказать, что маны читать полезно(особенно, если они на родном языке), но не всегда там видистя сразу то, что нужно. Ну и приведу скелет обработчика nginx с использованием mason и с обходом всех встретившихся мне граблей:
Надеюсь, что я трудился не зря, и этот топик поможет хоть кому-то. Удачи =)
_________
Текст подготовлен в VIM. Код раскрашен GNU Source Highlight
UPD: Я ничего не имею против пхп — сам раньше на нем немало писал. Просто мне перл интереснее. А первый пункт там стоит не потому что мы считаем пхп недоязыком, а потому что мы пишем на перле и раз было принято решение все переделать, то для нас было очевидным делать это на перле.
- Это было написано на PHP;
- Это было написано на таком PHP, что **** **** ***;
- Захардкожено было всё, и ни о какой расширяемости или настраиваемости речи не шло. А ведь нагрузка росла не по дням.
В общем, как настоящие русские парни, мы решили все сломать к хренам собачьим и построить заново, сделав красиво. Почесав репу решили, что apache+modperl — это для старпёров и не интересно, а fastcgi — путь слабых духом, поэтому все будем делать на nginx + его вечноэкспериментальный встроенный перл.
Позже я почувствовал себя на густозасеянном разного размера глаблями поле…
Путь джедая
Буквально с первой же строчкой кода начались и первые проблемы. Начнем с того, что многие всеми любимые перловые модули были заточены под Apache и под nginx работать отказывались сколько их не уговаривай. Плюс ко всему форумы пестрили заголовками вроде «nginx+perl+mason — это невозможно», которые сразу насторожили. Как оказалось зря! mason завелся с полпинка и работал отлично, но это потом.
Модули затачивать под nginx жутко не хотелось(см. замечание про русских парней выше), поэтому было решено и их написать самим. Повыкидывав все
ненужное, мы поняли, что не хватает одного лишь Apache::Session, который как видно из имени, работает с сессиями только под вражескими серверами. Хранилище сессий с таким же как и у Apache::Session интерфейсом было написано за вечер и счастье казалось таким близким.
На следующее утро решил начать то о чем, собственно, топик — завод встроенного в nginx перла. Тут-то и началось веселье.
Мануал nginx говорит, что все довольно просто. Надо в конфиг вставить
http { perl_modules habrahabr/lib; perl_require handler.pl; server { location / { perl habrahabr::webhandler; } } }
И написать сам хендлер
package habrahabr;
use nginx;
sub webhandler {
my $r = shift;
$r->send_http_header("text/html");
return OK if $r->header_only;
$r->print("hello habrahabr!\n<br/>");
return OK;
}
1;
Немного помусолив пути в конфиге, этот пример удалось запустить. Дальше заводим mason. Тут получили удар куда надо первыми граблями: обычно mason делают через HTML::Mason::ApacheHandler, который делает программисту хорошо. У нас этого не было, поэтому пришлось лезть в кишки mason'a и использовать HTML::Mason::Interp напрямую. Но он по умолчанию плюёт весь вывод в stdout, а нам надо через $r->print(). Покурив маны
увидели у конструктора HTML::Mason::Interp параметр out_method. Грабли появились вот какие: mason — довольно тяжелая штука и создавать его при каждом запросе страницы не комильфо, посему делаем один объект мейсона при инициализации(тут тоже грабельки, но об этом позже) и используем его потом при обработке всех запросов, но тогда придется сделать и $r глобальным!
Получилось как-то так:
our $r;
our $mason;
$mason = new HTML::Mason::Interp(
...
out_method => sub { $r->print(@_) },
...
);
sub webhandler {
$r = shift;
...
$mason->exec($r->uri);
}
Сразу же после этого выяснилось, что nginx не умеет передавать параметры из URL в переменные, а передаёт только сам URL — делай с ним всё, что тебе захочется. Ну ладно, подумал я, не велика потеря. URL распарсили, аккуратно все положили в переменные и поехали дальше.
В скором времени, конечно же, понадобилось использовать не GET, а POST запросы, а они в nginx обрабатываются совсем замечательно. Судя по мануалам, если приходит POST запрос, то его можно обработать, установив обработчик через $r->has_request_body(handler_ref).
Первый обработчик был такой:
sub HandlePOST {
my $body = $r->request_body;
...
DoRequest();
return OK; # В манах как-то скудно написано об этой функции,
# но можно понять, что назад в webhandler мы не вернемся, поэтому ретурним OK отсюда
}
Но после недолгого чтения самизнаетечего, это быстро заменилось на
sub HandlePOST {
my $body = $r->request_body;
unless ( $body ) {
my $file = $r->request_body_file;
if ( $file && -f $file ) {
my ($fh, $block);
open $fh, "<$file";
binmode $fh; # на всякий случай
while ( read $fh, $block, 4096 ) { $body .= $block }
close $fh;
}
}
...
DoRequest();
}
Работало на ура… на пустой странице без передачи параметров и так далее. Чуть что — сразу в логе появлялось красивое worker process exited on signal 11. Вычитка манов не помогала. Дебагом было установлено, что падаем мы всегда по-разному — иногда при вызове $r->request_body, иногда при $r->request_body_file, иногда уже в DoRequest, а иногда уже после DoRequest. Через два дня(!!!) со слезами на глазах я в тысячный раз открыл мануал, глаз вцепился в одну единственную строчку, сердце забилось со страшной силой, а губы беззвучно повторяли все непечатные заклинания, которые мне только известны.
Все оказалось как всегда очень просто: когда мы в webhandler делаем $r = shift; ни в коем случае не стоит в других местах работать с этим же объектом! в HandlePOST тоже передается объект запроса, который может быть уже совсем не таким, как наш глобальный. Ну и получается классический такой сегфолт.
После добавления в самое начало HandlePOST строчки $r = shift; все отлично заработало без проблем.
Почти в самом конце пришлось немного повозиться с загрузкой файлов. Поскольку CGI мы не использовали, пришлось читать всякие там гуглы, в хэндлере понимать, что нам пришел файл, и что-нибудь с ним делать. По долгу службы RFC822, RFC2822, RFC2046-2049 давно прочитаны и выучены наизусть, поэтому мне сразу стало понятно, что файлы-то приходят как обычный MIME part. Парсить его руками я не рискнул, поэтому попробовал распарсить через MIME::Parser. Распарсила, зараза!
Еще одна особенность, о которой я обещал написать, но не нашел подходящего места:
При старте nginx запускает перл и грузит наш файл с обработчиком. Этот файл перлом честно выполняется(то есть проходят все use, require, выполняется непроцедурный код и так далее). Вроде ничего страшного, кроме того, что все это счастье делается из под root'а =). В общем-то, с mason'ом тут и возникла основная проблема: у него есть свой кэш, чтобы каждый раз не перекомпилировать шаблоны. Путь к кэшу указывается в конструкторе, но этот путь не полный и mason внутри него создает еще кучку служебных папок и файлов. Далее, когда приходит запрос от клиента, webhandler выполняется уже с правами, данными ему админом(то есть nginx:nginx), и записать в кэш скомпилированные шаблоны не может, ибо созданы папки были рутом.
Выхода виделось два: либо при создании объекта мейсона делать chmod-ы папок на обычного пользователя, либо создавать объект мейсона уже в webahndler(чего изначально очень хотелось избежать).
Из двух зол я выбрал второе и чуть ниже станет ясно как это было реализовано.
Через несколько недель был запуск и было все хорошо, а первый миллион удачных ответов nginx'a(без ездиного разрыва) были восприняты мною как полная победа. Добро победило!
В конце хотелось бы сказать, что маны читать полезно(особенно, если они на родном языке), но не всегда там видистя сразу то, что нужно. Ну и приведу скелет обработчика nginx с использованием mason и с обходом всех встретившихся мне граблей:
package Habrahabr;
use strict;
use warnings;
use nginx;
use HTML::Mason;
# our - это старая привычка, тут уже можно без него
our $request;
our $mason;
our $init = 0;
sub WebHandler {
$request = shift;
if ( !$init || !$mason ) {
$mason = new HTML::Mason::Interp(
comp_root => "/opt/habrahabr/html",
data_dir => "/opt/habrahabr/var/mason_cache/",
use_strict => 1,
out_method => sub { $request->print(@_) },
error_format => 'html',
error_mode => 'output',
allow_globals => [ qw( $request %session $uri $post_param ) ], # это чтобы потом из шаблонов можно было использовать переменную запроса
);
}
# чистим остатки от прошлого запроса
$mason->set_global( uri => '' );
$mason->set_global( post_param => '' );
if ( $r->request_method eq 'POST' ) {
$request->has_request_body( \&Habrahabr::ProcessPOST );
# сюда мы больше не вернёмся!
}
elsif ( $r->request_method eq 'GET' ) {
DoRequest();
}
else {
return DECLINED;
}
return HTTP_OK;
}
sub ProcessPOST {
$request = shift; # теперь нужно использовать этот и только этот объект
# вытаскиваем post параметры
my $body = $r->request_body;
unless ( $body ) {
my $file = $r->request_body_file;
if ( $file && -f $file ) {
my ($fh, $block);
open $fh, "<$file";
binmode $fh;
while ( read $fh, $block, 4096 ) { $body .= $block };
close $fh;
}
}
# получить файлы
my $ContentType = $r->header_in('Content-type');
if ( $ContentType =~ 'multipart/form-data' ) {
my ($Boundary) = ( $ContentType =~ /boundary="?([^\s]+)"?/ );
# дописываем заголовок, чтобы парсер понял, что перед ним MIME и можно его парсить
$body = "Content-Type: multipart/form-data;\n boundary=\"$Boundary\"\n\n".$body;
my $Parser = new MIME::Parser;
$Parser->output_to_core(1);
$Parser->decode_bodies(0);
# парсер умирает страшно. А работать дальше надо. в eval его, в eval!
eval {
$body = $Parser->parse_data( $body );
};
}
$mason->set_global( post_param => \$body );
DoRequest();
}
sub DoRequest {
my $uri = $request->uri;
# Имитируем DirectoryIndex. Как сделать это из nginx'a мы так и не придумали.
if ( !$mason->comp_exists( $uri ) ) {
if ( $mason->comp_exists( $uri . "/index.html" ) ) {
$uri = $uri . "/index.html";
}
else {
$uri = "/404.html"; # не забыть там отдать 404, а не 200
}
}
$mason->set_global( request => $request );
$mason->set_global( uri => $uri );
$mason->exec( $uri );
}
Надеюсь, что я трудился не зря, и этот топик поможет хоть кому-то. Удачи =)
_________
Текст подготовлен в VIM. Код раскрашен GNU Source Highlight
UPD: Я ничего не имею против пхп — сам раньше на нем немало писал. Просто мне перл интереснее. А первый пункт там стоит не потому что мы считаем пхп недоязыком, а потому что мы пишем на перле и раз было принято решение все переделать, то для нас было очевидным делать это на перле.