Пользователь
0,0
рейтинг
30 июня 2014 в 15:45

Разработка → Документация Mojolicious: Потерянные Главы tutorial

Это продолжение серии статей о веб-фреймворке для Perl — Mojolicious: первая часть.

Этот цикл статей предполагает, что читатель уже поверхностно знаком с фреймворком, и у него возникла потребность разобраться в деталях, которые либо не описаны в документации, либо описаны недостаточно подробно и понятно. Для начального ознакомления идеально подходит официальная документация (на английском).

Асинхронность: синхронизируем с помощью Mojo::IOLoop::Delay


Mojo::IOLoop::Delay предоставляет механизм, обеспечивающий для асинхронно выполняющихся callback-ов:

  • описание последовательно выполняющихся операций без «лапши» callback-ов
  • передачу результатов из callback-а(ов) текущего шага на следующий
  • общие данные для callback-ов, объединённых в одну задачу
  • синхронизацию групп callback-ов
  • перехват и обработку исключений в callback-ах

Используемые термины:

  • (асинхронная) операция — обычно это вызов асинхронной функции вроде  таймера или выкачивания url, которой необходимо передать callback
  • шаг — callback, который анализирует данные полученные с предыдущего  шага (если это не первый шаг), и запускает одну или несколько новых  операций, либо возвращает финальный результат (если это последний шаг)
  • задача — список шагов, которые должны выполняться последовательно  (т.е. следующий шаг вызывается только после того, как все операции  запущенные на предыдущем шаге завершаются)

Альтернатива Promises

Это альтернативный подход к проблеме, обычно решаемой с помощью Promise/Deferred или Future. Вот приблизительное сравнение со спецификацией Promises/A+

  • Вместо цепочки ->then(\&cb1)->then(\&cb2)->… используется один вызов  ->steps(\&cb1, \&cb2, …).
  • Вместо передачи обработчика ошибки вторым параметром в ->then() он  устанавливается через ->catch(). Следствие: на все шаги этой задачи  может быть только один обработчик ошибок.
  • Результат возвращается через ->pass(), но в отличие от ->resolve()  в большинстве случаев он вызывается неявно — асинхронной операции в  качестве callback передаётся результат вызова генератора анонимных  функций ->begin, и возвращённая им функция автоматически делает  ->pass(), передавая срез своих параметров (т.е. результата работы  асинхронной операции) на следующий шаг. Следствие: не нужно писать  для каждой асинхронной функции callback, который будет возвращённый ею  результат преобразовывать в ->resolve() и ->reject().
  • Ошибки возвращаются только через исключения, аналога ->reject() нет.
  • Есть дополнительный шаг выполняемый в самом конце ->on(finish=>\&cb),  на который также можно перейти из обработчика ошибок.
  • Есть поддержка групп асинхронных операций: если на текущем шаге  запустить несколько операций, то следующий шаг будет вызван когда все  они завершатся.
  • Есть хранилище пользовательский данных, доступное всем шагам текущей  задачи.

По этим отличиям виден типичный для Mojo подход: всё что можно упрощено и предоставлены удобные «ленивчики» для типичных задач.

Что осталось за кадром

Я не буду описывать работу ->wait, с ним всё просто и понятно из официальной документации.

Кроме того, есть синонимы/альтернативы:

Mojo::IOLoop->delay(@params)
# это полный аналог более длинного:
Mojo::IOLoop::Delay->new->steps(@params)

$delay->catch(\&cb)
# это более удобный (т.к. возвращает $delay, а не \&cb,
# что позволяет продолжить цепочку вызовов) аналог:
$delay->on(error=>\&cb)

$delay→begin

Это ключевая функция, без неё использовать Mojo::IOLoop::Delay не получится. Каждый вызов ->begin увеличивает счётчик запущенных (обычно асинхронных) операций и возвращает ссылку на новую анонимную функцию. Эту возвращённую функцию необходимо однократно вызвать по завершению операции — она уменьшит счётчик запущенных операций и позволит передать результаты операции на следующий шаг (который будет запущен когда счётчик дойдёт до нуля).

Есть два способа использования ->begin: вручную и автоматически.

В первом варианте функция возвращённая ->begin запоминается во временной переменной и по завершению операции вызывается вручную:

