Perl
March 2009 4

FastCGI-приложение на Perl. Часть третья.

В предыдущей статье был продемонстрирован способ демонизации FastCGI-приложения. Получившийся демон успешно обрабатывает запросы, но у него есть один существенный недостаток — он не умеет обрабатывать несколько запросов одновременно.

Более того, в плане обработки нескольких одновременных запросов ситуация с демоном даже хуже, чем с обычным CGI-приложением. Если CGI-приложение может быть запущено веб-сервером в нужном количестве экземпляров (например, по экземпляру на каждый поступивший запрос), то демон, работающий в одном единственном экземпляре, будет вынужден ставить поступившие запросы в очередь. Пока предыдущий запрос не будет выполнен, все остальные запросы должны будут ждать.

Для того, чтобы FastCGI-приложение могло обслуживать несколько запросов одновременно, оно должно уметь создавать собственные копии. В этом случае одновременно пришедшие запросы будут обрабатываться параллельно несколькими экземплярами приложения.

А как создавать копии?

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

Модуль FCGI::ProcManager выполняет три основные задачи:

1) Создает рабочие копии демона — обработчики или воркеры (или серверы, в терминологии самого модуля)
2) Контролирует состояние обработчиков в процессе работы
3) Управляет поведением обработчиков в случае внешнего вмешательства

Помимо процессов-обработчиков FCGI::ProcManager запускает еще процесс-менеджер. Процесс-менеджер не обслуживает клиентские запросы, его задача — управление обработчиками.

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

Рассмотрим участок кода из предыдущей статьи (в сокращенном виде):

# Демонизация {
    #...тут я сократил...

    POSIX::setuid(65534) or die "Can't set uid: $!";

    reopen_std();
# }

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

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

