Выпуск 4. Июнь 2013

Создание RSS из списка файлов | Содержание | AnyEvent и fork

Введение в разработку web-приложений на PSGI/Plack. Часть 3. Starman.

Продолжение цикла статей о PSGI/Plack. Рассмотрен более подробно preforking PSGI-сервер Starman.

Starman?

Автор данного сервера (Tatsuhiko Miyagawa) говорит про него следующее:

Название Starman взято из из песни Star H. A. Otoko японской рок-группы Unicorn (Да, Unicorn). У David Bowie тоже есть одноименная песня, Starman — имя персонажа культовой японской игры Earthbound, название музыкальной темы из Super Mario Brothers.

Я устал от именования Perl-модулей наподобие HTTP::Server::PSGI::How::Its::Written::With::What::Module, а в результате люди называют это HSPHIWWWM в IRC. Это плохо произносится и создает проблемы новичкам. Да, может быть я упорот. Время покажет.

С названием разобрались. Теперь будем разбираться с самим сервером.

Preforking?

Preforking-модель у Starman подобна наиболее высокопроизводительным Unix-серверам. Он использует модель предварительно запущенных процессов. Также он автоматически рестартует пул воркеров и убирает свои зомби-процессы.

Plack-приложение

В этот раз Plack-приложение будет совсем элементарным:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

sub body {
    return 'body';
}

sub body2 {
    return shift;
}

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};

my $main_app = builder {
    mount "/" => builder { $app };
};

При разработке под Starman необходимо понимать один очень важный момент его работы. Рассмотрим, например, соединение с базой данных. Очень часто для того, чтобы сэкономить время и строки кода, инициализацию соединения выносят в самое начало скрипта. Это касается CGI и иногда FastCGI. В случае с PSGI так делать нельзя. И вот почему. При старте сервера этот код будет выполнен ровно один раз для каждого воркера. А опасность ситуации заключается в том, что поначалу, пока соединение не вылетит либо по таймауту, либо по каким-то еще причинам, приложение будет работать в штатном режиме. В случае с асинхронными серверами в начале кода приложения можно инициализировать пул соединений (соединение != пул соединений).

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

warn 'AFTER IMPORT';

Теперь приложение должно иметь вид:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

warn 'AFTER IMPORT';

sub body {
    return 'body';
}

sub body2 {
    return shift;
}

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};

my $main_app = builder {
    mount "/" => builder { $app };
};

Для чистоты эксперимента будем проводить запуск starman с одним воркером следующей командой:

starman --port 8080 --workers 1 app.psgi

Где app.psgi — приложение.

Незамедлительно после выполнения запуска видим следующую картину в STDERR:

noxx@noxx-inferno ~/perl/psgi $ starman --port 8080 app.psgi --workers 1
2013/06/02-15:05:31 Starman::Server (type Net::Server::PreFork) starting! pid(4204)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.

Если отправить запрос на localhost:8080/, можно убедиться, что ничего нового в STDERR не появилось, а сервер нормально отвечает.

Для того, чтобы убедиться, что worker действительно один, выполним следующую команду:

ps uax | grep starman

Результат:

noxx      4204  0.6  0.1  57836 11264 pts/3    S+   15:05   0:00 starman master --port 8080 app.psgi --workers 1
noxx      4205  0.2  0.1  64708 13164 pts/3    S+   15:05   0:00 starman worker --port 8080 app.psgi --workers 1
noxx      4213  0.0  0.0  13580   940 pts/4    S+   15:05   0:00 grep --colour=auto starman

Процесса два. Но на самом деле worker из них только один. Проведем еще один эксперимент. Запустим starman с тремя воркерами.

starman --port 8080 --workers 3 app.psgi

Результат:

2013/06/02-15:11:08 Starman::Server (type Net::Server::PreFork) starting! pid(4219)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.

Все верно. Теперь посмотрим на список процессов. У меня он выглядит так:

noxx      4219  0.1  0.1  57836 11264 pts/3    S+   15:11   0:00 starman master --port 8080 app.psgi --workers 3
noxx      4220  0.0  0.1  64460 12756 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4221  0.0  0.1  64460 12920 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4222  0.0  0.1  64460 12756 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4224  0.0  0.0  13580   936 pts/4    S+   15:12   0:00 grep --colour=auto starman