my $delay = Mojo::IOLoop->delay;
for my $i (1 .. 10) {
    my $end = $delay->begin;
    Mojo::IOLoop->timer($i => sub {
        say 10 - $i;
        $end->();
    });
}

Во втором варианте функция возвращённая ->begin используется в качестве callback для операции:

my $delay = Mojo::IOLoop->delay;
for my $i (1 .. 10) {
    Mojo::IOLoop->timer($i => $delay->begin);
}

В обоих вариантах если определить для $delay следующий (в данном случае он же первый и единственный) шаг, то он будет вызван после завершения всех 10-ти операций:

$delay->steps(sub{ say "all timers done" });

В данном примере есть проблема: во втором варианте не выполняется say 10 - $i т.к. таймер не передаёт никаких параметров своему callback, и мы не можем узнать значение $i в callback если только не заклозурим его как в первом варианте. Но даже если бы таймер передавал $i параметром в callback вам бы это всё-равно не сильно помогло, т.к. шанс выполнить все десять say 10 - $i мы бы получили только на следующем шаге, а он запустится только после завершения всех таймеров — т.е. пропадёт эффект обратного отсчёта, когда say выполнялся раз в секунду.

В таких, редких, ситуациях необходимо использовать первый «ручной» вариант работы с ->begin. Но во всех остальных намного лучше использовать второй вариант: это избавит от временной переменной, «лапши» callback-ов, и даст возможность использовать (точнее, перехватывать) исключения в callback-ах (исключение в обычном callback-е — не «шаге» — попадёт не в $delay->catch а в обработчик исключений event loop и, по умолчанию, будет проигнорировано).

Функции ->begin можно передать параметры, и на первый взгляд (в официальную документацию) они могут выглядеть не очень понятно. Суть в том, что когда функция возвращаемая ->begin используется не в ручном варианте (когда вы сами её вызываете и контролируете с какими параметрами она будет вызвана), а в качестве непосредственного callback для операции, то она будет вызвана с теми параметрами, с которыми её вызовет эта операция. И все эти параметры вы получите как результат этой операции в параметрах следующего шага.

Например, $ua->get($url,\&cb) передаёт в callback два параметра: ($ua,$tx), и если на одном шаге запустить выкачку 3-х url, то следующий шаг получит 6 параметров (каждый шаг получает первым обязательным параметром объект $delay, а зачем в этом примере используется ->begin(0) я скоро объясню):

Mojo::IOLoop->delay(
    sub {
        my ($delay) = @_;
        $ua->get($url1, $delay->begin(0));
        $ua->get($url2, $delay->begin(0));
        $ua->get($url3, $delay->begin(0));
    },
    sub {
        my ($delay, $ua1,$tx1, $ua2,$tx2, $ua3,$tx3) = @_;
    },
);

При этом все три $ua полученные вторым шагом будут одинаковыми. Поскольку это типичная ситуация, ->begin даёт вам возможность контролировать, какие именно из переданных операцией параметров он должен передать на следующий шаг. Для этого он принимает два параметра: индекс первого параметра и их количество — чтобы передать на следующий шаг срез. По умолчанию ->begin работает как ->begin(1) — т.е. передаёт на следующий шаг все параметры переданные операцией кроме первого:

Mojo::IOLoop->delay(
    sub {
        my ($delay) = @_;
        $ua->get($url1, $delay->begin);
        $ua->get($url2, $delay->begin);
        $ua->get($url3, $delay->begin);
    },
    sub {
        my ($delay, $tx1, $tx2, $tx3) = @_;
    },
);

$delay→data

В принципе с ->data всё банально: хеш, доступный всем шагам - альтернатива передаче данных с одного шага на другой через параметры.

Mojo::IOLoop->delay(
    sub {
        my ($delay) = @_;
        $delay->data->{key} = 'value';
        ...
    },
    sub {
        my ($delay) = @_;
        say $delay->data->{key};
    },
);

Альтернативой является использование клозур, что выглядит более лениво, привычно и читабельно:

sub do_task {
    my $key;
    Mojo::IOLoop->delay(
        sub {
            $key = 'value';
            ...
        },
        sub {
            say $key;
        },
    );
}

Но здесь вас поджидает неприятный сюрприз. Клозуры живут пока кто-то на них ссылается. А по мере выполнения шагов Mojo удаляет их из памяти. Таким образом, когда будет выполнен последний шаг, ссылавшийся на заклозуренную переменную — она тоже будет удалена. Что приводит к неприятному эффекту, если эта переменная была, например, объектом Mojo::UserAgent:

