Деревья супервизоров и стратегии перезапуска
Как Erlang организует самовосстановление в иерархию.
Супервизор — процесс OTP, единственная задача которого — запускать дочерние процессы, следить за ними и перезапускать упавших по заданной стратегии.
Супервизоры — то, что превращает отдельные устойчивые процессы в устойчивую систему. Они не содержат бизнес-логики; их работа — надзор и восстановление. В этом и состоит красота идеи: один процесс умеет хорошо делать свою работу и честно падать при сбое, а другой умеет хорошо перезапускать упавших — и из этих двух простых ролей складывается система, которая способна часами и сутками держаться сама, без вмешательства человека. Супервизор — прямое воплощение всего, что мы обсуждали раньше: он использует link и trap_exit, чтобы узнавать о смерти детей, и реализует политику восстановления поверх философии «let it crash». По сути это «менеджер среднего звена» вашей системы: сам ничего не производит, но следит, чтобы производящие не простаивали.
Дочерняя спецификация
Супервизор знает о своих детях из списка спецификаций. Каждая описывает: какой процесс запускать, как часто его перезапускать (permanent, transient, temporary) и как корректно завершать. Ключевое поле start — это тройка «модуль, функция, аргументы», указывающая, как именно поднять ребёнка; обратите внимание, что вызываемая функция должна быть из семейства start_link, ведь супервизору необходима связь с ребёнком, чтобы ловить его падения. Поле id — внутренний идентификатор ребёнка в рамках этого супервизора, по нему его потом можно найти, остановить или перезапустить вручную. В современном OTP спецификации записывают картами (maps), как в примере ниже, и поля с разумными значениями по умолчанию (например, способ завершения shutdown или тип worker) можно опускать — OTP подставит безопасные значения сам.
-module(my_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{strategy => one_for_one,
intensity => 3, period => 5},
Children = [
#{id => counter,
start => {counter, start_link, []},
restart => permanent}
],
{ok, {SupFlags, Children}}.
Стратегии перезапуска
Стратегия определяет, что делать с другими детьми, когда один упал.
| Стратегия | При падении ребёнка |
one_for_one | перезапустить только упавшего |
one_for_all | перезапустить всех детей |
rest_for_one | перезапустить упавшего и всех запущенных после него |
one_for_one подходит независимым детям. one_for_all — когда дети взаимозависимы и должны стартовать вместе. rest_for_one — когда есть цепочка зависимостей по порядку запуска. Разберём rest_for_one на примере: пусть запускаются по порядку соединение с БД, затем кэш, который пользуется этим соединением, затем веб-обработчик, опирающийся на кэш. Если упадёт кэш (второй в цепочке), перезапускать соединение бессмысленно — оно цело, — а вот веб-обработчик нужно поднять заново, ведь он держал ссылку на старый, уже мёртвый кэш. Стратегия rest_for_one сделает ровно это: перезапустит упавшего и всех, кто стартовал после него, не трогая тех, кто был запущен раньше и от упавшего не зависит. Выбор стратегии — это, по сути, явное описание графа зависимостей между детьми, и думать о нём стоит именно так: «кого ещё затронет смерть вот этого процесса?».
Типы перезапуска ребёнка
permanent— перезапускать всегда (для критичных сервисов).transient— перезапускать только при ненормальном завершении.temporary— не перезапускать никогда.
Интенсивность: защита от «петли смерти»
Что если процесс падает сразу после перезапуска, снова и снова? Бесконечный цикл перезапусков бесполезен. Поэтому у супервизора есть лимит: intensity перезапусков за period секунд. Если лимит превышен, супервизор сдаётся и сам падает — передавая проблему своему супервизору выше по дереву. В примере выше заданы intensity => 3 и period => 5: это значит «не более трёх перезапусков за любые пять секунд». Смысл этого механизма глубже, чем просто «не зацикливаться». Он проводит границу между двумя сортами проблем. Если сбой был случайным (тот самый Гейзенбаг), перезапуск его вылечит, и лимит никогда не будет достигнут — система тихо самоисцелится. Но если процесс падает детерминированно, на одних и тех же данных, перезапуск не поможет, лимит быстро исчерпается, и супервизор честно признает: «локально я это починить не могу». Тогда проблема эскалируется выше, где, возможно, перезапустят и сам супервизор вместе с целой подсистемой, восстановив больший кусок состояния. Так превышение интенсивности — не сбой механизма, а его штатный сигнал «нужно вмешательство уровнем выше».
Дерево супервизоров
Супервизоры могут надзирать за другими супервизорами, образуя дерево. Листья — рабочие процессы, узлы — супервизоры. Сбой поднимается по дереву ровно настолько высоко, насколько нужно для восстановления. Эта иерархия — не формальность, а способ локализовать отказ. Чем ниже в дереве удаётся погасить проблему, тем меньший кусок системы при этом перезапускается и тем меньше теряется работы. Поэтому деревья проектируют так, чтобы тесно связанные процессы сидели под общим супервизором поближе к листьям, а независимые подсистемы расходились по разным ветвям — тогда сбой в одной из них не заденет другие. Самый верхний супервизор называют корневым: его запускает приложение (о них в следующем уроке), и именно он держит всё дерево. Падение, дошедшее до самого верха и исчерпавшее лимиты там, означает, что приложение не смогло само себя восстановить — это уже серьёзный сигнал для системы в целом.
[Главный супервизор]
/ \
[Супервизор A] [Супервизор B]
/ \ |
worker worker worker
Как работает под капотом
Супервизор запускает детей с spawn_link и ставит trap_exit, так что смерть ребёнка приходит как сообщение {'EXIT', ...}. Получив его, супервизор применяет стратегию: перезапускает нужных детей в порядке их спецификаций. Счётчик перезапусков скользит по окну period; превышение intensity заставляет супервизор завершиться, передав отказ выше. Так дерево само эскалирует проблему до уровня, способного её решить.
Частые ошибки
- Класть бизнес-логику в супервизор. Его дело — только надзор; работу делают worker'ы.
- Выбрать
one_for_allдля независимых детей. Падение одного зря перезапустит остальных. - Слишком высокий
intensity. Тогда дерево не заметит «петлю смерти» и будет крутить её впустую.
Итоги
- Супервизор запускает детей, следит за ними и перезапускает по стратегии.
- Стратегии:
one_for_one,one_for_all,rest_for_one. intensity/periodзащищают от бесконечной «петли смерти».- Супервизоры образуют дерево, эскалируя сбой вверх до уровня восстановления.