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

З дозволу автора і головного редактора журналу PragmaticPerl.com я публікую цю статтю.
Оригінал статті можна прочитати тут.

Продовження циклу статей присвячених розробці PSGI/Plack. Розбираємося з асинхронностью.
У попередній статті ми розглянули основні аспекти розробки під PSGI/Plack, яких, в принципі, достатньо для розробки додатків практично будь-якої складності.

Ми розібралися, що таке PSGI, розібралися як влаштований Plack, потім ми розібралися, як влаштовані основні компоненти Plack (Plack::Builder, Plack::Request, Plack::Middleware). Потім ми докладно розглянули Starman, який є хорошим PSGI-сервером, готовим для використання у production.

Нюанс
Все, що було розглянуто раніше, стосувалося розробки під модель виконання, яка називається синхронної. Зараз розглянемо асинхронну модель.

Синхронність і асинхронність
Синхронна модель це просто і зрозуміло. Все відбувається один за одним в певному порядку. Це називається процесом виконання. Розглянемо один процес інтерпретатора, який, скажімо, виконує цикл, один з елементів якого — введення інформації. Наступна ітерація циклу не буде виконана, поки не буде закінчена попередня, яка включає в себе очікування введення даних. Це — синхронна модель.

Поки користувач нічого не вводить, програма очікує введення і нічого корисного не робить. Ця ситуація називається блокуванням процесу виконання. У цьому випадку простої програми просто утилізує процесорний час. А от якщо в процесі очікування користувача програма робить щось інше, очікуючи введення, то процес стає асинхронним, а ситуація, відповідно, — неблокірующіх.

Йдемо в бар
Розглянемо в якості прикладу бар. Простий бар або паб, в якому клієнти сидять і п'ють пиво. Клієнтів багато. У барі працюють два офіціанта — Боб і Джо. Вони працюють за двома різними схемами. Боб підходить до клієнтів, приймає замовлення, йде до барної стійки, замовляє бармену келих пива, чекає, поки бармен наллє келих, відносить його клієнту, ситуація повторюється. Боб працює синхронно. Джо ж надходить зовсім по іншому. Він приймає замовлення у клієнта, йде до бармена, говорить йому: «Ей ти, налий-ка келих %beername%», потім йде приймати замовлення у наступного клієнта. Як тільки бармен наливає келих, він кличе Джо, який забирає келих і відносить його клієнту.

У цьому випадку Боб працює синхронно, а Джо, інфляція, асинхронно. Модель роботи Джо — подієво-орієнтована. Це найбільш популярна модель роботи асинхронних систем. В нашому випадку очікування введення — час, потрібний на заповнення келиха пивом, менеджер подій — бармен, а подія — це крик бармена "%beername% налито".

Проблема
Ось тепер у читачів, які ніколи не працювали з асинхронними системами, повинен виникнути питання. «А навіщо, власне, робити синхронні речі, якщо асинхронність швидше і зручніше?».

Це дуже популярне оману, але це не так. Асинхронні рішення теж мають ряд проблем і недоліків. Дуже багато де можна прочитати, що асинхронні рішення більш продуктивні, ніж синхронні. І так, і ні.

Повернемося до офіціантів. Боб працює неквапливо, розповідає анекдоти бармену, розмірено розносить келихи, а Джо постійно мотається як очманілий. Навантаження на Джо, природно, вище, т. к. він робить набагато більше всього одночасно. Навантаження ж на Боба мінімальна, поки немає клієнтів. Як тільки клієнтів стає багато, вони починають голосно вимагати своє пиво і квапити Боба. Навантаження з боку клієнта на нього зростає, але Боб продовжує працювати в тому ж темпі, йому плювати, від своєї схеми роботи він відмовлятися не збирається, і хай хоч небо впаде.

Отже, звідси можна зробити висновок, що асинхронність це непогано, але слід розуміти, що асинхронна система буде постійно перебувати під навантаженням. Навантаження, в принципі, буде така ж, як і на синхронну систему, але з однією відмінністю. Синхронна система схильна до пікових навантажень, а асинхронна ці навантаження «розмазує» за часом виконання.

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

Асинхронний PSGI/Plack
Класичне Plack-додаток (пропустимо секцію builder):

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


З коду видно, що скаляр $app містить в собі посилання на функцію, яка повертає валідний PSGI-відповідь (посилання на масив). Таким чином — це посилання на функцію, яка повертає посилання на масив. Тут можна додати асинхронність, але справи з цього не вийде, адже виконуваний процес буде блокуватися.

PSGI-додаток, який є посиланням на функцію, яка повертає посилання на масив, що має здійснитися до кінця, а тільки потім звільнити потік виконання.

Природно, цей код буде працювати правильно на будь-якому PSGI-сервері, т. к. він синхронний. Будь асинхронний сервер вміє виконувати синхронний код, але синхронний сервер асинхронний код виконувати не може. Код, наведений вище, є синхронним. У минулій статті ми трохи стосувалися такого PSGI-сервера, як Twiggy. Рекомендую встановити його, якщо його у вас ще немає. Це можна зробити кількома способами. За допомогою cpan (cpan install Twiggy), за допомогою cpanm (cpanm Twiggy), або ж взяти на github.

Twiggy
Twiggy — асинхронний сервер. Автор у Twiggy і Starman один і той же — @miyagawa.

Про Twiggy @miyagawa говорить наступне:«PSGI/Plack HTTP-сервер, який базується на AnyEvent.»

Twiggy — супермодель з 60-х, яка, як багато хто вважають, поклала початок моді на «худих», а тому що сервер дуже «легкий», «тонкий», «маленький», то назву було вибрано не випадково.

Відкладений відповідь
PSGI-додаток з відкладеним відповіддю представлено в документації наступним чином:

my $app = sub {
my $env = shift;
return sub {
my $responder = shift;

fetch_content_from_server(sub {
my $content = shift;
$responder->([ 200, $headers, [ $content ] ]);
});
};
};


Розберемося, як це працює, щоб зрозуміти, як це використовувати далі і написати свій додаток, що працює з відкладеним відповіддю.

Додаток є посиланням на функцію, яка повертає функцію, яка буде виконана після виконання деяких умов (callback). В результаті програма є посиланням на функцію, яка повертає посилання на функцію. Ось і все, що треба розуміти. Сервер, якщо встановлена змінна оточення PSGI “psgi.streaming", буде намагатися виконати цю операцію в неблокирующем режимі, тобто асинхронно.

Так як же це працює?

Якщо виконувати подібне додаток на Starman, то різниці не буде, але якщо ми будемо використовувати відкладений відповідь на асинхронному сервері, то процес виконання буде виглядати наступним чином.

  • Сервер отримує запит.
  • Сервер запитує дані звідки-небудь, звідки вони йдуть тривалий час (функція fetch_content_from_server).
  • Потім, поки чекає відповіді, він може приймати ще запити.
Якщо б модель була синхронною, то сервер не зміг прийняти жодного запиту, поки не відпрацював би попередній.

Напишемо програму, використовуючи механізм відкладеного відповіді. Додаток буде виглядати наступним чином:

use strict;
use Plack;
my $app = sub {
my $env = shift;
return sub {
my $responder = shift;
my $body = "ok\n";
$responder->([ 200, [], [ $body ] ]);
}
}

А тепер запустимо додаток як за допомогою Starman, так і за допомогою Twiggy.

Команда на запуск за допомогою Starman у нас не змінюється і виглядає наступним чином:
starman --port 8080 app.psgi


Для запуску за допомогою Twiggy:
twiggy --port 8081 app.psgi


Тепер зробимо запит спочатку до одного серверу, потім до іншого.

Запит до Starman:
curl localhost:8080/
ok


Запит до Twiggy:
curl localhost:8081/
ok


Поки-що жодних відмінностей, і сервера відпрацьовують однаково.

А тепер проведемо простий експеримент з Twiggy і Starman. Уявімо, що нам треба написати програму, яка буде виконувати за запитом клієнта, а після завершення операції звітувати про виконану роботу. Але, т. к. клієнта нам тримати не треба, скористаємося для імітації виконання чого-небудь AnyEvent->timer() для Twiggy, sleep 5 для Starman. Взагалі, sleep тут не найкращий варіант, але іншого у нас немає, т. к. код з AnyEvent в Starman працювати не буде.

Отже, реалізуємо два варіанти.

Блокуючий:
use strict;
sub {
my $env = shift;
return sub {
my $responder = shift;
sleep 5;
warn 'Hi';
$responder->([ 200, [ 'Content-Type' => 'text/json'], [ 'Hi' ] ]);
}
}

Як би ми його не запускали, хоч за допомогою Starman, хоч за допомогою Twiggy, результат завжди один. Запустимо його, для початку, за допомогою Starman наступною командою:
starman --port 8080 --workers=1 app.psgi


Увага: для чистоти експерименту треба використовувати Starman з одним робочим процесом.
Звертаючись до сервера з різних терміналів одночасно, ми можемо бачити, як ця програма виконується. Спочатку worker візьме перший запит і почне його виконувати. У цей момент другий запит буде стояти в черзі. Як тільки перший запит повністю виконається, сервер почне обробляти наступний запит.

Сумарно два запиту будуть виконуватися приблизно 10 секунд (другий запускається на обробку тільки після першого). Якщо запиту буде 3, то приблизний час виконання 18 секунд. Саме ця ситуація називається блокуванням.

Асинхронний код
Якщо запустити попередній приклад на виконання за допомогою Twiggy, результат буде такий самий. Зараз може виникнути питання, навіщо потрібен асинхронний сервер, якщо він блокується і Starman працює точно також.

Справа в тому, що для того, щоб щось працювало асинхронно, необхідний механізм, який буде забезпечувати асинхронність, цикл подій (event loop), наприклад.

Twiggy побудована навколо AnyEvent-механізму, який запускається при старті сервера. Ми можемо ним користуватися відразу ж після старту сервера. Можливо використовувати і Coro, стаття за яким теж обов'язково буде.

Тепер напишемо код, який не працювати з Starman, і отримаємо готову асинхронне додаток.

Приведемо в порядок код і зробимо додаток асинхронним. В результаті у нас повинно вийти щось такого вигляду:
sub {
my $env = shift;
return sub {
my $respond = shift;
$env->{timer} = AnyEvent->timer(
after => 5,
cb => sub {
warn 'Hi' . time() . "\n";
$respond->([200, [], ['Hi' . time() . "\n"]]);
}
);
}
}


Варто нагадати, що блокування буде завжди, від написання коду залежить те, де вони будуть. Чим менше часу сервер буде заблокований, тим краще.

Як це працює?

В першу чергу запускається таймер. Основний момент полягає в тому, що у return sub {...} необхідно присвоювати об'єкт-спостерігач (AnyEvent->timer(...)) змінної, яка була оголошена до return sub {...}, або ж використовувати condvar. Інакше таймер ніколи не буде виконаний, т. к. AnyEvent вважатиме, що функція виконана і нічого робити не треба. По закінченню таймера виникає подія, функція виконується, і сервер повертає результат. Якщо зробити з різних терміналів, наприклад, три запиту, то вони будуть виконуватися асинхронно, а по спрацьовуванню події таймера буде повернутий відповідь. Але тут найголовніше те, що блокування не відбувається. Про це свідчить результат трьох запитів, виконаних з різних терміналів, висновок STDERR:

twiggy --port 8080 app.psgi
Hi1372613810
Hi1372613811
Hi1372613812

Запуск сервера був здійснений наступною командою:

twiggy --port 8080 app.psgi


А запити виконувалися за допомогою curl:
curl localhost:8080


Нагадаємо, що preforking-сервер в класичному вигляді синхронний. Одночасність запитів обробляється за допомогою певної кількості worker'ів. Тобто якщо запустити попередній синхронний код:
use strict;
sub {
my $env = shift;
return sub {
my $responder = shift;
sleep 5;
warn 'Hi';
$responder->([ 200, [ 'Content-Type' => 'text/json'], [ 'Hi' ] ]);
}
}

з кількома worker, то вийде, що два запиту будуть виконуватися одночасно. Але тут справа не в асинхронності, а в тому, що кожен запит обробляється своїм робочим процесом. Так працює Starman, preforking PSGI server.

Візьмемо асинхронний приклад:
sub {
my $env = shift;
return sub {
my $respond = shift;
$env->{timer} = AnyEvent->timer(
after => 5,
cb => sub {
warn 'Hi' . time() . "\n";
$respond->([200, [], ['Hi' . time() . "\n"]]);
}
);
}
}

Запуск зробимо наступною командою:

twiggy --port 8080 app.psgi

і повторимо експеримент з двома одночасними запитами.

Дійсно, Twiggy працює одним процесом, проте ніщо не заважає їй виконувати в процесі очікування інші корисні дії. Це і є асинхронність.

Даний приклад був використаний виключно заради демонстрації того, як можна використовувати відкладений відповідь. Для кращого розуміння принципів роботи Twiggy рекомендується ознайомитися зі статтями, присвяченими AnyEvent у попередніх номерах журналу («Все, що ви хотіли знати про AnyEvent, але боялися запитати» і «AnyEvent і fork»).

На даний момент існує досить велика кількість PSGI-серверів, які підтримують цикли подій. А саме:
  • Feersum — асинхронний XS-сервер з нереальною продуктивністю, базується на EV.
  • Twiggy — асинхронний сервер, базується на AnyEvent.
  • Twiggy::TLS — та ж сама Twiggy, але з підтримкою ssl.
  • Twiggy::Prefork — та ж сама Twiggy, але з workers.
  • Monoceros — молодий сервер, гібридний, має в собі як синхронний, так і асинхронну частини.
  • Corona — асинхронний сервер, базується на Coro.


Висновки
У будь-якої технології є свої нюанси. Вирішувати, який підхід використовувати, потрібно, спираючись на дані по кожній конкретній задачі, але не використовувати скрізь асинхронний підхід, тому що модно.
Дмитро Шаматрин

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

0 коментарів

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