while($request->Accept() >= 0) {

Команда reopen_std разрывает связь стандартных дескрипторов с консолью. Это означает, в частности, что сообщения обо всех ошибках, которые могут произойти после выполнения этой команды (например, в функции OpenSocket), будут отправлены в никуда и приложение просто молча умрет. Это и будет тем самым неприятным сюрпризом, о котором я говорил — кажется, что приложение нормально стартовало, но в списке процессов его вдруг волшебным образом не оказывается.

Ситуация станет еще хуже после того, как будет прикручено распараллеливание. Обработчик, вызвавший ошибку, будет убит, но на его место процессом-менеджером тут же будет запущен другой. В нем снова произойдет ошибка, он снова будет убит и так по кругу. Внешне все это будет выглядеть вполне безобидно — демон запустился, никаких ошибок не вывел, в списке процессов четко видно наличие заданного количества обработчиков. Однако, на запросы демон не отвечает и, что еще хуже, через некоторое время вы вдруг заметите, что система стала беспощадно тормозить, процессор занят на 100% и load average неумолимо растет.

Система — сюрприз! — будет занята диспетчеризацией непрерывно и с бешеной скоростью умирающих и вновь запускающихся процессов. Потребуется большая внимательность, чтобы заметить, что pid'ы обработчиков непрерывно изменяются, сообразить, что это означает и принять меры.

Во избежание такого сюрприза вызов функции reopen_std нужно отделить от остального кода из блока демонизации. Разместить вызов этой функции нужно непосредственно перед циклом обработки запросов.

Возьмем все тот же участок кода и внесем изменения:

# Демонизация {
    #...тут я сократил...

    POSIX::setuid(65534) or die "Can't set uid: $!";
# }

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

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

# Демонизация {
    reopen_std();
# }

while($request->Accept() >= 0) {

Как видите, при этом команды OpenSocket и Request окажутся как-бы «внутри» процесса демонизации. Другими словами, процесс демонизации будет разделен на две части, что было бы невозможно, если бы мы использовали для демонизации готовый модуль.

Теперь сообщение о любой ошибке, произошедшей до вызова команды reopen_std, будет выведено на консоль. Соответственно, мы сможем сразу увидеть, что что-то пойдет не так.

Ну, а теперь возьмем код демона из предыдущей статьи и встроим в него распараллеливание:

#!/usr/bin/perl

# Для пущего порядку
use strict;
use warnings;

# Этот модуль реализует протокол FastCGI
use FCGI;

# Этот модуль для разговора с операционкой по понятиям:)
use POSIX;

# Распараллеливание {
    # Этот модуль обеспечивает параллельную обработку запросов
    use FCGI::ProcManager qw(pm_manage pm_pre_dispatch pm_post_dispatch);
# }

# Форк
# избавляемся от родителя
fork_proc() && exit 0;

# Начать новую сессию
# наш демон будет родоначальником новой сесcии
POSIX::setsid() or die "Can't set sid: $!";

# Перейти в корневую директорию
# чтобы не мешать отмонтированию файловой системы
chdir '/' or die "Can't chdir: $!";

# Сменить пользователя на nobody
# мы же параноики, ага?
POSIX::setuid(65534) or die "Can't set uid: $!";

# Открываем сокет
# наш демон будет слушать порт 9000
# длина очереди запросов - 5 штук
my $socket = FCGI::OpenSocket(":9000", 5);

# Начинаем слушать
# демон будет перехватывать стандартные дескрипторы
my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);

# Распараллеливание
    # Запуск обработчиков
    # будет запущено указанное количество обработчиков (в данном случае 2)
    pm_manage(n_processes => 2);
# }

# Специфика {
    # Тут должен располагаться код, специфичный для каждого конкретного обработчика
    # например, открытие соединения с базой
# }

# Переоткрыть стандартные дескрипторы на /dev/null
# больше не разговариваем с пользователем
reopen_std();

my $count = 1;

# Бесконечный цикл
# при каждом принятом запросе выполняется один "оборот" цикла.
while($request->Accept() >= 0) {
    # Распараллеливание
        # Управление обработчиками
        # реагирует на внешнее вмешательство
        pm_pre_dispatch();
    # }
   
    # Внутри цикла происходит выполнение всех требуемых действие
    print "Content-Type: text/plain\r\n\r\n";
    print "$$: ".$count++;

    # Распараллеливание
        # Управление обработчиками
        # реагирует на внешнее вмешательство
        pm_post_dispatch();
    # }
};

# Форк
sub fork_proc {
    my $pid;
   
    FORK: {
        if (defined($pid = fork)) {
            return $pid;
        }
        elsif ($! =~ /No more process/) {
            sleep 5;
            redo FORK;
        }
        else {
            die "Can't fork: $!";
        };
    };
};

# Переоткрыть стандартные дескрипторы на /dev/null
sub reopen_std {   
    open(STDIN,  "+>/dev/null") or die "Can't open STDIN: $!";
    open(STDOUT, "+>&STDIN") or die "Can't open STDOUT: $!";
    open(STDERR, "+>&STDIN") or die "Can't open STDERR: $!";
};

Какие тут есть характерные особенности?

Прежде всего, обратите внимание на расположение команды pm_manage.

С одной стороны, команды, общие для всего FastCGI-приложения (такие, как создание сокета и начало прослушки) должны выполняться ДО запуска обработчиков. Нельзя запустить обработчики, а потом биндится на один сокет несколькими процессами, это приведет к ошибке.

С другой стороны, команды, специфичные для каждого конкретного обработчика (такие, как открытие соединения с базой данных) должны располагаться ПОСЛЕ запуска обработчиков. Нельзя создать соединение к БД, а потом расшаривать его по процессам, это приведет к ненужным проблемам.

Ну и, еще раз напоминаю, команда reopen_std должна располагаться после всех подготовительных команд, непосредственно перед началом цикла.

Цикл должен начинаться и заканчиваться командами pm_pre_dispatch и pm_post_dispatch соответственно. Эти две команды управляют поведением обработчиков в случае внешнего вмешательства. Под внешним вмешательством подразумевается получение FastCGI-приложением сигнала, например, от команды kill. Без них обработчики не будут реагировать на сигналы правильным образом.

Запускаем демона. При запуске демон выведет на консоль примерно следующее:

# ./test.pl
FastCGI: manager (pid 1858): initialized
FastCGI: manager (pid 1858): server (pid 1859) started
FastCGI: server (pid 1859): initialized
FastCGI: manager (pid 1858): server (pid 1860) started
FastCGI: server (pid 1860): initialized

Здесь мы видим сообщения о том, что запустились процесс-менеджер (pid 1858) и два обработчика (pid'ы 1859 и 1860).

Посмотрим список процессов:

# ps -aux | grep perl
nobody 1858 0,0 0,2 5852 3816 ?? Is 21:09 0:00,00 perl-fcgi-pm (perl5.8.8)
nobody 1859 0,0 0,2 5852 3848 ?? I 21:09 0:00,00 perl-fcgi (perl5.8.8)
nobody 1860 0,0 0,2 5852 3848 ?? I 21:09 0:00,00 perl-fcgi (perl5.8.8)

Здесь мы видим, что процесс-менеджер отличается от обработчиков незатейливым суффиксом «pm».

Для управления FastCGI-приложением сигналы нужно посылать процессу-менеджеру, а не обработчикам. Процесс-менеджер разруливает два сигнала, HUP и TERM. Делает он это следующим образом:

При получении сигнала HUP процесс-менеджер отправляет всем обработчикам сигнал TERM. Обработчики умирают, процесс-менеджер запускает новые. Таким образом осуществляется решение проблем с зависшими обработчиками.

При получении сигнала TERM процесс-менеджер посылает всем обработчикам сигнал TERM, ждет, пока они умрут, затем умирает сам. Если обработчики не желают умирать добровольно, процесс-менеджер посылает им сигнал KILL, от которого уже не отвертеться.

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

Это поведение можно изменить, добавив в вызов функции Request еще один параметр — FCGI::FAIL_ACCEPT_ON_INTR (это константа, экспортируемая модулем FCGI):

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

После добавления этого параметра обработчики будут умирать немедленно, как только был получен сигнал. Этот параметр удобно использовать на этапе отладки, чтобы не ждать, пока все обработчики доделают свою работу.

На этом трилогия о создании FastCGI-приложений на Perl закончена:)

(оригинальная статья)
+15
4.8k 39
Comments 34