Как вы проектируете архитектуру распределенной отказоустойчивой многоядерной системы на основе Erlang / OTP? - PullRequest
40 голосов
/ 05 сентября 2011

Я хотел бы создать систему на основе Erlang / OTP, которая решает проблему «смущающей параллельности».

Я уже прочитал / пролистал:

  • Learn You SomeErlang;
  • Программирование Erlang (Армстронг);
  • Программирование Erlang (Cesarini);
  • Erlang / OTP в действии.

Я получилСуть процессов, обмена сообщениями, супервизоров, gen_servers, ведения журналов и т. д.

Я понимаю, что определенные варианты архитектуры зависят от конкретного приложения, но все же я хотел бы знать некоторые общие принципы проектирования системы ERlang / OTP..

Должен ли я просто начать с нескольких gen_servers с супервизором и постепенно наращивать их?

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

Должен ли я добавить ведение журнала позже?

Каков общий подход к архитектуре распределенных отказоустойчивых многопроцессорных систем Erlang / OTP?

1 Ответ

104 голосов
/ 05 сентября 2011

Должен ли я просто начать с нескольких gen_servers с супервизором и постепенно наращивать его?

Вам не хватает одного ключевого компонента в архитектурах Erlang: приложения!(То есть концепция приложений OTP, а не программных приложений).

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

Первый шаг при разработке системы Erlang состоит в том, чтобы решить, какие приложения необходимы.Некоторые могут быть извлечены из Интернета, как они есть, это мы можем назвать библиотеками.Другие вам нужно написать самостоятельно (иначе вам не понадобится эта конкретная система).Эти приложения мы обычно называем бизнес-логикой (часто вам также нужно написать некоторые библиотеки самостоятельно, но полезно сохранять различие между библиотеками и основными бизнес-приложениями, которые все связывают).

Сколько у меня должно быть супервизоров?

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

Куча одинаковых временных работников?Один руководитель, чтобы управлять ими всеми.

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

Иногда это нормально, чтобы поставить кучу разных процессовтипы под одним и тем же руководителем.Обычно это имеет место, когда у вас есть несколько одноэтапных процессов (например, один супервизор HTTP-сервера, один процесс-владелец таблицы ETS, один сборщик статистики), которые всегда будут работать.В этом случае может быть слишком сложно для каждого супервизора иметь одного супервизора, поэтому обычно добавляется под одним супервизором.Просто помните о последствиях использования конкретной стратегии перезапуска при этом, чтобы, например, не прерывать статистический процесс в случае сбоя веб-сервера (one_for_one - наиболее распространенная стратегия, используемая в подобных случаях.).Будьте осторожны, чтобы не иметь каких-либо зависимостей между процессами в one_for_one супервизоре.Если процесс зависит от другого сбойного процесса, он может также аварийно завершиться, вызывая интенсивность перезапуска супервизоров слишком часто и сбой самого супервизора слишком рано.Этого можно избежать, имея двух разных супервизоров, которые полностью контролируют перезапуски с помощью настроенной интенсивности и периода ( более подробное объяснение ).

Как мне решить, какие части системы должны бытьна основе процесса?

Каждое параллельное действие в вашей системе должно осуществляться в своем собственном процессе.Неправильная абстракция параллелизма является самой распространенной ошибкой разработчиков системы Erlang в начале.

Некоторые люди не привыкли иметь дело с параллелизмом;их системы имеют тенденцию иметь слишком мало этого.Один или несколько гигантских процессов, которые запускают все по порядку.Эти системы обычно полны запаха кода, а код очень жесткий и трудно реорганизуется.Это также делает их медленнее, потому что они могут не использовать все ядра, доступные Эрлангу.

Другие люди сразу же понимают концепции параллелизма, но не могут применять их оптимально;их системы имеют тенденцию злоупотреблять концепцией процесса, заставляя многие процессы бездействовать в ожидании других, выполняющих работу.Эти системы, как правило, излишне сложны и трудны для отладки.

