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 закончена:)

    (оригинальная статья)
    Поделиться публикацией

    Похожие публикации

    Комментарии 34

      –3
      под кат и уберите дубль.
      спасибо.
        0
        Всё таки, порождением дополнительных FCGI-процессов умеют заниматься почти все веб-сервера.
        Так что, лучше эту задачу возложить на них.
          0
          Это не самая лучшая мысль, учитывая что FastCGI позволяет запускать CGIшки под аккаунтом юзера, а не под аккаунтом веб-сервера (и при этом без извратов типа suEXEC или CGIWrap) плюс держать веб-сервер и CGIшки на разных серверах. Помимо этих есть и менее явные достоинства — например возможность юзеру самостоятельно контролировать работу менеджера процессов (сколько запущено процессов, когда запускаются дополнительные, когда прибиваются, etc.) — что позволяет оптимизировать производительность вашего приложения не трогая веб-сервер.
            0
            Всё это apache'евские модули тоже умеют. Только делают это быстро и качественно.
            Работа с сокетами в перле вообще не очень шустрая, кстати.

            Бенчмарков не хватает. Пока только теоретические выкладки, не подкреплённые цифрами. Зачем городить огород с самописными примочками, вместо использования готовых простых модулей? Если есть реальный прирост производительности — это оправдано. А есть ли он на самом деле?
              0
              Коллега, Вы конкретные модули указывайте, и поясняйте, чем они лучше.
                0
                www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html
                Лучше тем, что это известный всем и отлаженный модуль.
                  0
                  Подробнее, пожалуйста. Какой модуль или какое решение из использованных в статье Вы предлагаете заменить этим апачевским модулем?
                    –1
                    Все.
                      +1
                      Ну, давайте прикинем.

                      1) На балансировщике Вместо легкого nginxa Вы предлагаете поставить тяжелый apache, из всей функциональности которого будут использоваться только модуль fastcgi.

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

                      2) На бэкендах вместо чистого fastcgi-приложения вы предлагаете использовать дополнительную прослойку в виде, опять же, тяжелого апача.

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

                      Вы знаете, как решить эти задачи грамотно и эффективно? Тогда мы с удовольствием прочитаем Вашу статью на эту тему.

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

                        nginx убивать я не предлагал, да он и не имеет прямого отношения к FastCGI.

                        Грамотно — значит так, чтобы надёжно и быстро работало. Ваш метод ненадёжен раз, и скорее всего медленен два. Смысл? Экономия пары мегабайт апачевских?
                          0
                          Вы уж определитесь, наконец. То Вы предлагаете заменить апачевским модулем ВСЕ, то вдруг оказывается, что nginx Вы убивать не предлагаете.

                          Мой метод очень надежен. Откуда у Вас вообще соображения о его ненадежности? И работает он гораздо быстрее без ненужной прослойки в виде апача.
                            –1
                            Коллега, так прочитайте свою статью — там нет слова nginx вообще!
                            Насчёт скорости — Вы же вместо быстрого апача предлагаете медленный perl. Я почему всё время про бенчмарки пишу — Вы пробовали сравнить скорости?
                              0
                              nginx фигурирует в первых частях статьи. С этого все начиналось.
                                –1
                                А я комментирую именно эту статью. И именно я в комментариях предложил вариант использования nginx, странно что Вы этого не заметили. А вот будет ли он, кстати, полноценно работать с предложенным перловым сервером — не факт, так как для апача написаны модули поддержки nginx'овских некоторых фич, придётся ещё их искать и переписывать.

                                Так какое отношение nginx имеет к fastcgi? Не правильней ли их рассматривать отдельно друг от друга? Сначала надо найти способ управлять процессами — чего пока не сделано, ибо перловый менеджер практически ничего не делает, а потом, если что-то получится, делать фронтенд.

                                Сейчас предложено заменить отработанный в куче реализаций метод своим, непроверенным и неподтверждённым экспериментально, с явными недоработками, с которыми он работать на практике не сможет.
                                  0
                                  Хватит. Вы вообще не поняли или не хотите понять, о чем говорится во всех трех связанных друг с другом частях статьи.
                                    –3
                                    Да всё я понял. У Вас есть желание написать, но нет желания понять.
                            0
                            Аргументируйте плз последний абзац чем-нибудь более обоснованным, нежели «скорее всего».
                              –2
                              Сколько я ни работал с сокетами в перле, всегда они тормозили. Почему мне надо надеяться, что в данном случае будет лучше? И ещё раз повторяю — сравнения производительности и расхода памяти нет, без этого обсуждение — сферический конь в вакууме.
                  0
                  Апачевские модули плохи тем, что для них нужен апач. Автор предлагает более легковесное решение, которое как минимум имеет свою нишу.
                  Не пробовал использовать FastCGI на маломальски нагруженных проектах — mod_perl мне более симпатичен — но в крошечной VDS с 64 метрами оперативы крутится решение, до боли похожее на то что написано в этом цикле статей. Апач туда тупо не влезет :)
                    –2
                    Во-первых есть не только апач, есть nginx+lighttpd+FastCGI, достаточно лёгкая реализация. Во-вторых, перл настолько прожорливей апача, что апачем можно просто пренебречь. В третьих, apache позволяет параметры указывать в конфигурационном файле, а не в тексте программы, да и параметры у mod_fastcgi есть, которые можно указать, в отличии от FCGI.

                    Если есть 64М, то проблем динамически запускать сервера нет — больше нескольких процессов не поместится всё равно и можно запустить фиксированное их число. А вот проблема контроля за количеством свободной памяти и загрузкой системы как раз появляется — как она решается с FCGI?..
                    • НЛО прилетело и опубликовало эту надпись здесь
                        +1
                        Черт, это была моя мысль:)
                        • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        > перл настолько прожорливей апача, что апачем можно просто пренебречь
                        Даже комментировать это не хочу. Отсутствие апача не может быть тяжелее его наличия.

                        > apache позволяет параметры указывать в конфигурационном файле, а не в тексте программы
                        А что, Вы указываете конфигурацию в теле программы? Это не для всех характерно.

                        По поводу свободной памяти и ее контроля — расковыряйте Apache::SizeLimit, который занимается под mod_perl контролем за оперативой, там есть идеи для всего, что нужно.
                          –2
                          > перл настолько прожорливей апача, что апачем можно просто пренебречь
                          >Даже комментировать это не хочу. Отсутствие апача не может быть тяжелее его
                          > наличия.
                          Почему нет? Вы же хотите апач заменить менеджером на перле. А цифр не приводите.

                          > apache позволяет параметры указывать в конфигурационном файле, а не в
                          >тексте программы
                          >А что, Вы указываете конфигурацию в теле программы? Это не для всех
                          >характерно.

                          Ну этот довод я могу принять — при желании можно конфигурационные файлы сделать. Вот только… У FCGI::ProcManager НЕТ ТОГО ЧТО МОЖНО КОНФИГУРИРОВАТЬ! Ну или почти нет.

                          >По поводу свободной памяти и ее контроля — расковыряйте Apache::SizeLimit,
                          >который занимается под mod_perl контролем за оперативой, там есть идеи для
                          >всего, что нужно.

                          Очередной велосипед? Это уже расковыряно и сделано, только не на перле.
                0
                Спасибо!!!
                  0
                  Благодарю Вас. Я рад, что моя работа приносит пользу.
                  0
                  Когда возникла необходимость перевода моей CMS на FastCGI для повышения производительности, я пришел к такому решению (потребовалась совсем небольшая модификация основного скрипта):

                  #… тут все инициализации не зависимых от запроса переменных

                  use CGI::Fast;
                  my $COUNTER = 1;

                  while (my $q = new CGI::Fast) {
                  # тут сброс всех данных, зависмых от запроса
                  $kernel->init($q);
                  # тут полезная работа скрипта
                  #…
                  # вывод результата
                  print $kernel->{cgi}->header(%{$kernel->{HEADERS}}), $RESULT;
                  }

                  # Autorestart
                  if($COUNTER >= $config::FASTCGI_MAX_COUNTER) {
                  $kernel->log(«Execute more than $config::FASTCGI_MAX_COUNTER times, reload!»);
                  exit;
                  }
                  if( -M $ENV{script_FILENAME} < 0) {
                  $kernel->log(«script modified, exit to reload!»);
                  exit;
                  }
                  $COUNTER++;
                  }

                  Понятное дело, что всю работу по демонизации, контролю числа потоков делает Апач (все, в том числе и достаточно тонкие параметры, настраивается в его конфиге).

                  При этом этот скрипт продолжает прекрасно запускаться и в обычном CGI-режиме, что полезно для универсальности CMS.
                    0
                    Ммм… А что у Вас делает код, который идет после строчки "# Autorestart"? Когда он вообще выполняется, он же находится ниже беспонечного цикла?
                      0
                      Вы правы, конечно — весь этот код должен быть внутри цикла. При вытаскивании примера лишняя скобка осталась.
                      0
                      >FASTCGI_MAX_COUNTER

                      Вот это меня в перле жутко злит — у меня тоже такой счётчик в демонах…
                      0
                      Ммм… А что у Вас делает код, который идет после строчки "# Autorestart"? Когда он вообще выполняется, он же находится ниже беспонечного цикла?
                        0
                        Тьфуты, не туда написал…
                        0
                        Спасибо, интересная статья.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое