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

в 13:03, , рубрики: perl, Веб-разработка, веб-разработка и программирование

С разрешения автора и главного редактора журнала 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 = "okn";
        $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

Запуск cервера был осуществлен следующей командой:

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.

Выводы

У любой технологии есть свои нюансы. Решать, какой подход использовать, нужно, опираясь на данные по каждой конкретной задаче, но не использовать везде асинхронный подход, потому что модно.
Дмитрий Шаматрин

Автор: inquisitor_ua

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js