пятница, 7 декабря 2007 г.

Отцы и дети или perl, fork и деструкторы

Я конечно не Тургенев Иван Сергеевич, но также кое-что попробую рассказать о взаимоотношениях отцов и детей. Рассказать не все, а лишь то, что Иван Сергеевич умышленно опустил, чтобы не уменьшать художественную ценности своего произведения излишними техническими подробностями. Скажу сразу: поскольку эта история произошла давным-давно, то некоторые детали потеряны или, наоборот, обросли некоторыми выдумками и преувеличениями, так что за 100% достоверность не ручаюсь.

А началась все тогда, когда при открытом соединении с базой данных процесс вызвал системный вызов fork, а затем с удивлением обнаружил, что база "упала". Пришлось ему звать на помощь. Вариант с базой проверили сразу: база как работала, так и продолжала работать. Пожурили процесс за ложные показания, и начали копаться в нем.

И так его покрутили и этак, и пришли к выводу, что все дело в деструкторах. Дочерний процесс завершился и как положено все за собой почистил и убрал. При этом он закрыл разделяемый с родительским процессом сокет, по которому было установлено соединением с базой.

Но как обычно, большинство дорожек уже протоптаны, и нужно лишь найти правильную. Посмотрели документацию на модуль DBI и увидели атрибут InactiveDestroy, который говорит, что не надо уничтожать соединение с базой, если оно не твое.

Но как быть, когда пойдем путями неведанными, дорогами не хожеными? Стали искать решение и для такого случая, когда в сторонней библиотеке нет такого замечательного атрибута как InactiveDestroy.

Как научиться управлять деструкторами? Искали, искали и наконец-то нашли в модуле POSIX вызов _exit. Этот вызов говорит: "а пошел-ка, то на ..., то есть убирай за собой сам!" Так это то, что нужно! Конечно прямо этому совету никто не последовал - кому самому все убирать охота. Обернули, то что необходимо прибрать в лексическую область видимости - пусть сборщик мусора убирается там, а после этой области прописали POSIX::_exit(0).

На это можно было бы ставить точку. Все работает отлично, да и на вооружении теперь еще один трюк. Но интуиция подсказывала, что это только начало. Что будет, если дочерний процесс сам начнет использовать дескриптор к базе данных? Конечно так делать очень не хорошо. Но вдруг как-то явно или не явно это произойдет. Поэтому надо выработать простые правила, чтобы такой ситуации можно было бы легко избежать, то есть надо по полочкам разложить взаимоотношения "отцов и детей".

Как оказалось эти взаимоотношения можно разделить на два варианта: когда дочерний процесс будет существовать дольше своего родителя и когда дочерний процесс, cделав свою работу, завершиться раньше родительского.

Второй вариант был уже описан выше и для него достаточно использовать POSIX::_exit(0), если, конечно нет InactiveDestroy. В первом же варианте надо в дочернем процессе сразу же создать копии необходимых данных из родительского.

Если забыть на минутку о процессах, fork и прочей компьютерной ерунде, закрыть глаза, то так и видишь как отцы общаются с детьми.

Когда отец отпускает своего ребенка в самостоятельное плавание, он его наставляет: "сынок, воспользуйся моими трудами вовремя, создай на их основе как можно быстрей свой капитал и стань скорей независимым".

А когда отец отпускает ребенка ненадолго погулять, он ему говорит: "дитя, когда прийдешь с улицы, то убери на место все, что брал играться, но так, чтобы то что я дал тебе, осталось в неизменном состоянии".

Ну вот и все. Еще одна страница в отношении "отцов и детей" стала более ясной. По крайней мере в мире информационных технологий, а вот в жизни все намного сложней и одновременно проще!

четверг, 6 декабря 2007 г.

FastCGI, mod_perl и прочие

С января 2001 года сфера моей профессиональной деятельности тесно связна с созданием web-сервисов, в основном аналитических.

Одной из технологий, используемых в работе, является mod_perl. FastCGI не использовался ни разу. Почему? Как часто отвечают: "по историческим причинам". А вот сейчас решил посмотреть в сторону FastCGI подробней. Специфика нового проекта подразумевает наличие frontend'да, например, nignx. "Нет, проблем", - говорю сам себе: "frontend'ом будет nignx, а backend'ом - Apache". Но не так все просто.

