All streams
Search
Write a publication
Pull to refresh
82
0
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

Send message
Опять доказательство по аналогии.

Давайте еще раз: статья о том, что происходит на практике при использовании акторов Хьюита. На практике акторы не защищены от перегрузок. И с этим нужно жить.

Тем, кому приходится с этим жить, как-то все равно, есть ли теоритические работы, описывающие общие случаи акторов с N входами и M выходами, или таковых нет. А если они и есть, то как там обстоят дела с защитой от перегрузки (пока что советы теоритиков вызывают смех в зале).

Попробуйте дать обычному разработку на Akka формулы из статьи «A STRUCTURED DESCRIPTION OF DATAFLOW ACTORS AND ITS APPLICATION», на которую вы ссылаетесь. И посмотрите, как это поможет при разработке реальной программной системы.
ясно любому, кто пытался реализовать и то, и другое.

Где можно посмотреть на ваши реализации?
И выделять акторы с 2 входами в отдельную категорию так же наивно, как выделять сложение и умножение из всего множества математических функций только на том основании, что у них 2 аргумента.

Может проблема в том, что вы говорите про математические функции, а я про практику использования акторов? На практике у акторов нет двух входов.

Сюрприз, правда?

Вам хочется думать, что с точки зрения математики, второй вход для актора — это его состояние. На практике, однако, состояние актора не является для него входом. Особенно если мы находимся в рамках императивных ОО-языков.

Вот и получается, что с точки зрения теории и досужих рассуждений с доказательствами по аналогии — у вас одно. А на практике у меня, скажем, несколько другое. И какой-то дополнительный «вход» для обратной связи привязывать-то и некуда.
Или разбить актор С на 2 стадии — первая получает ссылку на B и делает запрос к B на резервирование места, посылая в запросе ссылку на вторую стадию.

Спасибо, повеселили.

Вот этот «запрос к B на резервирование места» — он какой будет? Синхронный или асинхронный? Если асинхронный, то чем этот запрос отличается от простой и тупой прямой отсылки нужного сообщения? И кто гарантирует, что сам этот запрос не станет причиной перегрузки B?
Сильно зависит от контекста. Если для обучения арифметическим операциям детей в начальных классах школы — то, конечно же, не рассматриваю. При этом вы уходите на доказательства по аналогии, что суть демагогия.

Проблема в том, что вы выдвинули тезис о том, что actor model — это частный случай dataflow. При том, что термин actor model уже является устоявшимся термином. И нужно что-то более существенное, нежели аналогия с арифметическими операциями, для обоснования вашего термина.

Видите ли, вы ссылаетесь на работу от 2003-го года, в которой авторы сами пишут:
In all dataflow models of computation, the model components (which we call actors,
and which might also be called processes in other models of computation) communicate
by sending each other packets of data called tokens along unidirectional channels with
exactly one reader and one writer.

Они английским по белому говорят «которые мы называем _акторами_, но которые могут быть названы процессами в других моделях». Т.е. авторы одной работы, вышедшей через 30 лет после работы Хьюита используют термин _актор_ в контексте dataflow-а. При том, что они пытаются ввести такое понятие, как dataflows actors. Не просто actors, а именно dataflow actors.

Больше напоминает, что вы прочли про dataflows actors, вам показалось, что это обобщение, в том числе, и Хьюитовских акторов, и теперь вы носитесь с этой идеей. Ничего не имею против ваших личных взглядов на проблемы dataflows actors.

Однако, в статье речь идет про Хьюитовских акторов и их проблемы, в том числе и проблемы отсутствия защиты от перегрузки. А вовсе не о так любимых вами dataflow actors.
Ну и зачем этот вход акторам C, D и E? Более того, откуда этот вход будет появляться во run-time, если, скажем, ссылку на B актор C получает в run-time, отправляет актору B одно-единственное сообщение и выбрасывает данную ссылку как ненужную?
Собственно проблема в том, что при описании dataflow и workflow сетей (а по большому счету, это одно и то же) узлы этих сетей обычно называют не акторами, а как-нибудь по другому

Ну да. Там будет про dataflow/workflow. Но не про «традиционную» модель акторов от Хьюита.
хотя они именно акторы и есть

