Pull to refresh

Использование контролёров для того, чтобы удержать ErlyBank на плаву

Reading time8 min
Views2.3K
Original author: Mitchell Hashimoto
Это четвертая статья в серии «Введение в ОТП». Если вы только что присоединились к нам, рекомендую начать с первой части, в которой говорится о gen_server и закладывается фундамент нашей банковской системы. Если же вы способный ученик, можете взглянуть на готовые к настоящему моменту модули: eb_server.erl, eb_event_manager.erl, eb_withdrawal_handler.erl и eb_atm.erl.

Сценарий: Момент, который нам нравится в банках и банкоматах, заключается в том, что они всегда на том же месте. Используя банкомат, мы можем снять или положить деньги когда захотим, 24 часа в сутки. Или пойти в любое отделение банка, когда оно открыто, зная, что будем иметь полный доступ к нашим финансам. Чтобы гарантировать это, необходимо быть уверенным в том, что наша система автоматизации ErlyBank всегда остается рабочей: процессы должны быть запущены постоянно. ErlyBank поручил нам реализовать эту цель. Стопроцентный uptime! (Или настолько близкий, насколько мы сможем обеспечить)

Результат: Используя контролёр (supervisor) OTP, мы создадим процесс, чья обязанность — следить за запущенными процессами и удостовериться, что они активны.

Что такое контролёр


Контролёр — это процесс, который отслеживает то, что называется дочерними процессами. Если дочерний процесс «сдулся», контролёр использует стратегию перезапуска (restart strategy) этого потомка для перезапуска. Этот способ может обеспечить вечную работоспособность систем Erlang.

Контролёр — часть того, что называется деревом контроля (supervision tree). Хорошо написанное приложение Erlang/OTP запускается, начиная с корневого контролёра, который следит за дочерними контролёрами, которые, в свою очередь, отслеживают дополнительные контролёры или процессы. Идея в том, что если контролёр вылетает, контролёр-родитель его перезапускает, и так далее вверх до корневого контролёра. В среде исполнения Erlang присутствует отличный режим, в котором вся система отслеживается и рестартует, если умирает корневой контролёр. Таким образом, дерево контроля всегда будет работоспособно.

У контролёра есть только один метод обратной связи: init/1. Его задача — вернуть список дочерних процессов и стратегии перезапуска для каждого процесса, чтобы контролёр знал, за чем следить и что предпринять, если что-то пойдет не так.

Разделяем eb_server и менеджер событий


Одна из вещей, которые я реализовал в предыдущей статье о gen_events, заключалась в явном запуске процесса менеджера событий в методе инициализации модуля eb_server. Тогда это было единственной возможностью, которая у меня была, если я действительно хотел с легкостью запустить сервер с такой зависимостью. Но теперь, раз мы собираемся реализовать запуск и остановку, используя контролёр, у нас есть возможность запустить менеджер событий в дереве контроля. Так что давайте-ка уберем запуск eb_event_manager из кода сервера.

Чтобы сделать это, просто уберите строку 84, которая запускает менеджер событий, из модуля eb_server. Так же я добавил на это место вызов add_handler, чтобы подключить к менеджеру событий обработчик eb_withdrawal_handler (если вы последовательно читаете и реализуете описанное в настоящем цикле переводов, включая дополнение к предыдущей статье, то вам ничего добавлять не нужно, потому что мы уже сделали это ранее; после исключения запуска менеджера событий ваш код метода инициализации должен выглядеть так же, как нижеследующий — прим. переводчика). Теперь метод init модуля eb_server должен выглядеть подобно этому:

init([]) ->
  eb_event_manager:add_handler(eb_withdrawal_handler),
  {ok, dict:new()}.

Кликните сюда, чтобы увидеть eb_server.erl после внесенных изменений.

Каркас контролёра