sub do_task {
    my $ua = Mojo::UserAgent->new->max_redirects(5);
    Mojo::IOLoop->delay(
        sub {
            my ($delay) = @_;
            $ua->get($url1, $delay->begin);
            $ua->get($url2, $delay->begin);
            $ua->get($url3, $delay->begin);
        },
        sub {
            my ($delay, $tx1, $tx2, $tx3) = @_;
            # все $tx будут с ошибкой "соединение разорвано"
        },
    );
}

Как только первый шаг запустит неблокирующие операции выкачки url, завершится, и будет удалён из памяти — вместе с ним будет удалена и переменная $ua, т.к. больше нет шагов, которые на неё ссылаются. А как только будет удалена $ua все открытые соединения, относящиеся к ней, будут разорваны и их callback-и будут вызваны с ошибкой в параметре $tx.

Один из вариантов решения этой проблемы — использовать ->data для гарантирования времени жизни клозур не меньше, чем время выполнения всей задачи:

sub do_task {
    my $ua = Mojo::UserAgent->new->max_redirects(5);
    Mojo::IOLoop->delay->data(ua=>$ua)->steps(
        sub {
            my ($delay) = @_;
            $ua->get($url1, $delay->begin);
            $ua->get($url2, $delay->begin);
            $ua->get($url3, $delay->begin);
        },
        sub {
            my ($delay, $tx1, $tx2, $tx3) = @_;
            # все $tx будут с результатами
        },
    );
}

finish

Устанавливать обработчик события «finish» не обязательно, но во многих случаях очень удобно последний шаг указать не после остальных шагов, а обработчиком события «finish». Это вам даст следующие возможности:

  • Если используется обработчик исключений ->catch, и бывают не фатальные  ошибки, после которых всё-таки имеет смысл штатно завершить текущую  задачу выполнив последний шаг — обработчик исключений сможет передать  управление обработчику «finish» через ->emit("finish",@results), но не  сможет обычному шагу.
  • Если финальный результат получен на промежуточном шаге, то чтобы  передать его на последний шаг нужно реализовать ручной механизм  «прокидывания» готового результата через все шаги между ними — но  если вместо последнего шага используется обработчик «finish», то можно  сразу вызвать его через ->remaining([])->pass(@result).
    • Так же нужно учитывать, что если этот шаг успел запустить какие-то  операции до передачи результатов в «finish», то обработчик «finish»  будет запущен только после того, как эти операции завершатся, причём  он получит параметрами не только вышеупомянутый @result, но и всё  что вернут операции.


ВНИМАНИЕ! Делать ->emit("finish") можно только внутри обработчика исключений, а в обычном шаге нельзя. При этом в обычном шаге это же делается через ->remaining([])->pass(@result), но в обработчике исключений это не сработает.

$delay→pass

Очень часто шаг запускает операции условно — внутри if или в цикле, у которого может быть 0 итераций. В этом случае, как правило, необходимо чтобы этот шаг (обычно в самом начале или конце) вызвал:

$delay->pass;

Эта команда просимулирует запуск одной операции, которая тут же завершилась и вернула пустой список в качестве результата. Поскольку она вернула пустой список, то этот её «запуск» никак не скажется на параметрах, которые получит следующий шаг.

Дело в том, что если шаг не запустит ни одной операции вообще, то он будет считаться последним шагом (что логично — следующему шагу уже нечего «ожидать» так что в нём пропадает смысл). Иногда такой способ завершить выполнение задачи подходит, но если вы установили обработчик «finish», то он будет вызван после этого шага, причём получит параметрами параметры этого шага — что, как правило, не то, чего вы хотели.

Пример сложного парсера

Давайте рассмотрим пример, в котором используется почти всё вышеописанное. Предположим, что нам нужно скачать данные с сайта. Сначала нужно залогиниться ($url_login), потом перейти на страницу со списком нужных записей ($url_list), для некоторых записей может быть доступна ссылка на страницу с деталями, а на странице с деталями могут быть ссылки на несколько файлов «приаттаченных» к этой записи, которые необходимо скачать.

