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

Автор оригинала: 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. Предыстория

Комментарии 19

    0
    А что-то simple_one_for_one нет.
      0
      Статьи 2008 года. Может, не была тогда еще эта стратегия реализована?
        0
        Ах, тогда понятно. Вполне возможно.
          0
          меня удивляет качество ваших переводов.

          Мало того, что вы практически не вникаете в то, что переводите, так ещё и придумываете термины, которые никто кроме вас не использует.

          В целом ценности от ваших переводов очень немного.

          В русском сообществе используется калька «супервизор» и в 2008 конечно же simple_one_for_one был. Но авторы статей и переводов упорно рассказывают про обычные супервизоры, которые на практике мало нужны.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              По делу есть чего сказать?
                0
                Вы себя считаете русским сообществом? Мало того, что вы открыто практически хамите, так еще и не по делу. Вот, к примеру, здесь употребляется термин «контролёр». Я не вижу причин, по которым необходимо заимствовать слова, которые и так есть в русском языке. Это говорит лишь о плохом словарном запасе. Вы живете в крутом хаусе в центре тауна (ах, простите, судя по амбициям — не меньше, чем в сити), а ивнингами за биром с пиплами спикаете о гёрлах?

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

                И буду рад увидеть конструктивную критику переводов. По поводу использования или неиспользования конструкция языка в данных статьях (в частности, стратегии simple_one_for_one) — к их автору, пожалуйста, не понимаю, какие претензии могут быть ко мне. Что же касается вдумчивости: давайте-ка по шагам. Пока еще в отсутствии мозгов меня никто не обвинял, особенно публично, так что раз уж начали — будьте добры конкретику.
                  0
                  Я не вижу причин, по которым необходимо заимствовать слова, которые и так есть в русском языке
                  я вижу. заимствовать стоит, если речь идёт о специальной лексике. такой подход имеет ряд плюсов.
                  * Супервизор — он и в африке супервизор. А вот с переводом сложнее. Вы переведёте «контролёр», вася пупкин — «начальник», джон смит — надсмотрщик. И все будут правы. А читателю потом ломай моск, какое слово они все имели в виду — overseer/taskmaster, controller/inspector/supervisor/auditor/checker или head/chief/commander/superior/director/supervisor/superintendent
                  * не нужно следить за контекстом, чтобы было наверняка понятно — о том ли контролёре идёт речь
                    0
                    Чтобы не было недопонимания и было понятно, о чем идет речь, я при первом использовании термина привожу оригинал. Но это что касается именно данного случая. И я готов согласиться с применением другого термина, если эта мысль обоснована выражена не хамски. Когда делал текущие переводы, то, так как не имею большого опыта в языке и не вращался в сообществе, смотрел использование термина на http://erlanger.ru. Плюс логика была очень прозрачной: термин «контролёр» очень точно описывает род занятий процесса. Он «контролирует» выполнение. Но, опять же, я не настаиваю на этом варианте. :)

                    Если же говорить в общем, то я все равно считаю, что стоит использовать русский язык. Иначе тогда вообще все термины можно простым транскрибированием использовать. И получим (с поправкой на специализацию) примерно то, что я привел в ответ невежливому комментатору выше.
                      0
                      «надсмотрщик» описывает его род занятий не менее точно ;)
                      Иначе тогда вообще все термины можно простым транскрибированием использовать
                      термины — можно. лексику общего назначения — нельзя. поэтому нет, не получим
                        0
                        Слово «надсмотрщик» помимо смысловой несет определенную эмоциональную окраску. Но я вашу мысль понял еще в предыдущий раз :).

                        Я бы на эту тему подискутировал, но здесь это будет, боюсь, оффтопиком.
                        0
                        supervisor = контролёр, ОК.
                        Но что вы будете делать, когда попадется слово controller ;)
                        0
                        Кстати, в качестве добавки: если говорить именно о supervisor, то я очень долго колебался, не зная, на чем остановиться. Любовь к русскому языку боролась во мне с «понимаем непонимания» некоторых граждан. Но, увидев двойное использование на erlanger.ru — успокоился. Как оказалось, зря.
                        0
                        Я тоже за термин супервизор ).
                          0
                          Уговорили.
              0
              никогда не заново не запускается
              — думаю тут лишнее «не». Поправьте плиз.
                0
                И да, статья была очень кстати. Спасибо большое за перевод.
                  0
                  Спасибо, поправил!
                  0
                  «Пятая статья уже готова, запланирована к публикации в ближайшие несколько дней»

                  Странно, не могу ее найти.

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

                  Самое читаемое