Основной каркас для написания контролера можно увидеть здесь. Как вы могли заметить, в нем присутствует метод запуска и базовый метод инициализации, который на данный момент возвращает стратегию перезапуска и несуществующие спеки потомка. Стратегии перезапуска (restart stategies) и спецификации потомков (child specifications) раскрываются в следующих разделах этой статьи.

Сохраните каркас как eb_sup.erl. Именование этого файла — еще одно соглашение. Контролёр определенной группы всегда имеет суффикс "_sup." Это не обязательное требование, но является стандартной практикой.

Стратегии перезапуска


Контролёр имеет единую стратегию перезапуска, которую он использует в сочетании со спецификациями потомков, чтобы определить действия в случае, если один из его потомков умрет. Нижеследующий список содержит возможные стратегии перезапуска:

  • one_for_one — Когда один из дочерних процессов умирает, контролёр перезапускает его. Другие потомки не трогаются.
  • one_for_all — Когда один из дочерних процессов умирает, все другие потомки останавливаются, а затем все перезапускаются.
  • rest_for_one — Когда один из дочерних процессов умирает, «остаток» потомков, определенных в списке спецификаций потомков после умершего, завершается, после чего они все стартуют заново.

Стратегия перезапуска указывается в следующем формате:

{RestartStrategy, MaxRetries, MaxTime}

Его очень просто понять, проговорив по-русски: Если потомок перезапускается чаще, чем MaxRetries раз за MaxTime секунд, то контролёр завершает все дочерние процессы и затем прекращает работу сам. Это сделано для того, чтобы предотвратить бесконечную петлю перезапусков потомка.

Синтаксис и основные понятия спецификации потомков


Метод init контролёра отвечает за возврат списка спецификаций потомков. Эти спецификации рассказывают контролёру, какие процессы запускать и как это сделать. Контролёр запускает процессы в порядке «слева направо» (от начала списка к его концу). Стратегия перезапуска — это кортеж со следующим форматом:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

Определения:
Id = term()
 StartFunc = {M,F,A}
  M = F = atom()
  A = [term()]
 Restart = permanent | transient | temporary
 Shutdown = brutal_kill | int()>=0 | infinity
 Type = worker | supervisor
 Modules = [Module] | dynamic
  Module = atom()


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

StartFunc — это кортеж в формате {Module, Function, Args}, который указывает функцию, вызов которой запускает процесс. ОЧЕНЬ ВАЖНО: функция запуска обязана запустить процесс и привязать (link) к нему и должна вернуть {ok, Pid}, {ok, Pid, Other} или {error, Reason}. Обычные методы OTP start_link следуют этому правилу. Но если вы реализуете модуль, который запускает свои собственные процессы, убедитесь, что используете для их запуска spawn_link.

Restart — один из трех атомов (atom), описанных в блоке кода выше. Если в качестве restart используется атом "permanent", то процесс всегда запускается заново. Если значение — "temporary", то процесс никогда заново не запускается. И если это значение равно "transient", то процесс запускается заново только в случае непредвиденного завершения.

Shutdown объясняет контролёру, как завершать дочерние процессы. Атом "brutal_kull" завершает потомка без вызова его метода завершения. Любое целое число выше нуля подразумевает таймаут для корректного завершения. Атом "infinity" вежливо завершит процесс и будет ждать его остановки вечно.

Type говорит контролеру, что из себя представляет потомок: другой контролёр или любой прочий процесс. Если это контролёр, используйте атом «supervisor», иначе воспользуйтесь атомом «worker».

Modules — это либо список модулей, на которые этот процесс влияет, либо атом «dynamic». В 95% случаев в списке для этого значения вы будете использовать единственный модуль обратной связи OTP. «Dynamic» используется в случае, если процесс — gen_event, так как его влияние на модули динамическое (различные обработчики, которые не могут быть определены сразу). Этот список используется только для управления релизами и не важен в контексте данной статьи, но будет использоваться в одной из будущих статей, посвященной управлению релизами.