sub parse_site {
    my ($user, $pass) = @_;
    # сюда будем накапливать данные в процессе выкачки:
    # @records = (
    #   {
    #       key1 => "value1",
    #       …
    #       attaches => [ "content of file1", … ],
    #   },
    #   …
    # );
    my @records;
    # каждой запущенной задаче нужен свой $ua, т.к. можно запустить
    # несколько одновременных выкачек с разными $user/$pass, и нужно
    # чтобы в $ua разных задач были разные куки
    my $ua = Mojo::UserAgent->new->max_redirects(5);
    # запускаем задачу, удерживая $ua до конца задачи
    Mojo::IOLoop->delay->data(ua=>$ua)->steps(
        sub {
            $ua->post($url_login, form=>{user=>$user,pass=>$pass}, shift->begin);
        },
        sub {
            my ($delay, $tx) = @_;
            die $tx->error->{message} if $tx->error;
            # проверим ошибку аутентификации
            if (!$tx->res->dom->at('#logout')) {
                die 'failed to login: bad user/pass';
            }
            # всё в порядке, качаем список записей
            $ua->get($url_list, $delay->begin);
        },
        sub {
            my ($delay, $tx) = @_;
            die $tx->error->{message} if $tx->error;
            # если записей на странице не будет и никаких операций
            # на этом шаге не запустится - перейдём на следующий шаг
            $delay->pass;
            # считаем все записи
            for ($tx->res->dom('.record')->each) {
                # парсим обычные поля текущей записи
                my $record = {
                    key1 => $_->at('.key1')->text,
                    # …
                };
                # добавляем эту запись к финальному результату
                push @records, $record;
                # если есть страница с деталями - качаем
                if (my $a = $_->at('.details a')) {
                    # качаем страницу с деталями и приаттаченные к ней
                    # файлы как отдельную задачу - это немного
                    # усложнит, но зато ускорит процесс т.к. можно
                    # будет одновременно качать и страницы с
                    # деталями и файлы приаттаченные к уже скачанным
                    # страницам (плюс при таком подходе мы лениво
                    # клозурим $record и не нужно думать как привязать
                    # конкретную страницу с деталями к конкретной
                    # записи) - альтернативой было бы поставить на
                    # выкачку только страницы с деталями, а на
                    # следующем шаге основной задачи когда все
                    # страницы с деталями скачаются ставить на выкачку
                    # приаттаченные файлы
                    Mojo::IOLoop->delay(
                        sub {
                            $ua->get($a->{href}, shift->begin);
                        },
                        sub {
                            my ($delay, $tx) = @_;
                            die $tx->error->{message} if $tx->error;
                            # если файлов не будет - идём на след.шаг
                            $delay->pass;
                            # качаем 0 или более приаттаченных файлов
                            $tx->res->dom('.file a')->each(sub{
                                $ua->get($_->{href}, $delay->begin);
                            });
                        },
                        sub {
                            my ($delay, @tx) = @_;
                            die $_->error->{message} for grep {$_->error} @tx;
                            # добавляем файлы к нужной записи
                            for my $tx (@tx) {
                                push @{ $record->{attaches} }, $tx->body;
                            }
                            # нам необходимо чтобы finish вызвался без
                            # параметров, а не с нашими @tx, поэтому:
                            $delay->pass;
                        },
                    )->catch(
                        sub {
                            my ($delay, $err) = @_;
                            warn $err; # ошибка выкачки или парсинга
                            $delay->emit(finish => 'failed to get details');
                        }
                    )->on(finish => $delay->begin);
                } ### if .details
            } ### for .record
        },
    )->catch(
        sub {
            my ($delay, $err) = @_;
            warn $err; # ошибка логина, выкачки или парсинга
            $delay->emit(finish => 'failed to get records');
        }
    )->on(finish =>
        sub {
            my ($delay, @err) = @_;
            if (!@err) {
                process_records(@records);
            }
        }
    );
}

Немного не очевидным моментом является способ обработки ошибок. Поскольку результаты работы передавать между шагами не требуется (они накапливаются в заклозуренном @records), то при успехе на следующий шаг передаётся пустой список (через $delay->pass;), а при ошибке передаётся текст ошибки. Таким образом, если последний шаг в обработчике finish получит какие-то параметры — значит где-то в процессе выкачки или парсинга была ошибка(и). Саму ошибку уже перехватили и обработали (через warn) в обработчиках ->catch — собственно это как раз они и обеспечили передачу ошибки параметром в обработчик finish.

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

