Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Riak и Erlang/OTP

Глава 15 из книги "Архитектура приложений с открытым исходным кодом", том 1.

Оригинал: "Riak and Erlang/OTP", глава из книги "The Architecture of Open Source Applications"
Авторы: Francesco Cesarini, Andy Gross and Justin Sheehy
Перевод: Н.Ромоданов

Creative Commons: Перевод был сделан в соответствие с лицензией Creative Commons. С русским вариантом лицензии можно ознакомиться здесь.

15.5. Супервизоры

Задачей поведения супервизора является отслеживание его потомков и, основываясь на некоторых предварительно заданных правилах, выполнение конкретный действий в случае, когда выполнение потомков завершается. В качестве потомков могут быть как супервизоры, так и рабочие процессы. Это позволило при разработке кода Riak сосредоточить внимание на его корректности, благодаря чему с помощью супервизоров можно во всей системе постоянно следить за ошибками в программном обеспечении, повреждениями данных или системными ошибками. В мире Erlang такой небезопасный подход к программированию часто называют стратегией «пускай выходит из строя». Среди потомков, за которыми наблюдает супервизор, могут быть как супервизоры, так и рабочие процессы. Рабочие процессы являются поведениями OTP, в том числе gen_fsm, gen_server и gen_event. Команда разработчиков Riak, у которой не было возможности обрабатывать пограничные ошибочные ситуации, ограничилась работой с кодом небольшого объема. Размер этого кода из-за того, что в нем используются поведения, гораздо меньшего размера, т. к. это код конкретного приложения. В Riak точно также как и в большинстве приложений Erlang, есть супервизор верхнего уровня, а также есть супервизоры следующих уровней для групп процессов соответствующего назначения. Примерами являются виртуальные узлы Riak, процессы, слушающие сокеты TCP, а также менеджеры запросов-ответов.

15.5.1. Функции обратного вызова супервизора

Чтобы продемонстрировать, как реализуется поведение супервизора, мы воспользуемся модулем riak_core_sup.erl. Супервизор ядра Riak является супервизором верхнего уровня приложения ядра Riak. Он запускает набор статических рабочих процессов и супервизоров, а также ряд динамических рабочих процессов, осуществляющих обработку привязок HTTP и HTTPS для интерфейса узлов RESTful API, определенного в конкретных конфигурационных файлах приложения. Точно также как и для gen_servers, во всех модулях обратного вызова супервизора должна быть директива -behavior(supervisor).. Модули запускаются при помощи функций start или start_link, в которых могут быть необязательные параметры ServerName, CallBackModule и Argument, передаваемые в функцию обратного вызова init/1.

Если взглянуть на несколько первых строк кода в модуле riak_core_sup.erl, то наряду с директивой поведения и макросом, о которых мы расскажем далее, мы обнаружим функцию start_link/3:

-module(riak_core_sup).
-behavior(supervisor).
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

В результате запуска супервизора будет порожден новый процесс и в модуле обратного вызова riak_core_sup.erl будет вызвана функция обратного вызова init/1. ServerName является кортежем в формате {local, Name} или {global, Name}, где Name является зарегистрированным именем супервизора. В нашем примере, как зарегистрированное имя, так и модуль обратного вызова являются атомом riak_core_sup, происходящим от макроса ?MODULE. Мы в качестве аргумента передаем в init/1 пустой список, который трактуется как значение null. Функция init является единственной функцией обратного вызова супервизора. Она должна возвращать кортеж следующего формата:

{ok,  {SupervisorSpecification, ChildSpecificationList}}

где SupervisorSpecification является трехэлементным кортежем {RestartStrategy, AllowedRestarts, MaxSeconds}, содержащим информацию о том, что делать в случае разроушения или перезапуска процесса. RestartStrategy является одним из трех конфигурационных параметров, определяющих какое влияние должно оказываться на потомков при аварийном завершении поведения:

  • one_for_one: влияние на другие процессы в дереве мониторинга не происходит.
  • rest_for_one: процессы, запущенные после завершения процесса, завершаются и перезапускаются.
  • one_for_all: все процессы завершаются и перезапускаются.

AllowedRestarts указывает, сколько раз любой из потомков супервизора может завершиться в течение MaxSeconds секунд прежде, чем будет завершен сам супервизор (и его потомок). Когда происходит завершение, в супервизор посылается сигнал выхода EXIT, который, в соответствие с используемой стратегией перезапуска, обрабатывается определенным образом. Завершение супервизора после того, как будет достигнуто максимально допустимое количество перезагрузок, гарантирует, что не произойдет увеличение количества циклических перезагрузок и не возникнут другие проблемы, которые нельзя решить на этом уровне. Скорее всего, эта проблема, возникшая в процессе, будет локализована в отдельном поддереве, т. е. супервизор, получивший сообщение о распространении проблемы, завершит выполнение дерева, на которое проблема уже распространилась, и перезапустит его заново.

Если взглянуть на последнюю строку функции обратного вызова init/1 в модуле riak_core_sup.erl, то мы увидим, что в этом конкретном супервизоре используется стратегия one-for-one, означающая, что процессы не зависят друг от друга. Супервизор разрешит максимум десять раз выполнить перезапуск прежде, чем он сам будет перезапущен.

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

{Id, {Module, Function, Arguments}, Restart, Shutdown, Type, ModuleList}

Id является уникальным идентификатором для этого конкретного супервизора. {Module, Function, Arguments} является экспортируемой функцией, результаты работы которой будут использованы в поведении при вызове функции start_link и будет возвращен кортеж вида {ok, Pid}. В стратегии определяется, что в зависимости от того, как завершится процесс, должно происходить, а именно:

  • временный процесс типа transient, который никогда не перезапускается;
  • временный процесс типа temporary, который перезапускается только в случае, если он завершился аварийно, и
  • постоянный процесс типа permanent, который всегда перезапускается независимо от того, было ли его завершение нормальным или аварийным.

Shutdown является значением, указываемым в миллисекундах, которое используется в качестве времени, в течение которого поведению разрешается выполнять функцию terminate при ее перезапуске или при остановке системы. Можно также использовать атом бесконечного выполнения infinity, но это делать настоятельно не рекомендуется для поведений, не являющихся поведениями супервизора. Type является либо атомом рабочего процесса worker, если ссылка делается на общие серверы, обработчики событий и конечные автоматы, либо атомом supervisor. Вместе с ModuleList, списком модулей, реализующих поведение, они используются для управления процессами и их приостановкой во время выполнения процедуры обновления программного обеспечения. В списке спецификаций потомков можно указывать только поведения, которые уже существуют или реализованы пользователями и, следовательно, уже включены в дерево мониторинга.

Обладая этими знаниями, мы теперь сможем сформулировать стратегию перезапуска, в которой определены межпроцессные зависимости, пороги отказоустойчивости и ограничения эскалации работы процедур, базирующуюся на общей архитектуре. Мы также теперь должны суметь понять, что происходит в примере init/1 модуля riak_core_sup.erl. Прежде всего, изучим макрос CHILD. Он создает спецификацию потомка для одного потомка, используя для этого имя модуля обратного вызова, например, Id, делая его постоянно используемым и задавая для него время завершения, равное 5 секундам. Потомки могут быть различного типа — рабочие процессы и супервизоры. Давайте взглянем на пример и посмотрим, что из него можно выяснить:

-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).

init([]) ->
    RiakWebs = case lists:flatten(riak_core_web:bindings(http),
                                  riak_core_web:bindings(https)) of
                   [] ->
                       %% check for old settings, in case app.config
                       %% was not updated
                       riak_core_web:old_binding();
                   Binding ->
                       Binding
               end,

    Children =
                 [?CHILD(riak_core_vnode_sup, supervisor),
                  ?CHILD(riak_core_handoff_manager, worker),
                  ?CHILD(riak_core_handoff_listener, worker),
                  ?CHILD(riak_core_ring_events, worker),
                  ?CHILD(riak_core_ring_manager, worker),
                  ?CHILD(riak_core_node_watcher_events, worker),
                  ?CHILD(riak_core_node_watcher, worker),
                  ?CHILD(riak_core_gossip, worker) |
                  RiakWebs
                 ],
    {ok, {{one_for_one, 10, 10}, Children}}.

Большинство потомков Children, запущенных этим супервизором, являются статически заданными рабочими процессами (или в случае vnode_sup, супервизором). Исключением является часть RiakWebs, которая определяется динамически в зависимости от части HTTP конфигурационного файла Riak.

За исключением библиотечных приложений, каждое приложение OTP, в том числе и те, что есть в Riak, будет иметь свое собственное дерево мониторинга. В Riak различные приложения верхнего уровня работают в узле Erlang, например, riak_core - алгоритмы для распределенных систем, riak_kv — семантики хранилищ вида «ключ/значение», webmachine - для HTTP, и многие другие. Мы показали расширенное дерево для riak_core с тем, чтобы продемонстрировать, как происходит многоуровневый мониторинг. Одним из многих преимуществ этой структуры является то, что когда данная подсистема может выйти из строя (из-за ошибки, проблемы со средой окружения или преднамеренного действия), то можно ограничиться завершением работы только для поддерева первого экземпляра.

Супервизор перезапустит необходимые процессы и система в целом затронута не будет. На практике мы видели как это работает в случае использования Riak. Пользователь может обнаружить, что произошло разрушение виртуального узла, что требует только перезапуска супервизора riak_core_vnode_sup. Если такое разрушение находится под контролем, то супервизор riak_core перезапустит нужный супервизор и предотвратит распространение остановок процессов супервизоров более высокого уровня. Такая изоляция отказов и механизм восстановления позволяет разработчикам Riak (и Erlang) достаточно просто создавать устойчивые системы.

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

15.5.2. Приложения

Поведение application, которое мы ввели ранее, используется для упаковки модулей и ресурсов Erlang в виде компонентов, допускающих многократное использование. В OTP есть два вида приложений. Наиболее распространенная форма, называемая нормальными приложениями, запускает дерево мониторинга и все соответствующие статические рабочие процессы. В библиотечных приложениях, таких как стандартная библиотека Standard Library, которые поставляются как часть дистрибутива Erlang, содержатся библиотечные модули, но в них не происходит запуск дерева моноторинга. Это не значит, что в коде не может быть деревьев процессов или деревьев мониторинга. Это просто означает, что такие приложения запускаются как часть дерева мониторинга, принадлежащего другому приложению.

Система Erlang должна состоять из набора слабо связанных приложений. Некоторые из них пишут разработчики, некоторые из них доступны как проекты с открытым исходным кодом, а другие - являются частью дистрибутива Erlang/OTP. Система времени исполнения Erlang и его инструментальные средства работают со всеми приложениями одинаково независимо от того, являются ли те частью дистрибутива Erlang или нет.


Продолжение статьи: Репликация и коммуникация в Riak.