В рассылке по nignx, до того как было все разложено по полочкам в различных документациях и заметках, часто задавались вопросы по использованию PHP в режиме FastCGI. То есть люди массово избавлялись от Apache mod_php, переходя не FastCGI. При этом, что в тот момент, не знаю как сейчас, патч php-fpm (http://php-fpm.anight.org/) не был включен в официальные исходники, так что многие пользовалось spawn-fcgi от lighttpd.

Что это? Дань моде или нечто большее? Тем не-менее этот процесс подтолкнул меня посмотреть подробней, какие есть у FastCGI преимущества. Да, я знаю, mod_perl и mod_php координально отличаются в области загрузки кода, но будем считать, что php акселераторы работают хорошо и это не является причиной перехода с mod_php на FastCGI.

Известно, что процессы Apache с mod_perl очень прожорливы на память. Может с FastCGI ее требуется меньше? Теоретически разница не должна быть слишком большой: ведь под mod_perl можно все используемые модули загрузить до начала создания дочерних процессов.

Стоп! Если мне не изменяет память, то mod_php так не может! Наверно это и есть причина перехода с mod_php на FastCGI. Хм, но ведь и spawn-fcgi в этом вопросе не помощник...

Тем не-менее с FastCGI решил разобраться до конца. Прописал в конфиге nignx какой порт слушать и занялся perl. Для perl существует три основных модуля FCGI, FCGI::ProcManager и FCGI::Spawn. (о FCGI::Async и других разговор не ведем). Первый является основным, второй - это менеджер процессов. Третий является надстройкой над первым двумя и предназначен для запуска скриптов через require, то есть является в некотором смысле аналогом Apache::PerlRun, поэтому мы его не рассматриваем.

Модули FCGI, FCGI::ProcManager являются очень "зрелыми" и все приведенные примеры предназначены для запуска FastCGI из-под "пускалок", а не как самостоятельные, поэтому все примеры слегка модифицируем. Перед основным циклом открываем сокет, который будет слушать nignx:

use FCGI;
use CGI;

my $socket = FCGI::OpenSocket(":9000", 5);
my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);

my $count = 0;

while($request->Accept() >= 0) {
$count++;
print <<TEXT;
Content-Type: text/html

<h1>hello</h1>
<p>$count</p>
<hr>
TEXT

print "$_ = $ENV{$_}<br>\n" foreach sort keys %ENV;

print "<hr>\n";

my $query = CGI->new();
print "$_ = ", $query->param($_), "<br>\n" foreach sort $query->param();

}

FCGI::CloseSocket($socket);

Но это пример имеет большой недостаток: всего лишь один процесс FastCGI.
На помощь приходит модуль FCGI::ProcManager, который создает до основного цикла
заданное количество потомков, выполняющих всю полезную работу по обработке запроса:

use FCGI;
use FCGI::ProcManager;
use CGI;

my $proc_manager = FCGI::ProcManager->new({ n_processes => 10 });

my $socket = FCGI::OpenSocket(":9000", 5);

my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);

$proc_manager->pm_manage();

my $count = 0;
while($request->Accept() >= 0) {
$proc_manager->pm_pre_dispatch();
$count++;
print <<TEXT;
Content-Type: text/html

<h1>hello</h1>
<p>$count</p>
<hr>
TEXT

print "$_ = $ENV{$_}<br>\n" foreach sort keys %ENV;

print "<hr>\n";

my $query = CGI->new();
print "$_ = ", $query->param($_), "<br>\n"
foreach sort $query->param();

$proc_manager->pm_post_dispatch();
}

FCGI::CloseSocket($socket);

Кстати, если кто знает ответе: в линуксе до сих про плохо с большим количеством
прослушивающих один сокет процессов и проходится перед аccept делать монопольную
блокировку файла "регулировщика"?

Вернемся к основной теме. В примерах использования FCGI::ProcManager используется модуль CGI::Fast, используя которых вышеприведенный вариант можно привести к следующему виду:

BEGIN {
$ENV{FCGI_SOCKET_PATH} = ":9000";
$ENV{FCGI_LISTEN_QUEUE} = 5;
}

use CGI::Fast;
use FCGI::ProcManager;

my $proc_manager = FCGI::ProcManager->new({ n_processes => 10 });

$proc_manager->pm_manage();

my $count = 0;

while(my $query = CGI::Fast->new()) {
$proc_manager->pm_pre_dispatch();

$count++;
print <<TEXT;
Content-Type: text/html

<h1>hello</h1>
<p>$count</p>
<hr>
TEXT

print "$_ = $ENV{$_}<br>\n" foreach sort keys %ENV;

print "<hr>\n";

print "$_ = ", $query->param($_), "<br>\n"
foreach sort $query->param();

$proc_manager->pm_post_dispatch();
}