По сути, в обоих вариантах вы сталкиваетесь с одной и той же проблемой, вы не используете весь доступный вам параллелизм и не получаете максимальной производительностивне системы.

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

Существуют веские причины для простоя процессов. Иногда они сохраняют важное состояние, иногда вы хотите временно сохранить некоторые данные, а затем отменить процесс, иногда они ждут внешних событий. Больше ловушки - пропускать важные сообщения через длинную цепочку в основном неактивных процессов, так как это замедлит работу вашей системы с большим количеством копирования и увеличит использование памяти.

Как мне избежать узких мест?

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

Золотое правило здесь состоит в том, чтобы измерить, измерить, измерить ! Не думайте, что вам есть что улучшить, пока вы не измерили.

Erlang великолепен тем, что позволяет скрыть параллелизм за интерфейсами (известный как неявный параллелизм). Например, вы используете API функционального модуля, обычный интерфейс module:function(Arguments), который, в свою очередь, может порождать тысячи процессов без необходимости знать об этом вызывающую сторону. Если вы правильно поняли абстракции и API, вы всегда можете распараллелить или оптимизировать библиотеку после того, как начнете ее использовать.

Как говорится, вот несколько общих указаний:

  • Старайтесь отправлять сообщения получателю напрямую, избегайте направления или маршрутизации сообщений через посреднические процессы. В противном случае система просто тратит время на перемещение сообщений (данных) без реальной работы.
  • Не злоупотребляйте шаблонами разработки OTP, такими как gen_servers. Во многих случаях вам нужно только запустить процесс, запустить какой-то фрагмент кода и затем выйти. Для этого gen_server является излишним.

И еще один совет: не используйте повторно процессы. Создание процесса в Erlang настолько дешево и быстро, что нет смысла повторно использовать процесс, когда его срок службы истек. В некоторых случаях может иметь смысл повторно использовать состояние (например, сложный анализ файла), но это лучше канонически хранить в другом месте (в таблице ETS, базе данных и т. Д.).

Должен ли я добавить ведение журнала позже?

Вы должны добавить запись сейчас! Существует отличный встроенный API под названием Logger , который поставляется с Erlang / OTP с версии 21:

logger:error("The file does not exist: ~ts",[Filename]),
logger:notice("Something strange happened!"),
logger:debug(#{got => connection_request, id => Id, state => State},
             #{report_cb => fun(R) -> {"~p",[R]} end}),

Этот новый API имеет несколько расширенных функций и должен охватывать большинство случаев, когда вам необходимо войти в систему. Также есть старая, но все еще широко используемая сторонняя библиотека Lager .

Каков общий подход к архитектуре распределенных отказоустойчивых многопроцессорных систем Erlang / OTP?

Подводя итог сказанному выше:

  • Разделите вашу систему на приложения
  • Поместите ваши процессы в правильную иерархию контроля, в зависимости от их потребностей и зависимостей
  • У вас есть процесс для каждого по-настоящему параллельного действия в вашей системе
  • Поддерживать функциональный API по отношению к другим компонентам в системе. Это позволяет вам:
    • Рефакторинг вашего кода без изменения кода, который его использует
    • Оптимизировать код впоследствии
    • Распределите вашу систему, когда это необходимо (просто позвоните другому узлу за API! Вызывающий абонент не заметит!)
    • Тестировать код проще (меньше работы по настройке тестовых систем, легче понять, как его использовать)
  • Начните использовать библиотеки, доступные вам в OTP, пока вам не понадобится что-то другое (вы узнаете, когда придет время)

Общие подводные камни:

  • Слишком много процессов
  • Слишком мало процессов
  • Слишком много маршрутизации (перенаправленные сообщения, связанные процессы)
  • Слишком мало приложений (на самом деле я никогда не видел противоположного случая)
  • Недостаточно абстракции (затрудняет рефакторинг и рассуждения. Это также затрудняет тестирование!)
...