Эрланг на практике. 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_child4 функции супервизора позволяют добавлять и убирать дочерние потоки.
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.
Остановка супервизора
В АПИ супервизора не предусмотрено функции для его остановки. Он останавливается либо по своей стратегии, либо по сигналу родителя.
При этом он завершает все свои дочерние потоки в очередности, обратной их запуску, затем останавливается сам.