Или это вам лично так кажется.

Чтобы было понятно: есть уже давно ставшим общеупотребительным термин «модель акторов», под которым более-менее понятно, что понимается. Вы, как приверженец dataflow-модели, можете считать, что модель акторов есть частный случай dataflow-модели. Однако вряд ли ваш взгляд на вещи найдет широкое понимание. У меня, например, не находит.
Dataflow актор защитить от перегрузки легко — стоит лишь добавить обратную связь по переполнению и завести ее на добавочный вход.

Попутно вопрос: допустим, есть актор B, на которого сыпятся сообщения от акторов C, D, E и далее по списку. Общий темп отсылки сообщений актору B превышает способности B по обработке входящих сообщений. Этот самый «добавочный вход» кому нужно завести и куда?
Мне казалось, что «модель акторов» (она же Actor Model на английском) — это именно то, что было озвучено сперва Хьюитом, а потом подхвачено Клингером и Агха. Об этой модели акторов и идет речь.

Где можно прочитать про «модель акторов» с другими корнями?
Ну и в современных реализациях модели Акторов супервизоры стали стандартом де-факто.
Есть ли, скажем, супервизоры в такой реализации, как Orleans?
Здесь не соглашусь, так как именно эти свойства являются в модели Акторов основопологающими.

Вы же сами ниже описываете, что является основополагающим:
— создавать новые акторы
— посылать сообщения
— устанавливать, как следует реагировать на последующие полученные сообщения

Вот это и есть основополагающие свойства модели акторов. А свойства из Reactive Manifesto — это свойства реактивных систем, которые можно создавать и без использования модели акторов.
И многие системы применяют модель Акторов, чтобы достичь масштабируемости.

Это говорит лишь о том, что модель акторов применима и имеет некоторые преимущества в таких задачах. Но вовсе не о том, что responsive+resilient+elastic+message-driven являются свойствами модели акторов.
В Erlang Actor-ы называются process, но при этом они не перестают быть Акторами.

Сейчас Erlang считается одной из основных реализаций модели акторов. Но ирония в том, что описывая историю создания Erlang-а Джо Армстронг не говорил о том, что он использовал модель акторов в разработке Erlang-а (хотя этой модели к моменту начала работ над Erlang-ом уже было 13 лет), Erlang создавался под влиянием Prolog-а и Smalltalk-а. Так что авторы Erlang-а просто переизобрели модель акторов. И создали систему супервизоров, которой в модели акторов нет.
Зря вы привязали четыре свойства реактивных систем (responsive, resilient, elastic и message-driven) к свойствам модели акторов. Их старательно увязывают вместе продавцы услуг из Lightbend-а, которые, по совместительству, являются и разработчиками Akka, и соучастниками написания этого самого Reactive Manifesto. На самом же деле Actor Model вовсе не гарантирует, что разработанные на ее базе приложения будут иметь более-менее нормальную отзывчивость и/или масштабируемость. И, обратно, responsive+resilien+elastic+message-driven системы могут быть сделаны и без использования модели акторов.

По поводу Actors vs Microservices. Модель акторов вовсе не обязывает деплоить один и тот же инстанс приложения на все ноды. Запросто можно разрабатывать приложения на акторах так, что в одном компоненте приложения работают акторы типа X и Y, а в другом компоненте — акторы типов Z, V и W. При этом компоненты могут деплоится на разные ноды. И как раз то, что общение между акторами построено на базе асинхронного обмена сообщениями, а акторы не имеют разделяемого состояния, позволяет делать это безболезненно.

Так что Actors и Microservices в вашей терминологии соотносятся так же, как Microservices и SOA: microservices могут быть частью SOA, а акторы могут быть частью реализации микросервиса.