Один мастер, три воркера.

С порядком выполнения разобрались. Теперь добавим еще один warning.

warn 'IN BUILDER'

Приложение выглядит следующим образом:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

warn 'AFTER IMPORT';

sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

Для одного worker-процесса вывод выглядит так (команда запуска: starman --port 8080 --workers 1 app.psgi):

2013/06/02-17:33:27 Starman::Server (type Net::Server::PreFork) starting! pid(4430)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.

Если же мы запустим приложение с тремя воркерами, то увидим следующую картину в STDERR:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.

Сделав запрос на localhost:8080/, легко можно убедиться в том, что ничего нового в STDERR не появилось.

Можно сделать следующие выводы:

  • Данное действие будет выполняться при старте приложение. Это справедливо как для начала скрипта, так и для builder-секции, если она есть.
  • Данное действие не будет выполняться при запросах на сервер.
  • Рабочие процессы Starman стартуют последовательно.

Это дает возможность конструировать тяжелые объекты как при старте скрипта, так и в builder-части.

А вот теперь добавим в код еще один warning следующего вида:

warn 'REQUEST';

И приведем приложение к следующему виду:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

warn 'AFTER IMPORT';

sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

Теперь запустим приложение с одним рабочим процессом (starman --port 8080 --workers 1 app.psgi). Пока что ничего не изменилось:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24.

Но стоит сделать запрос, как в STDERR появится новая запись.

REQUEST at /home/noxx/perl/psgi/app.psgi line 16.

Подведем итог. При каждом запросе к starman будет выполняться только код непосредственно приложения (стоит вспомнить return sub ...), но при старте этот код выполняться не будет.

А теперь, допустим, один процесс упал. Добавим следующую строчку в return sub ...:

die("DIED");

В результате должны получить приложение следующего вида:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

warn 'AFTER IMPORT';

sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    die("DIED");
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

Запускаем приложение с одним рабочим процессом, делаем запрос. Приложение, естественно, падает. Но результат любопытен, хотя и закономерен. Приложение не упало, в STDERR появилось только два уведомления:

REQUEST at /home/noxx/perl/psgi/app.psgi line 16.
DIED at /home/noxx/perl/psgi/app.psgi line 21.

А теперь заменим die('DIED'); на exit 1;. Запустим Starman, сделаем запрос на localhost:8080/. Вот теперь рабочий процесс упал. Это видно по STDERR, который будет выглядеть теперь так:

REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <$read> line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 8.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 26, <$read> line 8.

После каждого запроса рабочий процесс будет падать, но master-процесс будет его поднимать.

Оставим Starman ненадолго. Попробуем запустить данное приложение, например, под Twiggy. Если данный сервер не установлен, то самое время его установить. Пакет называется Twiggy.

После установки Twiggy запустим наше приложение следующей командой:

twiggy --port 8080 app.psgi

И сделаем запрос. Все как у Starman, за исключением одной особенности. Сервер свалился.

noxx@noxx-inferno ~/perl/psgi $ twiggy --port 8080 app.psgi
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 26.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <> line 5.
noxx@noxx-inferno ~/perl/psgi $

Разумеется, это потому, что у Twiggy отсутствует мастер-процесс и поднять упавшего рабочего некому. А теперь отсюда следует очень важный момент, который надо учитывать. Перед рестартом сервера необходимо убедиться в том, что его код корректен и не содержит синтаксических ошибок. Если попробовать запустить приложение, которое содержит ошибку, при помощи Starman, произойдут несколько событий в следующем порядке:

  • Starman запустит master-процесс, проверит, может ли он запустить рабочие процессы.
  • Starman запустит рабочие процессы и передаст на исполнение код приложения.
  • Рабочие процессы начнуть падать, а мастер начнет их поднимать.

Нагрузка увеличивается невероятно и за очень короткий промежуток времени.

Ошибки во время исполнения не настолько критичны. Уберем падения из приложения, приведя его практически к начальному виду:

use strict;
use warnings;

use Plack;
use Plack::Builder;
use Plack::Request;

warn 'AFTER IMPORT';

sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

И попробуем сделать следующее именно в таком порядке:

  • Приводим приложение к начальному виду.
  • Запустим его при помощи Starman.
  • Сделаем запрос.
  • Изменим код приложения и сохраним его.
  • Не рестартуя приложение сделаем запрос на него еще раз.