Теперь рассмотрим фрейморки Catalyst и CGI::Application.

Для Catalyst создаем новый проект

catalyst.pl MyApp

и запускаем

myapp_fastcgi.pl -listen=localhost:9000 -nproc=10

Все просто. А вот с CGI::Application немного сложней. Модуль CGI::Application::FastCGI, как оказалось, предназначен для работы из-под "пускалки", поэтому используем не его, а непосредственно FCGI::ProcManager.

BEGIN {
$ENV{FCGI_SOCKET_PATH} = ":9000";
$ENV{FCGI_LISTEN_QUEUE} = 5;
}

use WebApp;
use CGI::Fast;
use FCGI::ProcManager;

my $proc_manager = FCGI::ProcManager->new({ n_processes => 10 });
$proc_manager->pm_manage();

while(my $query = CGI::Fast->new()) {
$proc_manager->pm_pre_dispatch();
my $app = WebApp->new(QUERY => $query);
$app->run();
$proc_manager->pm_post_dispatch();
}

Ну вот, с запуском разобрались, теперь посмотрим на использование памяти в реальных условиях. Как и ожидалось выигрыша по памяти почти нет, разумеется, когда Apache собран без излишеств.

Так что за nignx можно смело ставить либо mod_perl, либо FastCGI. Это когда один скрипт. А когда скриптов несколько и они используют множество общих модулей, то выигрыш варианта с mod_perl очевиден.

Ну вот пожалуй и все.

P.S. Если я где-то ошибся в рассуждения, просьба поправить.

20 декабря 2007, Николай.

P.P.S
Первоначально опубликовано на http://kiev.pm.org/?q=node/111.
Дату оставил как есть. :-)
В perl коде добавил вызовы pm_pre_dispatch pm_post_dispatch, которые для краткости примеров первоначально удалил.
По адресу http://kiev.pm.org/?q=node/111 смотрите ценные замечания, так же оставляйте свои.

Содержание

Об unicode в Perl
HTML::Parser vs HTML::TreeBuilder vs HTML::Gumbo
Упрощаем работу с многоуровневыми структурами данных из внешних источников
HTTP content encoding
COW in perl-5.20
Деструкторы для замыканий, часть 2
Very simple Multithreading with Continuation-passing style
Одна история с Perl, Coro и IPC::MPS
Сравниваем MongoDB с MySql как Key-Value
Деструкторы для замыканий
PostgreSQL, MySql and MariaDB as Key-Value storage
BerkeleyDB и TokyoCabinet
Redis Sharding
Стиль передачи продолжений и связывание. Оператор связывания как синтаксический сахар
IPS::MPS, AnyEvent::HTTPD, AnyEvent::HTTP and DBI
IPC::MPS - Message Passing Style of Inter-process communication
Почему Perl
"Сегодня без..." - точка, превратившаяся в запятую
each и return - опасное соседство
Reduced map
Url, обработчики и диспетчера
Perl - предотвратить случайное изменение данных
Perl Closure - последний бастион пал
Аспектно-ориентированное программирование в Perl
Tailcall оптимизация в Perl5
Parrot: PIR, PASM и L1, Скорость Rakudo
Parrot 2009 - взгляд с 2002 года
Поддержка YAML в Perl
Moose Benchmark - as swift as an arrow!
Введение в контроль версий
Объекты на замыканиях - надежная защита
Сегодня без того - не знаю чего
Сегодня без return - выворачиваем наизнанку
Сегодня без return
Сегодня без циклов, часть 2 - tail call оптимизаци...
Сегодня без циклов
Гипер-, редакшн- и кроссоператоры в Perl6
"Закрытые объекты" в Perl5 и Perl6
Многоликие объекты
Использование Perl Data Language для расчета Page Rank
Background execution of subroutines in child processes
Cooking Plain Old Documentation
Perl: Асинхронный конвейер HTTP клиентов
О ключике -w замолвите слово
Perl: Cooking warnings
Один нюанс использования map
Осторожно, Perl!
Perl JSON модули с поддержкой UTF-8
Объекты? Зачем?! Точнее, не всегда. Замыкания!
Отцы и дети или perl, fork и деструкторы
FastCGI, mod_perl и прочие