Введення в розробку web-додатків на PSGI/Plack. Частина 3. Starman

Введення в розробку web-додатків на PSGI/Plack. Частина 3. Starman.
Продовження циклу статей про PSGI/Plack. Розглянуто більш докладно preforking PSGI-сервер Starman.

З дозволу автора статті і головного редактора журналу PragmaticPerl.com. Оригінал статті розташований тут

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 вже звичне:

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.
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.

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.