. Эрланг на практике. Supervisor. — Эрланг на практике
Эрланг на практике. Supervisor. — Эрланг на практике

Эрланг на практике. Supervisor. — Эрланг на практике

На прошлом уроке мы выяснили, что стратегия эрланг -- разделить потоки на рабочие (worker) и системные (supervisor), и поручить системным потокам обрабатывать падения рабочих потоков.

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

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

На картинке нарисовано такое дерево. Узлы в нем -- супервизоры, а листья -- рабочие процессы. Падение любого потока и любой части системы не останется незамеченным.

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

В эрланг есть стандартная реализация супервизора. Он работает аналогично gen_server. Вы должны написать кастомный модуль, реализующий поведение supervisor, куда входит одна функция обратного вызова init/1. С одной стороны это просто -- всего один callback. С другой стороны init должен вернуть довольно сложную структуру данных, с которой нужно как следует разобраться.

Запуск супервизора

Запуск supervisor похож на запуск gen_server. Вот картинка, аналогичная той, что мы видели в 10-м уроке:

Напомню, что два левых квадрата (верхний и нижний), соответствуют нашему модулю. Два правых квадрата соответствуют коду OTP. Два верхних квадрата выполняются в потоке родителя, два нижних квадрата выполняются в потоке потомка.

Начинаем с функции start_link/0:

Здесь мы просим supervisor запустить новый поток.

Первый аргумент, -- это имя, под которым нужно зарегистрировать поток. Есть вариант supervisor:start_link/2 на случай, если мы не хотим регистрировать поток.

Второй аргумент, ?MODULE -- это имя модуля, callback-функции которого будет вызывать supervisor.

Третий аргумент -- это набор параметров, которые нужны при инициализации.

Дальше происходит некая магия в недрах OTP, в результате которой создается дочерний поток, и вызывается callback init/1.

Из init/1 нужно вернуть структуру данных, содержащую всю необходимую информацию для работы супервизора.

Настройка супервизора

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

Спецификация супервизора -- это кортеж из трех значений:

RestartStrategy описывает политику перезапуска дочерних потоков. Есть 4 варианта стратегии:

one_for_one -- при падении одного потока перезапускается только этот поток, остальные продолжают работать.

one_for_all -- при падении одного потока перезапускаются все дочерние потоки.

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

simple_one_for_one -- это особый вариант, будет рассмотрен ниже.

Многие проблемы можно решить рестартом, но не все. Супервизор должен как-то справляться с ситуацией, когда рестарт не помогает. Для этого есть еще две настройки: Intensity -- максимальное количество рестартов, и Period -- за промежуток времени.

Например, если Intensity = 10, а Period = 1000, это значит, что разрешено не более 10 рестартов за 1000 миллисекунд. Если поток падает 11-й раз, то супервизор понимает, что он не может исправить проблему. Тогда супервизор завершается сам, а проблему пытается решить его родитель -- супервизор уровнем выше.

В 18-й версии эрланг вместо кортежа:

Но и кортеж поддерживается для обратной совместимости.

child specifications

Теперь разберем, как описываются дочерние потоки. Каждый из них описывается кортежем из 6-ти элементов:

ChildID -- идентификатор потока. Тут может быть любое значение. Супервизор не использует Pid дочернего потока, потому что Pid будет меняться при рестарте.

Start -- кортеж описывающий, с какой функции стартует новый поток.

Restart -- атом, указывающий необходимость рестарта дочернего потока. Возможны 3 варианта:

  • permanent -- поток нужно рестартовать всегда.
  • transient -- поток нужно рестартовать, если он завершился аварийно. При нормальном завершении рестартовать не нужно.
  • temporary -- поток не нужно рестартовать.

Shutdown -- определяет, сколько времени супервизор дает дочернему потоку на нормальное завершение работы.

Когда супервизор хочет остановить дочерний поток, он шлет сигнал shutdown, и ждет заданное время. Если за это время дочерний поток не завершился, супервизор останавливает его сигналом kill.

Shutdown может быть указан как время в миллисекунах, либо атомами:

  • brutal_kill -- не давать время, завершать принудительно сразу же.
  • infinity -- не ограничивать время, пусть дочерний поток завершается сколько, сколько ему нужно.

Обычно для worker-потоков указывают время в миллисекундах, а для supervisor-потоков указывают infinity.

Type -- тип дочернего потока. Может быть либо worker, либо supervisor.

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

Пример child specitication:

В 18-й версии эрланг используется map:

Пример функции init:

То же самое для 18-й версии эрланг:

С map это все выглядит понятнее и лаконичнее.

Динамическое создание воркеров

Дерево супервизоров не обязательно должно быть статичным. При необходимости его можно менять: добавлять/удалять новые рабочие потоки, и даже новые ветки супервизоров. Есть два способа это сделать: либо вызовами start_child либо использованием simple_one_for_one стратегии.

start_child

4 функции супервизора позволяют добавлять и убирать дочерние потоки.

start_child/2

Функция позволяет добавить новый дочерний поток, не описанный в init. Она принимает 2 аргумента: имя/pid супервизора, и спецификацию дочернего потока.

terminate_child/2

Функция позволяет остановить работающий дочерний поток. Она принимает 2 аргумента: имя/pid супервизора, и Id дочернего потока.

После того, как поток остановлен, его можно либо рестартовать вызовом restart_child/2, либо вообще убрать его спецификацию из списка дочерних потоков вызовом delete_child/2.

simple_one_for_one стратегия

Использование simple_one_for_one стратегии -- это особый случай, когда нам нужно иметь большое количество потоков: десятки и сотни.

При использовании этой стратегии супервизор может иметь потомков только одного типа. И, соответственно, должен указать только одну child specitication.

Дочерние потоки нужно запускать явно, вызовом start_child/2. Причем, тут меняется роль второго аргумента. Это теперь не child specification, а дополнительные аргументы дочернему потоку.

И дочерний поток в своей функции start_link получит аргументы и из child specification, и из start_child.

Остановка супервизора

В АПИ супервизора не предусмотрено функции для его остановки. Он останавливается либо по своей стратегии, либо по сигналу родителя.

При этом он завершает все свои дочерние потоки в очередности, обратной их запуску, затем останавливается сам.

📎📎📎📎📎📎📎📎📎📎