______________________
Текст конвертирован используя habrahabr backend для AsciiDoc.
Alex Efros @powerman
карма
302,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (20)

  • 0
    Если нужны аналогичные статьи по другим частям Mojolicious — скажите по каким. У меня пока в планах только нычка-копилка ->stash.
    • 0
      доброго времени суток, интересна статья о разработке приложений с использованием технологии websocket и особенности реализации этой технологии в mojolicious

      Зы: спасибо за статью
  • 0
    Какие преимущества / недостатки у асинхронного подхода в сравнении с форками?
    Очевидное преимущество — потребление памяти — все в одном процессе.
    Может ли асинхронное приложение справляться с нагрузкой?
    • +1
      Apache и Nginx за много лет вроде бы вполне чётко показали, что в плане производительности и памяти форки это слишком медленно и дорого, а асинхронное приложение способно справляться с громадной нагрузкой без проблем (а при необходимости держать совсем дикую нагрузку самый эффективный вариант — нафоркать по одному асинхронному приложению на каждое ядро). Но дело вовсе не в нагрузке и не в производительности — для абсолютного большинства веб-приложений в этом плане и форки и асинхронность будут работать достаточно хорошо.

      Если вся обработка входящих HTTP-запросов сводится к тому, что нужно сделать несколько SQL-запросов в базу и отрендерить шаблон с ответом — асинхронность всё только усложнит без нужды.

      Но если в процессе обработки этого запроса необходимо делать запросы в разные базы данных/redis/memcached на разные сервера, или выполнить несколько HTTP-запросов (напр. скачать какие-то данные или отправить что-то на другие сервера), или выполнить несколько RPC-запросов в разные сервисы — вот тут оказывается, что без асинхронности жить очень тяжело.

      Если делать все эти операции последовательно в одном процессе, то они займут в несколько раз больше времени, чем если бы часть их них выполнялась одновременно. Конечно, всё зависит от ситуации, но разница легко может быть раз в 10. Что любопытно, практически 99% времени процесс который этим занят — ничего не делает, только ждёт сетевого I/O… и в случае форков он просто занимает место — и в памяти, и в списке worker-ов (что легко может привести к тому, что большинство worker-ов будет «занято» и новые запросы будет некому обслуживать) — а в случае асинхронной модели этот процесс мог бы параллельно продолжать обслуживать сотни других запросов.

      Если попытаться распараллелить эти задачи форкая дополнительные процессы, то всё усложнится ещё больше, чем если бы использовалась асинхронность: мало того, что это будет сильным ударом по производительности и памяти, но гораздо важнее то, что корректно нафоркать вспомогательных процессов, проконтролировать их, обработать все ошибки и сигналы, и собрать от них результаты работы — очень нетривиальная задача (если сомневаетесь — почитайте классику вроде APUE).

      Есть, правда, одно исключение — для типовой задачи «сделать одновременно кучу HTTP-запросов» обычно есть готовое решение в библиотеках, поэтому если нужно только это, и ничего другого параллельно с выкачкой пачки url делать не нужно — можно воспользоваться таким решением (внутри оно будет реализовано через асинхронность, но от вас это будет скрыто интерфейсом библиотеки).

      Что касается моего случая, то у нас активно используется SOA, поэтому при обработке входящего запроса обычно нужно выполнить кучу запросов в другие сервисы (проверить аутентификацию, права доступа, получить профайл, …) и многие из этих запросов можно делать одновременно — именно в таких условиях асинхронный подход предпочтительнее.

      Что касается преимуществ, то если приложение работает асинхронно в одном процессе возникает масса новых возможностей:
      • т.к. процесс один нет необходимости в синхронизации, блокировках, и сложном обмене информацией между процессами (через IPC или базу данных)
      • можно держать в памяти этого процесса любые кеши, и это будет проще и быстрее чем использовать memcached — ведь все входящие запросы приходят только в этот процесс, поэтому нет необходимости во внешнем «общем» для всех процессов хранилище вроде memcached
      • можно упростить некоторые задачи благодаря тому, что легко пользоваться таймерами — например, асинхронному приложению очень легко чистить все эти кеши или обновлять какую-то информацию с определённой периодичностью (нет нужды во внешней «дёргалке», когда в системный cron добавляется задача выкачивания какой-то url нашего приложения только ради того, чтобы оно периодически выполняло какую-то операцию)
      • удобно и просто в одном приложении совмещать несколько сервисов — например, помимо обработки входящих HTTP-запросов этот же процесс может работать RPC-сервисом на другом TCP-порту

      Я предпочитаю использовать асинхронное программирование «по умолчанию», даже если в данный момент острой необходимости в этом нет. Во-первых, если позже возникнет необходимость добавить ту же работу с сервисами или сложные парсеры, а изначально приложение было не асинхронным, то его придётся серьёзно модифицировать, фактически переписать (да, такая «причина» звучит сомнительно, но как я уже говорил у нас часто используется SOA, поэтому в моей личной практике такое действительно случается достаточно часто). Во-вторых, я предпочитаю хорошо освоить один подход (который к тому же всё-равно приходится использовать при программировании фронтэнда на Javascript, GUI, и в кучке других мест — за отсутствием других вариантов) чем иметь одну кучу проблем вызванных форками и наличием группы worker-ов в одних проектах и вторую кучу проблем вызванных асинхронностью в других проектах.
      • 0
        О, ещё я забыл упомянуть о задачах вроде вебсокетов — когда сервис должен поддерживать кучу соединений с клиентами, и время от времени что-то им отправлять или получать от них запросы — здесь тоже без асинхронного программирования никуда.
      • 0
        Ещё стоит упомянуть один важный недостаток асинхронного подхода: callback-обработчик какого-то события не должен работать слишком долго, т.к. это приостановит все остальные задачи, которые выполнял этот процесс. Это касается только сложных вычислительных задач, и их приходится выносить в отдельные процессы/сервисы.
        • 0
          Да, это я и подозревал. Промахнулся комментарием, см. ниже.
      • 0
        можно держать в памяти этого процесса любые кеши, и это будет проще и быстрее чем использовать memcached — ведь все входящие запросы приходят только в этот процесс, поэтому нет необходимости во внешнем «общем» для всех процессов хранилище вроде memcached


        Как это возможно, ведь все равно основной процесс приходится форкать при старте сервиса, значит и память и кеши у каждого потомка свои?
        Это получится только, если наш сервис настолько прост, что не нуждается в форках.
        Если же в рамках обработки одного запроса делается много запросов к БД, API и другим внешним сервисам, или проводится большая вычиислительная работа, то нужно строить сервис на форках, при асинхронном подходе будет блокироваться обработка других параллельно поступивших запросов.

        Правильно?
        • 0
          Не, не правильно. Всё работает вообще без форков. В случае Mojo запускается hypnotoad, который вообще-то форкающий сервер, но я его ограничиваю одним worker-ом, так что формально процессов запущено два, но фактически все запросы обрабатывает только один.
    • 0
      Вот, кстати, отличная статья MarcusAurelius, описывающая в том числе и преимущества асинхронного подхода: Назад, к технологиям верхнего палеолита, от любимых всеми REST, STATEless, CRUD, CGI, FastСGI и MVC.
      • 0
        Интересный подход — привязывать клиента к процессу, можно на уровне nginx роутить.
        Я в своих проектах использую ExtDirect RPC для доступа API, какой смысл ограничивать себя RESTом.
        Да и чистым REST API не обойдешься, все равно нужно передавать данные, которые в URL не запихнешь. REST — дань моде).
        • 0
          При чём тут мода? REST хорош тогда, когда некоторые данные по своей сути это типичные для REST-а «ресурсы», причём есть необходимость обращаться к ним обычным HTTP-клиентам и активно их кешировать в браузерах и проксях. А в остальных случаях лучше более универсальный RPC.
          • 0
            Я имел ввиду, что его действительно часто хотят использовать там, где он архитектурно не подходит, в этом и выражается дань моде.
  • 0
    Спасибо за ответ.
    С nginx все понятно, там асинхронность уместна, т.к. работа в основном с чтение / записью в сокет.

    Если я правильно понял, вы все-таки используете форки, а каждый отдельный процесс внутри себя может использовать асинхронность для каких-то задач?

    То, что форкать подпроцесс для асинхронного вызова накладно — это понятно, есть разные подходы, как распараллелить вызовы, в основном базирующиеся на неблокирующих сокетах.

    Вопрос был «имеет ли смысл использовать чисто асинхронный подход, без форков основного процесса вообще».

    Скорее всего это возможно только для очень простых сервисов.
    • 0
      Не совсем так. Форки я не использую. Чисто асинхронный подход отлично работает в куче проектов много лет (я его активно использую примерно с 2005-го, как в линухе появился epoll — в то время пришлось самому писать XS-модуль чтобы получить доступ к epoll из перла).

      Что касается медленных чисто вычислительных задач, которые нельзя делать в callback-ах асинхронного приложения — мне пока что такие вообще не встречались. Просто держу в уме, что если такое понадобится — это нужно будет делать в отдельном процессе (а скорее всего просто отдельным сервисом).

      На самом деле много лет единственной такой «медленной» задачей была работа с MySQL т.к. не было нормальной поддержки неблокирующей работы с базой. Но сейчас уже есть поддержка в драйвере, и есть AnyEvent::DBI::MySQL, да и для других баз уже появились модули поддерживающие асинхронность, так что об этой проблеме можно забыть.
      • 0
        Очень интересно.
        Т.е. типичные задачи веб приложения нормально выполняются в одном асинхронном процессе и вы не сталкивались с недостатком производительности из-за блокировок процесса.
        Но ведь есть же различные операции, которые всегда будут блокирующими, например, расчет хеша bcrypt. Это будет серьезно тормозить общую производительность.
        Конечно, возможность держать все кеши в памяти очень подкупает, но, наверное, лучшей будет модель nginx — несколько процессов / потоков, которые могут обрабатывать запросы асинхронно.

        А как происходит работа с БД, каждая асинхронная ветка кода создает собственное соединение?
        • 0
          Что касается bcrypt, то в любом случае надо ограничивать количество запросов при обработке которых используется bcrypt — иначе тривиально заDoSить любое приложение, сколько worker-ов не нафоркай.

          Да, каждый входящий запрос использует свой собственный $dbh (используется пул уже соединённых с базой $dbh чтобы сэкономить время на connect-е). Вот пример для Mojo и AnyEvent::DBI::MySQL.
          • 0
            Использование какого асинхронного фреймворка, на ваш взгяд, наиболее оптимально?
            • +1
              А по статье не заметно? ☺

              Если серьёзно, то много лет назад я тщательно протестировал все event loop-движки для перла. Поскольку до этого я успел написать свой собственный (на epoll), то я уже знал где искать потенциальные проблемы. Так что тестирование было достаточно сложным и серьёзным. На тот момент у всех протестированных движков кроме EV были серьёзные проблемы: при некоторых условиях терялись события, утекала память, и даже бывали segfault-ы. Возможно, за прошедшие годы ситуация улучшилась, но лично я сомневаюсь: разработчики допустившие такие серьёзные ошибки в настолько критичном коде (причём не в версии 0.01 — все движки были вполне mature на первый взгляд) вряд ли внезапно перестанут делать ошибки. А event loop должен работать как часы, иначе будут возникать крайне сложно отлаживаемые баги в приложении пользователя. Поэтому с тех пор я использую исключительно EV, и всем рекомендую пользоваться только им — всё это время он отлично работал. И AnyEvent и Mojo умеют работать через EV.

              Что касается более высокоуровневых модулей — я много лет использовал свои (свой веб-фреймворк я на CPAN так целиком и не выложил, но базовые модули вроде IO::Stream выложены) пока не узнал про Mojolicous. У меня, безусловно, есть сильная тяга писать свои велосипеды, но это не из принципа и не от хорошей жизни — просто я люблю качественные и надёжные решения с идеологически правильной архитектурой, и не всегда такое удаётся найти готовое. Поэтому когда удаётся найти чужое правильное решение — я с радостью на него переключаюсь. Это же произошло и с Mojolicious — я его достаточно тщательно изучил, и пришёл к выводу что он делает примерно то же самое, что и мой фреймворк, примерно тем же способом, и вообще придраться почти не к чему. Так что я с радостью на него перешёл и пока абсолютно об этом не жалею. Возможно технически Dancer тоже неплох, но он мне идеологически не понравился — впрочем это может быть дело вкуса.

              В общем, я рекомендую, в зависимости от задач, EV, IO::Stream, AnyEvent и Mojo (в случае последних двух — проследить чтобы они использовали EV).
  • 0
    Большое спасибо за статью и за пояснения.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.