Процеси и OTP : Supervisor
Продължаваме с друго поведение идващо от OTP
- supervisor
и неговата Elixir
-ска
обвивка - Supervisor
.
Какво е Supervisor?
Това е процес, чиято единствена роля е да наглежда други процеси и да се грижи за тях
по някакъв начин в случай на проблем. С помощта на Supervisor
-ите по лесен начин
можем да изградим fault tolerant
система.
Казваме лесен, защото те ни предлагат изградена система от функции точно за това
и идеологията около тях е лесна за възприемане. Това, което е трудно е да направим
добър дизайн на такава система - къде какви Supervisor
-и ще ни трябват.
Често сме споменавали за девиза “Let it crash!”. Тази мантра се базира върху следното:
Важно е програмата да върви, ако части от нея се сринат, не е проблем - нещо наблюдава тези части,
нещо ще се погрижи те да бъдат възстановени. Това нещо е Supervisor
.
За да имплементираме Supervisor
, просто трябва да направим следното:
defmodule SomeSupervisor do
use Supervisor
end
Подобно на GenServer
, Supervisor
е поведение, за което callback
функциите
имат имплементации по подразбиране.
В модула Supervisor
има код, който може:
- Да инициализира и стартира
Supervisor
процес. - Да осигури това, че
Supervisor
процесът прихващаEXIT
сигнали. - Да стартира определен списък от процеси-деца, зададени на
Supervisor
-а и да гиlink
-не към него.
Поведението Supervisor
дава възможност:
- Ако някой от процесите-деца ‘умре’ непредвидено,
Supervisor
-ът ще получи сигнал и ще предприеме конкретно действие, зададено при създаването му. - Ако
Supervisor
-ът бъде терминиран, всичките му процеси-деца биват ‘убити’.
Отново ще разгледаме пример от Blogit
и след това ще разгледаме всичко, което Supervisor
предлага.
Пример : Supervisor и Blogit - Component Supervisor
Нека си припомним за GenServer
процеса Blogit.Component.Posts
от статията за GenServer.
Той държеше състояние - всичките публикации и се грижеше да ги връща под някаква форма на клиентите си.
Този процес-компонент на Blogit
не е единствения, в момента на писане на тази статия те са три:
- Компонент за публикации
- Компонент за публикации, удобен за заявки по година и месец
- Компонент за конфигурация на блога.
Тези три GenServer
процеса, които наричаме компоненти не зависят един от друг.
Ако един от тях ‘умре’ поради някаква причина, другите могат да продължат да сервират заявки.
При тази ситуация обаче ще загубим дадена функционалност на блога, да речем, ако конфигурацията падне ще спрем да виждаме заглавието и стиловете на блога ще се променят.
Именно затова има процес Suervisor
, който се грижи да рестартира компонент, който ‘умре’:
defmodule Blogit.Components.Supervisor do
use Supervisor
alias Blogit.Components.Posts
alias Blogit.Components.PostsByDate
alias Blogit.Components.Configuration
def start_link() do
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_) do
children = [
worker(Posts, []),
worker(Configuration, []),
worker(PostsByDate, []),
]
opts = [strategy: :one_for_one]
supervise(children, opts)
end
end
Всъщност този модул дефинира само две функции - една за инициализация и една
за стартирането на Supervisor
процеса.
Подобно на GenServer
, Supervisor
може да се стартира със start_link/3
, който
приема същите аргументи - модул имплементиращ поведението, аргументи за init/1
и опции.
За разлика от GenServer
, Supervisor
поведението идва с една единствена callback
функция - init/1
.
Тя връща много специфичен резултат, за който ще поговорим по-късно. Този резултат лесно се генерира с функцията Supervisor.Spec.supervise/2
.
Редът use Supervisor
ни предоставя всички функции от Supervisor.Spec
.
Тази функция, която генерира резултата от init/1
, Supervisor.Spec.supervise/2
приема два аргумента - списък от спецификации и keyword list
от опции.
В нашия пример тези спецификации, често наричани деца, защото точно те описват процесите-деца на Supervisor
-а са три. За всеки от компонентите има спецификация създадена чрез Supervisor.Spec.worker/3
.
Опциите пък включват единствено стратегия със стойност :one_for_one
.
Какво означава всичко това?
Всъщност, когато Supervisor
процес стартира, ще се изпълни init/1
функцията, която можем да дефинираме.
Тя ще върне резултат който съдържа спецификации описващи децата-процеси.
Тези спецификации се използват да се стартират тези деца-процеси и да се свържат със Supervisor
-a.
Първият параметър на worker/3
е модулът, който обикновено е GenServer
имплементация, а вторият са аргументите, които ще му се подадат при стартиране.
Това означава че Blogit.Component.Supervisor
-ът има грижата да стартира трите компонента и да направи връзки към тях.
Опциите се използват за да се зададе поведение на Supervisor
-а, когато някой от процесите-деца умре. Стратегията, която задаваме в примера означава, че ако даден процес умре, той ще бъде рестартиран,
без да се прави нищо с другите процеси-деца.
Следователно Blogit.Component.Supervisor
стартира трите си GenServer
компонента и ако някой от тях ‘умре’ го рестартира.
Тъй като всеки такъв компонент знае как да си вземе състоянието, това рестартиране не е проблем и ще възстанови системата.
Ето как Blogit.Components.Posts
си вземаше състоянието:
def init(_) do
send(self(), :init_posts)
{:ok, nil}
end
def handle_info(:init_posts, nil) do
posts = GenServer.call(Blogit.Server, :get_posts)
{:noreply, posts}
end
Всъщност тук се прави call
към друг GenServer
процес, наречен Blogit.Server
, който се наблюдава от друг Supervisor
, който пък наблюдава Blogit.Components.Supervisor
.
Да - Supervisor
може да наблюдава Supervisor
. Ще научим повече за това малко по-късно. Нека сега да разгледаме функциите и опциите, които идват със Supervisor.Spec
.
Опциите на Supervisor.Spec.supervise/2
strategy
Стратегията няма стойност по подразбиране и е задължителна опция. Има четири възможни стойности:
:one_for_one
: Ако наблюдаван процес ‘умре’, той се рестартира. Другите не се влияят. Тази стратегия е добра за процеси които нямат връзки и комуникация помежду си, които могат да загубят състоянието си без това да повлияе на другите процеси-деца наSupervisor
-а им.:one_for_all
: Ако наблюдаван процес ‘умре’, всички наблюдавани процеси се ‘убиват’ и след това всички се стартират. Обикновено тази стратегия се използва за процеси, чиито състояния зависят доста едно от друго и ако само един от тях бъде рестартиран ще се наруши общата логика на програмата.:rest_for_one
: Ако наблюдаван процес ‘умре’, всички наблюдавани процеси стартирани СЛЕД недго също ‘умират’. Всички тези процеси, включително процесът-причина се рестартират по първоначалния си стартов ред. Тази стратегия е подходяща за ситуация като : процес ‘A’ няма зависимости, но процес ‘Б’ зависи от ‘А’, а има и процес ‘В’, който зависи както от ‘Б’, така и транзитивно от ‘А’.:simple_one_for_one
: При тази стратегия даденSupervisor
има право само на един тип процеси-деца. Обикновено стартира без процеси-деца, но знае как да си ги произведе. Процесите-деца се рестартират както при:one_for_one
, но при много децаSupervisor
-и с тази стратегия са по-бързи - не знаят реда на стартиране на процесите-си-деца. Подходяща е за ситуации е които искаме да управлявамеpool
-ове от процеси да речем.
Всъщност при simple_one_for_one
стратегията различни функции на Supervisor
работят малко по-различно. Повече за тази стратегия, прочетете в документацията.
max_restarts
С опцията :max_restarts
задаваме колко пъти може един процес да бъде рестартиран в даден период от време. По подразбиране е 3
.
Да речем ако процес ‘умира’ защото е станала някаква грешка в remote service
, от който той зависи, колкото и пъти Supervisor
-ът да го рестартира, той ще се чупи и след даден брой рестартирания, Supervisor
-ът е добре да се откаже.
Това е този брой. Но тази опция е тясно свързана с интервал от време. Да речем ако за 5
секунди пробваме да рестартираме процеса 3 пъти, трябва да се откажем.
max_seconds
Точно тази опция е интервала от време, който споменахме в предишната секция. По подразбиране е 5
.
Какво е Supervisor.Spec.spec
Един Supervisor
знае как да рестартира всеки от процесите си. Това е така защото за всеки от тях (или за всички ако е simple_one_for_one
) държи спецификация.
Една такава спецификация съдържа модула дефиниращ процеса и параметрите с които трябва да се стартира, както и няколко специални настройки.
Функцията Supervisor.Spec.supervise/2
взима като първи аргумент списък с елементи от типа Supervisor.Spec.spec
.
Този тип представлява точно тази спецификация на процес-дете.
Както видяхме функцията Supervisor.Spec.worker/3
помага за създаването спецификации.
Нека разгледаме спецификацията на този тип:
@type spec :: {
child_id :: term,
start_fun :: {module, atom, [term]},
restart :: :permanent | :transient | :temporary,
shutdown :: :brutal_kill | :infinity | non_neg_integer,
worker :: :worker | :supervisor,
modules :: :dynamic | [module]
}
Функцията Supervisor.Spec.worker/3
взима модул за свой първи аргумент.
Този модул обикновено имплементира GenServer
. Вторият ѝ аргумент е списък от параметри за стартиране/рестартиране.
Третият е опции.
Тези опции са:
[restart: restart, shutdown: shutdown, id: term, function: atom, modules: modules]
Както виждате, точно те се използват за изграждането на спецификацията на процеса-дете. Нека сега се върнем към нея и да разгледаме всяко от полетата ѝ.
child_id
Това е стойност която се ползва от Supervisor
-ите вътрешно.
Рядко ще я използваме за нещо, макар че можем да я подадем като опция на worker/3
с [id: <id>]
.
Може да се ползва за debugging
да речем.
start_fun
Тази стойност е tupple
съдържащ MFA
.
Използва се за стартиране на процеса-дете.
Модулът, който съдържа логиката на процеса се подава като първи аргумент на worker/3
.
Функцията за стартиране на процеса се подава от опциите на worker/3
чрез [function: <atom-representing-public-function-from-the-module>]
.
По подразбиране е :start_link
.
Задължително тази функция трябва да стартира процес и да го свързва със процеса, който я е извикал.
Аргументите ще се подадат на зададената като атом функция при старт.
Тези аргументи се подават във формата на списък на worker/3
.
restart
Атом, който указва кога и дали ‘терминиран’ процес-дете ще се рестартира. Възможните стойности са:
:permanent
- Процесът винаги се рестартира отSupervisor
-а. Това е и стойността по подразбиране наrestart
. Може да се зададе друга от опциите наworker/3
с[restart: :permanent | :transient | :temporary]
. Този начин на рестартиране е подходящ за дълго-живеещи процеси, които не трябва да ‘умират’.:transient
- С тази опция, даденият процес-дете няма да бъде рестартиран ако излезе нормално - сexit(:normal)
или просто завърши изпълнение. Ако обаче излезе с друга причина (exit(:reason)
), ще бъде рестартиран. Тази опция е подходяща за процеси, които трябва да свършат някаква задача и ако го направят без проблеми няма да има нужда повече от тях. Ако има проблем, обаче, ще пробват отново и отново.:temporary
- Процесът-дете няма да бъде рестартиран ако ‘умре’. Няма значение дали е излязъл с грешка или не. Подходяща е за кратко-живеещи процеси за които е очаквано, че могат да ‘умрат’ с грешка и няма много код зависещ от тях.
Начинът на рестартиране може да е различен за всеки процес-дете. Той и зададената стратегия на Supervisor
-а си влияят.
Да речем процес с temporary restart
няма да бъде рестартиран даже, когато стратегията е one_for_all
или rest_for_one
.
Също така, ако temporary
процес ‘умре’, той няма да задейства one_for_all
стратегия, но пък щя бъде рестартиран ако permanent
процес, под същия Supervisor
‘умре’ първи.
shutdown
Когато Supervisor
трябва да убие някои или всички свои процеси-деца, той извиква Process.exit(child_pid, :shutdown)
за всеки от тях.
Стойността зададена като shutdown
се използва за timeout
след като това се случи. По подразбиране е 5000
или пет секунди.
Когато процес получи :shutdown
, ако е Genserver
, ще му се извика terminate
функцията. Изчистването на някакви
ресурси може да отнеме време. Ако това време е повече от зададеното в shutdown
, Supervisor
-ът ще изпрати нов EXIT
сигнал с Process.exit(child_pid, :kill)
.
Всъщност ако зададем стойност :brutal_kill
за shutdown
, Supervisor
-ът винаги ще терминира даденият процес направо с Process.exit(child_pid, :kill)
.
Можем да зададем и :infinity
за да оставим процеса да си излезе спокойно.
Когато Supervisor
трябва да завърши изпълнението си, той изпълнява логиката за shutdown
за всяко от децата си.
Ако стратегията му е simple_one_for_one
, обаче, не го прави, а ги оставя сами да си излязат.
worker
Това свойство определя дали процесът дете е worker
процес или Supervisor
.
Всъщност има функция Supervisor.Spec.supervisor/3
, която генерира спецификация по същият начин като Supervisor.Spec.worker/3
, но задава тази worker
част от спецификацията да е supervisor
.
modules
Трябва да е списък от един елемент - модул. Това е модулът съдържащ callback
функциите на GenServer
имплементация или на Supervisor
имплементация.
По подразбиране е модулът подаден като първи елемент на worker/3
или supervisor/3
. Но е възможно стартиращата функция да е в друг модул и да дадем модула-имплементация тук.
Може и да има стойност :dynamic
но това е свързано със deprecated
поведението GenEvent
.
Supervisor - друг начин за направа
Много е просто да си конфигурираме и ‘пуснем’ Supervisor
без да създаваме специален модул за него с use Supervisor
.
Просто си дефинираме функция подобна на тази:
def start_supervising! do
import Supervisor.Spec, warn: false
children = [
worker(SomeModule, [])
worker(SomeOtherModule, [arg1, arg2])
supervisor(SomeModuleUsingSupervisor, [])
]
options = [strategy: one_for_one]
supervise(children, options)
end
И това е то. Имаме си Supervisor
.
Функциите на Supervisor
Досега си говорихме за статично наблюдение на процеси със Supervisor
-и.
Стартираме Supervisor
със стратегия, опции и спецификации и той използва тези спецификации,
стратегия и опции да стартира, а след това, ако се наложи да рестартира процесите си.
В модула Supervisor
има функции с които можем да добавяме нови спецификации,
да трием спецификации и да спираме процеси ръчно. Нека ги разгледаме.
Supervisor.start_child/2
Динамично добавя нова спецификация към Supervisor
и стартира процес за нея.
Първият аргумент е pid
на Supervisor
процес а вторият - валидна Supervisor.Spec.spec
.
Ако стратегията е simple_one_for_one
вместо спецификация подаваме списък от аргументи за стартиране на
процес по вече описаната спецификация при създаването на Supervisor
-а.
Напомняме че при тази стратегия имаме само един тип процеси-деца.
Използва се в различни случаи. Пример е, да речем при Blogit
, Blogit.Components.Supervisor
модулът реално не
изглежда като в примера по-горе, защото компонентите зависят от информация, която се взима от git repository
от друг процес
и не могат да стартират веднага със Supervisor
-а си.
Реално Blogit.Components.Supervisor
-ът изглежда така:
defmodule Blogit.Components.Supervisor do
use Supervisor
def start_link() do
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_), do: supervise([], [strategy: :one_for_one])
end
Има празен списък от деца.
Впоследствие, когато данните от git repository
-то са готови за ползване използваме следната функция:
defp supervisor_spec(module) do
import Supervisor.Spec, warn: false
worker(module, [])
end
С нейна помощ си правим прости спецификации за всеки компонент. И така ги стартираме:
[
Blogit.Components.Posts,
Blogit.Components.Configuration,
Blogit.Components.PostsByDate
]
|> Enum.each(fn (module) ->
{:ok, _} =
Supervisor.start_child(ComponentsSupervisor, supervisor_spec(module))
end)
Ако процес се стартира без проблеми, получаваме {:ok, pid}
. Можем да получим и {:error, error}
.
За повече информация, вижте в документацията.
Supervisor.count_children/1
Тази функция връща Map
, в който има различни бройки свързани с подадения Supervisor
.
Пример:
Blogit.Components.Supervisor |> Supervisor.count_children
# %{active: 3, specs: 3, supervisors: 0, workers: 3}
active
- това е броят на всички активни процеси-деца, които се управляват от подаденияSupervisor
.specs
- това е броят на всички процеси-деца, няма значение дали са ‘живи’ или ‘мъртви’.supervisors
- броят на всички процеси-деца, които се управляват от подаденияSupervisor
и саSupervisor
-и на свой ред. Няма значение дали са активни или не.workers
- това е броят на всички процеси-деца, които се управляват от подаденияSupervisor
и не саSupervisor
-и. Няма значение дали са активни или не.
В примера трите процеса-компоненти, управлявани от Blogit.Components.Supervisor
са прости GenServer
процеси и са активни, затова active
, spec
и workers
са 3
.
Supervisor.which_children/1
Връща списък с информация за всичките процеси-деца на Supervisor
. Пример:
Blogit.Components.Supervisor |> Supervisor.which_children
# [
# {Blogit.Components.PostsByDate, #PID<0.13088.0>, :worker, [Blogit.Components.PostsByDate] },
# {Blogit.Components.Configuration, #PID<0.13087.0>, :worker, [Blogit.Components.Configuration]},
# {Blogit.Components.Posts, #PID<0.13086.0>, :worker, [Blogit.Components.Posts]}
# ]
Всяка четворка в този списък съответства на процес-дете.
- Това е
id
-то на процеса заSupervisor
-а. Известно и катоchild_id
. За именовани процеси, това е името. Приsimple_one_for_one
стратегия е:undefined
. PID
-ът на процеса, може също и да е атома:restarting
, ако процесът се рестартира или:undefined
, ако процесът не съществува.- Тип -
:worker
или:supervisor
. - Модули - както в спецификацията. Обикновено списък от един модул - модулът с
callback
функциите отGenserver
/Supervisor
поведенията.
В примера, процесите са именовани и активни, всички са :worker
тип.
Supervisor.terminate_child/2
Може да ‘убие’ процес-дете на Supervisor
, подаден като първи аргумент.
Ако стратегията е simple_one_for_one
, процесът се подава като pid
, при другите стратегии като child_id
.
Нека да използваме тази функция за да ‘убием’ процесът Blogit.Components.PostsByDate
:
Blogit.Components.Supervisor |> Supervisor.terminate_child(Blogit.Components.PostsByDate)
# :ok
Blogit.Components.Supervisor |> Supervisor.count_children
# %{active: 2, specs: 3, supervisors: 0, workers: 3}
Тази функция терминира един от worker
-ите и той не се рестартира.
Supervisor
-ът си пази спецификациите, което означава че процесът може да бъде рестартиран.
Това може да стане или ръчно или чрез стратегията. Ако стратегията е one_for_all
, например, ако някой друг процес бъде убит чрез изпращане на EXIT
сигнал,
всички, включително и терминирания от тази функция ще се рестартират.
Ръчното рестартиране става със Supervisor.restart_child/2
.
Supervisor.restart_child/2
Рестартира процес-дете, чиято спецификация се пази в подаденият като първи аргумент Supervisor
.
Процесът се идентифицира по child_id
. От това значи, че при simple_one_to_one
стратегията, тази функция не работи.
Както и при temporary
процеси, чиито child_id
-та се губят.
Нека рестартираме терминирания Blogit.Components.PostsByDate
:
Blogit.Components.Supervisor |> Supervisor.restart_child(Blogit.Components.PostsByDate)
# {:ok, #PID<0.15486.0>}
Blogit.Components.Supervisor |> Supervisor.count_children
# %{active: 3, specs: 3, supervisors: 0, workers: 3}
Всичко е както беше преди терминирането му.
Supervisor.delete_child/2
Изтрива спецификация за дадено child_id
. Не работи за simple_one_to_one
стратегия.
За да проработи тази функция, процесът трябва да е спрян с Supervisor.terminate_child/2
.
Supervisor.stop/3
Спира подаденият като първи аргумент Supervisor
с подадена като втори аргумент причина и изчаква с timeout
- трети аргумент.
Това изчакване по подразбиране е :infinity
, а причината - :normal
. Когато Supervisor
-ът е подаден на тази функция, той спира и всеки от процесите си.
Всеки процес е спрян по shutdown
начина, описан в спецификацията му.
Разбира се ако даденият Supervisor
на свой ред се управлява от друг Supervisor
, той може да бъде рестартиран, което ще рестартира и децата му в зависимост от стратегията му.
Нека разгледаме Supervisor
-и, управлявани от Supervisor
-и.
Дърво от Supervisor-и
Обикновено в една програма ползваща supervision
, Supervisor
-ите образуват дърво.
Имаме Supervisor
-корен с дадена стратегия, който има свои процеси-деца - worker
-и и други Supervisor
-и.
Тези други Supervisor
-и от своя страна си имат свои стратегии и свои деца, които отново могат да бъдат worker
-и и други Supervisor
-и.
Можем да приемем worker
-ите за листа в това дърво. Нека разгледаме още един пример от Blogit
.
Няколко пъти споменахме процеса Blogit.Server
. Той е този, който управлява суровото състояние на блога, той е този който създава компонентите. Ако той ‘умре’,
всичко друго трябва да се рестартира с него.
Именно затова имаме Blogit.Supervisor
, коренът на малкото supervison
дърво на Blogit
.
Ето го кода му:
defmodule Blogit.Supervisor do
use Supervisor
def start_link(repository_provider) do
Supervisor.start_link(__MODULE__, repository_provider, name: __MODULE__)
end
def init(repository_provider) do
children = [
supervisor(Blogit.Components.Supervisor, []),
supervisor(Task.Supervisor, [[name: :tasks_supervisor]]),
worker(Blogit.Server, [repository_provider])
]
opts = [strategy: :one_for_all]
supervise(children, opts)
end
end
Както виждате той има три процеси деца:
Blogit.Components.Supervisor
, който ние разгледахме и опростихме в тази статия. Той започва без спецификации на процеси-деца, но по-късно има триworker
процеса.Blogit.Server
, който при инициализация прочита състоянието на блога отgit rpository
-то, зададено наBlogit
. След като има състоянието, задава три спецификации наBlogit.Components.Supervisor
и ги стартира.:tasks_supervisor
, специаленSupervisor
със стратегияsimple_one_for_one
, който идва сElixir
и е използван тук за създаването на процеси на даден интервал, които проверяват за промени вgit repository
-то.
Ако Blogit.Server
‘умре’, както казахме, най-добре е всичко да се рестартира, компонентите на Blogit.Components.Supervisor
са добавени като спецификации от него и задачите на :tasks_supervisor
-а нотифицират него.
Ако той ‘умре’ и се рестартира, PID
-ът ще е друг и програмата ни няма да работи правилно.
Ако Blogit.Components.Supervisor
умре, би се рестартирал без компонентите си, което ще счупи програмата, така че по-добре всичко да се рестартира и да се инициализира по същият начин като при пускане на програмата.
При :tasks_supervisor
рестартирането на цялата система не е нужно, но задачите които се създават от него работят с Blogit.Server
и ще е по-чисто ако просто цялата система се рестартира при грешка в него.
От всичко това следва, че правилната стратегия за този Supervisor
е :one_for_all
.
В този пример имаме Supervisor
със стратегия one_for_all
, управляващ Supervisor
-и със стратегии one_for_one
и simple_one_for_one
, което е интересно.
Заключение
В тази статия се запознахме със Supervisor
-ите и какво и как те правят за да имаме системи, които при грешка знаят как да се съвземат.
Следващата статия ще разгледа какво представлява една OTP
програма като цяло.