Ну и по поводу существующих реализаций модели акторов: имя им легион (языки программирования: https://en.wikipedia.org/wiki/Actor_model#Later_Actor_programming_languages, фреймворки для универсальных языков: https://en.wikipedia.org/wiki/Actor_model#Actor_libraries_and_frameworks). Сам же Erlang создавался без оглядки на Actor Model (автор Erlang-а описывая историю разработки языка не говорил, что использовал модель акторов).
Значит всё-таки решение задачи ложных или повторных срабатываний таймера остаётся разработчику.
Я бы не сказал, что это «ложные и повторные». Это скорее очередная гримаса многопоточности, особенно во времена реальных многоядерных машин. Ведь два потока действительно могут работать параллельно и независимо, от чего взаимные сочетания «кто кого обогнал, кто от кого отстал» могут принимать самые причудливые формы.
Я просто считал, что [T-d1, T+d2] входит в документированную погрешность, которую нужно учитывать. Разве что, думал, что по таймерам есть гарантия «сработает НЕ РАНЬШЕ заданного времени»(т.е. доставка будет только в момент >=T).
Это, кстати, хороший момент. Тут зависит от таймерного механизма. Если нет округления времени срабатывания (например, в механизмах timer_heap и timer_list), то таймер срабатывает только в момент >=T. А вот как для timer_wheel, где T должно попадать в «окно»… Наверное, тоже >=T, но навскидку не вспомню. Кроме того, пользователь может и свой таймерный механизм подсунуть, что у него будет — хз…

Но тут другое важно. Допустим, что агент A решает отменить заявку в (T-d1), он пытается вызвать timer_.release() и тут его нить вытесняется операционкой. Проходит немного времени и наступает момент T, нить таймера выставляет заявку. Тут просыпается нить агента A и выполнение timer_.release() продолжается. Но заявка уже в очереди. Вероятность этого невысока, но, как ни странно, на больших нагрузках она регулярно трансформируется в реальность.
К сожалению, я не понял идею такого API :(

Отменить таймер можно и сейчас. Например:
so_5::timer_id_t timer = so_5::send_periodic<msg>(*this,
  std::chrono::milliseconds(250), // Задержка.
  std::chrono::milliseconds::zero() // Нет повторения, однократная доставка.
);
...
timer.release(); // Отменили таймер.
Но тут вот в чем проблема, если таймер должен сработать в момент T, то на самом деле отложенное сообщение может стать в очередь получателя в диапазоне [T-d1, T+d2], где d1 и d2 — это очень маленькие величины, но, к сожалению не нулевые.

Предположим, что отложенное сообщение встает в очередь в момент времени (T-20us), а агент в момент времени (T-15us) отменяет таймер. Реальной отмены не будет, т.к. сообщение уже в очереди получателя.

И тут есть всего два надежных способа:

1. Самый простой. Передавать в отложенном сообщении некий прикладной ID. Как правило, в каждой задаче этот ID разный. Где-то строковый идентификатор, где-то указатель.

2. Создать отдельный mbox, подписаться на сообщение из него. Отослать отложенное сообщение в этот mbox. Затем, при отмене, нужно и отменить отложенное сообщение, и снять подписку с этого mbox-а. В таком случае даже если сообщение уже в очереди получателя, то оно будет проигнорировано, т.к. подписки на него уже нет.

Второй способ достаточно накладный. Хотя вот реализация time_limit для state_t сейчас работает именно по такому принципу.
Ну или моя задача не особо на них ложится и я пытаюсь притянуть ее за уши.
Ну или же ее можно решить на КА, но за счет дополнительных сообщений и, возможно, еще одного состояния.

Например:
st_neutral:
..on_enter: отослать себе check_queue
..msg_check_queue -> если очередь пуста, то идем в st_wait_command, если не пуста, то идем в st_wait_perform;

st_wait_command:
..msg_command -> поставить заявку в очередь, перейти в st_wait_perform;

st_wait_perform:
..on_enter: инициировать первую операцию из очереди;
..msg_io_result: обработать результат, перейти в st_neutral;
..msg_command: поставить заявку в очередь;
..time_limit: перейти в st_io_timedout;

st_io_timeout:
..on_enter: обработать тайм-аут для текущей операции, отослать себе msg_check_queue;
..msg_check_queue: делегировать обработку msg_check_queue состоянию st_neutral;

Правда, не уверен, что эта логика оказывается проще и эффективнее.

Делегировать обработку сообщения из текущего состояния в другое состояние S можно посредством метода state_t::transfer_to_state (пример здесь).
И тут бы идеально подошел time_limit для состояния st_wait_perform.
Ну вот тут я не уверен. time_limit хорош для безусловного перехода в другое состояние, когда не приходится при выходе анализировать, что успели сделать, что не успели.

У вас же, при использовании time_limit, потребуется проверять некоторый признак, пришел ли уже результат IO-операции или нет. Это дополнительный атрибут в агенте, дополнительная логика и т.д.

Если у вас агент входит в st_wait_perform и может в этом состоянии обрабатывать несколько IO-операций последовательно, то обычное отложенное сообщение выглядит более удобным решением, чем time_limit. При этом, однако, нужно не забыть вот о чем: если вы взвели отложенное сообщение M(1) для операции OP(1), потом операция OP(1) у вас успешно закончилась и вы успели начать операцию OP(2) (отправив M(2) для контроля тайм-аута OP(2)), то вы запросто можете получить M(1) и принять его за M(2). Например, у вас может быть что-то вроде:
class io_performer : public so_5::agent_t {
  struct io_timeout : public so_5::signal_t {};
  ...
  void on_next_operation(mhood_t<start_next_io>) {
    // Начинаем отсчет времени для очередной операции.
    so_5::send_delayed<io_timeout>(*this, ...);
    // Начинаем саму IO-операцию.
    perform_io(...);
  }
  void on_io_result(mhood_t<io_result> cmd) {
    ... // Должным образом обрабатываем результат.
    if(has_more_io_ops())
      so_5::send<start_next_io>(*this, ...);
  }
  void on_timeout(mhood_t<io_timeout>) {
     ... // Обрабатываем тайм-аут текущей операции.
  }
};
Вот в этом случае у вас отложенные сигналы от предыдущих операций будут восприниматься как тайм-ауты для текущей операции. Самый надежный способ избежать этого на данный момент — это включать в отложенное сообщение какой-то ID текущей операции. Например:
class io_performer : public so_5::agent_t {
  struct io_timeout : public so_5::message_t {
    op_id id_;
    io_timeout(op_id id) : id_(std::move(id)) {}
  };
  ...
  void on_next_operation(mhood_t<start_next_io>) {
    // Начинаем отсчет времени для очередной операции.
    current_id_ = create_current_op_id();
    so_5::send_delayed<io_timeout>(*this, ..., current_op_id_);
    // Начинаем саму IO-операцию.
    perform_io(...);
  }
  void on_io_result(mhood_t<io_result> cmd) {
    ... // Должным образом обрабатываем результат.
    current_op_id_ = null_id; // Сбрасываем ID, т.к. текущая операция завершилась.
    if(has_more_io_ops())
      so_5::send<start_next_io>(*this, ...);
  }
  void on_timeout(mhood_t<io_timeout> cmd) {
    if(current_op_id_ == cmd->id_) {
       ... // Обрабатываем тайм-аут текущей операции.
    }
  }
  ...
  op_id current_op_id_;
};
Угу. Поэтому в первой реализации поддержки иерархических конечных автоматов и обработчиков on_enter/on_exit мы пошли по пути жестких ограничений. Тогда и реализация оказывается более простой, и поведение более предсказуемым и понятным.

По мере накопления опыта и рассмотрения сценариев от разных пользователей можно будет подумать о том, как эти ограничения смягчить.
Просто логично было бы предположить, что SO будет действовать согласно so_exception_reaction, а не просто вызовет std::terminate.

Не так все просто, к сожалению. Пользователь может выставить реакцию ignore_exception. Но т.к. смена состояния не была нормально завершена, то агент оказывается в непонятном (и, скорее всего, некорректном) состоянии.

Пользователь может получить полный контроль за действиями при смене состояния, если он делает их сам вне on_enter/on_exit. Но вот внутри on_enter/on_exit возможности сильно ограничиваются.
Сейчас я уже нашел более-менее подходящее решение — вместо перехода в статус st_wait_command вызывать отдельный метод, который решит нужно ли менять состояние или же начать выполнять новую команду. Это работает, но эстетически смущает.

Если я правильно понял ситуацию, то это самый простой и надежный способ. Я бы и сам так делал.

Information

Rating
5,267-th
Location
Гомель, Гомельская обл., Беларусь
Registered
Activity