Pull to refresh

Встроенному perl'у быть!

Reading time 12 min
Views 6.6K
Однажды холодной сибирской зимой начальство решило согреть нашу команду горячей начальственной любовью и передало в наши руки еще один проект. С первого же взгляда обнаружилось несколько серьёзных проблем:
  • Это было написано на 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: Я ничего не имею против пхп — сам раньше на нем немало писал. Просто мне перл интереснее. А первый пункт там стоит не потому что мы считаем пхп недоязыком, а потому что мы пишем на перле и раз было принято решение все переделать, то для нас было очевидным делать это на перле.
Tags:
Hubs:
+91
Comments 170
Comments Comments 170

Articles