- Немного теории
- Запуск супервизора
- Настройка супервизора
- Динамическое создание воркеров
- Остановка супервизора
Немного теории
На прошлом уроке мы выяснили, что стратегия эрланг -- разделить потоки на рабочие (worker) и системные (supervisor), и поручить системным потокам обрабатывать падения рабочих потоков.
Существуют научные работы, которые доказывают, что значительная часть ошибок в серверных системах вызваны временными условиями, и перегрузка части системы в известное стабильное состояние позволяет с ними справиться. Среди таких работ докторская диссертация Джо Армстронга, одного из создателей эрланг.
Систему на эрланг рекомендуется строить так, чтобы любой поток был под наблюдением супервизора, а сами супервизоры были организованы в дерево.
На картинке нарисовано такое дерево. Узлы в нем -- супервизоры, а листья -- рабочие процессы. Падение любого потока и любой части системы не останется незамеченным.
Дерево супервизоров разворачивается на старте системы. Каждый супервизор отвечает за то, чтобы запустить своих потомков, наблюдать за их состоянием, рестартовать и корректно завершать, если надо.
В эрланг есть стандартная реализация супервизора. Он работает аналогично gen_server. Вы должны написать кастомный модуль, реализующий поведение supervisor, куда входит одна функция обратного вызова init/1. С одной стороны это просто -- всего один callback. С другой стороны init должен вернуть довольно сложную структуру данных, с которой нужно как следует разобраться.
Запуск супервизора
Запуск supervisor похож на запуск gen_server. Вот картинка, аналогичная той, что мы видели в 10-м уроке:
Напомню, что два левых квадрата (верхний и нижний), соответствуют нашему модулю. Два правых квадрата соответствуют коду OTP. Два верхних квадрата выполняются в потоке родителя, два нижних квадрата выполняются в потоке потомка.
Начинаем с функции start_link/0:
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
Здесь мы просим supervisor запустить новый поток.
Первый аргумент, {local, ?MODULE} -- это имя, под которым нужно зарегистрировать поток. Есть вариант supervisor:start_link/2 на случай, если мы не хотим регистрировать поток.
Второй аргумент, ?MODULE -- это имя модуля, callback-функции которого будет вызывать supervisor.
Третий аргумент -- это набор параметров, которые нужны при инициализации.
Дальше происходит некая магия в недрах OTP, в результате которой создается дочерний поток, и вызывается callback init/1.
Из init/1 нужно вернуть структуру данных, содержащую всю необходимую информацию для работы супервизора.
Настройка супервизора
Разберем подробнее:
{ok, {SupervisorSpecification, ChildSpecifications}}
Нам нужно описать спецификацию самого супервизора, и дочерних процессов, за которыми он будет наблюдать.
Спецификация супервизора -- это кортеж из трех значений:
{RestartStrategy, Intensity, Period}
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-й версии эрланг вместо кортежа:
{RestartStrategy, Intensity, Period}
используется map:
#{strategy => one_for_one,
intensity => 10,
period => 1000
Но и кортеж поддерживается для обратной совместимости.
child specifications
Теперь разберем, как описываются дочерние потоки. Каждый из них описывается кортежем из 6-ти элементов:
{ChildID, Start, Restart, Shutdown, Type, Modules}.
ChildID -- идентификатор потока. Тут может быть любое значение. Супервизор не использует Pid дочернего потока, потому что Pid будет меняться при рестарте.
Start -- кортеж {Module, Function, Args} описывающий, с какой функции стартует новый поток.
Restart -- атом, указывающий необходимость рестарта дочернего потока. Возможны 3 варианта:
- permanent -- поток нужно рестартовать всегда.
- transient -- поток нужно рестартовать, если он завершился аварийно. При нормальном завершении рестартовать не нужно.
- temporary -- поток не нужно рестартовать.
Shutdown -- определяет, сколько времени супервизор дает дочернему потоку на нормальное завершение работы.
Когда супервизор хочет остановить дочерний поток, он шлет сигнал shutdown, и ждет заданное время. Если за это время дочерний поток не завершился, супервизор останавливает его сигналом kill.
Shutdown может быть указан как время в миллисекунах, либо атомами:
- brutal_kill -- не давать время, завершать принудительно сразу же.
- infinity -- не ограничивать время, пусть дочерний поток завершается сколько, сколько ему нужно.
Обычно для worker-потоков указывают время в миллисекундах, а для supervisor-потоков указывают infinity.
Type -- тип дочернего потока. Может быть либо worker, либо supervisor.
Modules -- модули, в которых выполняется дочерний поток. Обычно это один модуль, и он совпадает с указанным в кортеже Start.
Пример child specitication:
{some_worker,
{some_worker, start_link, []},
permanent,
2000,
worker,
[some_worker]},
В 18-й версии эрланг используется map:
#{id => some_worker,
start => {some_worker, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [some_worker]
}
Пример функции init:
init(_Args) ->
RestartStrategy = one_for_one, % one_for_one | one_for_all | rest_for_one
Intensity = 10, %% max restarts
Period = 60, %% in period of time
SupervisorSpecification = {RestartStrategy, Intensity, Period},
Restart = permanent, % permanent | transient | temporary
Shutdown = 2000, % milliseconds | brutal_kill | infinity
ChildSpecifications =
[
{some_worker,
{some_worker, start_link, []},
Restart,
Shutdown,
worker,
[some_worker]},
{other_worker,
{other_worker, start_link, []},
Restart,
Shutdown,
worker,
[other_worker]}
],
{ok, {SupervisorSpecification, ChildSpecifications}}.
То же самое для 18-й версии эрланг:
init(_Args) ->
SupervisorSpecification = #{
strategy => one_for_one,
intensity => 10,
period => 60},
ChildSpecifications =
[#{id => some_worker,
start => {some_worker, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [some_worker]},
#{id => other_worker,
start => {other_worker, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [other_worker]}
],
{ok, {SupervisorSpecification, ChildSpecifications}}.
С map это все выглядит понятнее и лаконичнее.
Динамическое создание воркеров
Дерево супервизоров не обязательно должно быть статичным. При необходимости его можно менять: добавлять/удалять новые рабочие потоки, и даже новые ветки супервизоров. Есть два способа это сделать: либо вызовами start_child либо использованием simple_one_for_one стратегии.
start_child
4 функции супервизора позволяют добавлять и убирать дочерние потоки.
start_child/2
Функция позволяет добавить новый дочерний поток, не описанный в init. Она принимает 2 аргумента: имя/pid супервизора, и спецификацию дочернего потока.
supervisor:start_child(
MySupervisor,
{some_worker,
{some_worker, start_link, []},
Restart,
Shutdown,
worker,
[some_worker]})
terminate_child/2
Функция позволяет остановить работающий дочерний поток. Она принимает 2 аргумента: имя/pid супервизора, и Id дочернего потока.
supervisor:terminate_child(MySupervisor, some_worker)
После того, как поток остановлен, его можно либо рестартовать вызовом restart_child/2, либо вообще убрать его спецификацию из списка дочерних потоков вызовом delete_child/2.
simple_one_for_one стратегия
Использование simple_one_for_one стратегии -- это особый случай, когда нам нужно иметь большое количество потоков: десятки и сотни.
При использовании этой стратегии супервизор может иметь потомков только одного типа. И, соответственно, должен указать только одну child specitication.
init(_Args) ->
SupervisorSpecification = {simple_one_for_one, 10, 60},
ChildSpecifications =
[
{some_worker,
{some_worker, start_link, [A, B, C]},
transient,
2000,
worker,
[some_worker]}
],
{ok, {SupervisorSpecification, ChildSpecifications}}.
Дочерние потоки нужно запускать явно, вызовом start_child/2. Причем, тут меняется роль второго аргумента. Это теперь не child specification, а дополнительные аргументы дочернему потоку.
supervisor:start_child(MySupervisor, [D, E, F]).
И дочерний поток в своей функции start_link получит аргументы и из child specification, и из start_child.
-module(some_worker).
start_link(A, B, C, D, E, F) ->
...
Остановка супервизора
В АПИ супервизора не предусмотрено функции для его остановки. Он останавливается либо по своей стратегии, либо по сигналу родителя.
При этом он завершает все свои дочерние потоки в очередности, обратной их запуску, затем останавливается сам.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»