Выпуск 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 →