Ух! Так много информации для усвоения за столь короткое время. У меня ушло довольно много времени на то, чтобы запомнить формат спецификаций потомков и различные стратегии перезапуска, так что не переживайте, если не можете сделать этого сразу. Вы всегда можете освежить информации в руководстве по контролёрам.

Спека потомка менеджера событий


Первое, что требуется запустить — это менеджер событий, потому что сервер от него зависит. Спецификация потомка выглядит как-то так:

EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
            permanent,2000,worker,dynamic}.

После чтения раздела о синтаксисе спецификации потомков этот кусок кода должен быть достаточно простым. Возможно, вам понадобится вернуться назад и свериться с описанием, чтобы понять действие каждого параметра, и это абсолютно нормально! Лучше задержаться и разобраться в коде, чем покивать головой и забыть все через пару минут. Я полагаю, что одной «странной» штукой в описании спецификации будет указание списка модулей как «dynamic» (динамический; в данном случае это атом — прим. переводчика). Это сделано потому, что речь идет о gen_event, и список модулей, которые в нем используются, динамический, т.к. к нему подключаются обработчики (число которых может меняться в процессе работы — прим. переводчика). В прочих случаях вы должны перечислить все модули, которые использует процесс.

Вот метод инициализации после включения в него спеки потомков:

init([]) ->
  EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
              permanent,2000,worker,dynamic},
  {ok,{{one_for_one,5,10}, [EventManager]}}.

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

Если вы сейчас скомпилируете и запустите контролёр (я думаю, стоит это сделать!), то после запуска метода start_link, принадлежащего контролёру, введите whereis(eb_event_manager), и команда должна вернуть идентификатор (pid) процесса менеджера событий. Затем, если вы убьете контролёр, выполнив exit(whereis(eb_sup), kill), и потом попытаетесь снова получить идентификатор eb_event_manager, то в ответ должны получить сообщение о том, что он не определен, так как процесс был убит.

Так же, для прикола, убейте eb_event_manager во время работы под управлением контролёра. Подождите несколько секунд и проверьте процесс. Он должен восстановиться!

Сервер и банкомат


Со справкой по спецификации потомков и примером, приведенным выше, вы должны знать достаточно для того, чтобы запустить сервер и банком. Так что если вы чувствуете эту задачу заманчивой, сделайте это сейчас. Если же нет, то я привел спецификации и для того, и для другого ниже:

Server = {eb_server, {eb_server, start_link, []},
              permanent,2000,worker,[eb_server]},
  ATM = {eb_atm, {eb_atm, start_link, []},
         permanent,2000,worker,[eb_atm]},


После создания этих спецификаций добавьте их в список, возвращаемый инициализационным методом. Убедитесь, что поставили их после менеджера событий.

Вы можете увидеть завершенный eb_sup.erl, нажав здесь.

Добавление и удаление потомков во время выполнения


К несчастью, я не смог придумать остроумный сценарий, позволяющий вставить этот механизм в ErlyBank, но чувствовал, как важно отметить возможность динамически добавлять и удалять спецификации потомков в уже запущенный процесс контролёра, используя методы start_child и delete_child.

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

Заключение


В этой статье о контроллерах я ввёл такие понятия, как дерево контроля, стратегии перезапуска, спецификации потомков и динамическое добавление и удаление потомков.

На этом заканчивается четвертая статья из цикла «Введение в Erlang/OTP». Пятая статья уже готова, запланирована к публикации в ближайшие несколько дней и представит приложения (applications).



Статьи из серии

4. Использование контролёров для того, чтобы удержать ErlyBank на плаву (текущая статья)
3. Введение в gen_event: Уведомления об изменениях счета

Автор перевода — tiesto:
2. Введение в gen_fsm: Банкомат ErlyBank
1. Введение в gen_server: «ErlyBank»
0. Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
-1. Предыстория
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+21
Comments19

Articles

Change theme settings