Результат:

curl localhost:8080/
body

Сохраняем приложение, меняем функцию body. Пусть теперь, например, она возвращает nobody. Делаем запрос — результат, если мы не рестартовали сервер, следующий:

curl localhost:8080/
body

Но стоит сделать рестарт, как все меняется:

curl localhost:8080/
nobody

Еще один важный вывод. Для того, чтобы приложение обновилось, изменения файлов недостаточно. Необходимо перезапустить сервер. Или же отправить специальный сигнал мастер-процессу.

Starman и сигналы

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

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

Сигнал, который указывает Starman, что надо бы перечитать — SIGHUP.

Команда на отправку данного сигнала выглядит так:

kill -s SIGHUP [pid]

Получить значение pid можно следующей командой:

ps uax | grep starman | grep master

Пример вывода команды:

noxx      6214  0.8  0.1  54852 10288 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi

pid = 6214.

Проверяем запрос-ответ. Заменяем nobody обратно на body и запускаем приложение.

Результат:

curl localhost:8080
body
kill -s SIGHUP 6214
curl localhost:8080
nobody

А тем временем в STDERR Starman мы можем видеть следующее:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16.
Sending children hup signal
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 2.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 2.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <$read> line 2.

Таким образом, есть два способа обновления PSGI-приложения. Какой выбирать — зависит от задачи.

Допустим, понадобился еще один рабочий процесс. Его можно добавить двумя способами. Рестартовать сервер с необходимым параметром (--workers) или же отправить сигнал. Сигнал на добавление одного рабочего процесса — TTIN, на удаление — TTOU. Если же мы хотим полностью безопасно остановить сервер, мы можем воспользоваться сигналом QUIT.

Итак. Запустим наше приложение с одним рабочим процессом:

starman --port 8080 --workers 1

Затем добавим два процесса, выполнив следующую команду дважды:

kill -s TTIN 6214

Список процессов Starman:

noxx      6214  0.0  0.1  54852 10304 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi
noxx      6221  0.0  0.1  64724 13188 pts/3    S+   19:19   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6233  0.0  0.1  64476 12872 pts/3    S+   19:26   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6239  2.0  0.1  64480 12872 pts/3    S+   19:29   0:00 starman worker --port 8080 --workers 1 app.psgi

В STDERR уже привычное:

BAFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 4.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 4.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 4.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 4.

Затем уберем один процесс:

kill -s TTOU 6214

Можем видеть, что команда возымела эффект, посмотрев на список процессов:

noxx      6214  0.0  0.1  54852 10304 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi
noxx      6221  0.0  0.1  64724 13188 pts/3    S+   19:19   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6233  0.0  0.1  64476 12872 pts/3    S+   19:26   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6238  0.0  0.0  13584   936 pts/4    S+   19:29   0:00 grep --colour=auto starman

Но в STDERR это не отобразится.

А теперь завершим работу нашего приложения, отправив ему сигнал QUIT.

kill -s QUIT 6214

Сервер пишет в STDERR:

2013/06/02-19:32:15 Received QUIT. Running a graceful shutdown
Sending children hup signal
2013/06/02-19:32:15 Worker processes cleaned up
2013/06/02-19:32:15 Server closing!

И завершает работу.

Это все, что необходимо знать о Starman для того, чтобы начать с ним работать.

Осталась еще одна важная деталь. При запуске Starman можно указать через ключ -M необходимый модуль для загрузки через master-процесс. Но тогда начинает работать следующее ограничение. Модули, загруженные через -M (-MDBI -MDBIx::Class), при SIGHUP перечитываться не будут.

Еще одна полезная опция сервера — -I. Она позволяет указать путь Perl-модулям перед стартом master-процесса. Starman умеет также работать с Unix-сокетами, но эта возможность будет рассмотрена подробнее в следующих статьях, начиная со статьи по разворачиванию и администрированию Plack.

Ну и напоследок — флаг -E, который устанавливает переменную окружения (PLACK_ENV) в переданное состояние.

Следующая статья будет посвящена асинхронному PSGI-серверу — Twiggy.

Дмитрий Шаматрин


Создание RSS из списка файлов | Содержание | AnyEvent и fork
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье