Elixir語言中 Supervisor
相關用法介紹如下。
用於實現監督者的行為模塊。
監督者是監督其他進程的進程,我們稱之為child processes
。主管用於構建稱為 supervision tree
的分層流程結構。監督樹提供fault-tolerance 並封裝我們的應用程序如何啟動和關閉。
主管可以通過
直接使用子列表啟動,或者您可以定義一個基於模塊的主管來實現所需的回調。下麵的部分在大多數示例中使用start_link/2
來啟動supervisor,但它也包括一個關於基於模塊的特定部分。start_link/2
例子
為了啟動監督者,我們需要首先定義一個將被監督的子進程。作為示例,我們將定義一個表示堆棧的GenServer:
defmodule Stack do
use GenServer
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
## Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, head}, tail) do
{:noreply, [head | tail]}
end
end
堆棧是列表的一個小包裝器。它允許我們將一個元素放在棧頂,通過預先添加到列表中,並通過模式匹配獲得棧頂。
我們現在可以啟動一個主管,它將啟動並監督我們的堆棧過程。第一步是定義一個子規範列表,以控製每個子規範的行為方式。每個子規範都是一個映射,如下所示:
children = [
# The Stack is a child started via Stack.start_link([:hello])
%{
id: Stack,
start: {Stack, :start_link, [[:hello]]}
}
]
# Now we start the supervisor with the children and a strategy
{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
# After started, we can query the supervisor for information
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
請注意,在啟動 GenServer 時,我們使用名稱 Stack
注冊它,這允許我們直接調用它並獲取堆棧中的內容:
GenServer.call(Stack, :pop)
#=> :hello
GenServer.cast(Stack, {:push, :world})
#=> :ok
GenServer.call(Stack, :pop)
#=> :world
但是,我們的堆棧服務器中有一個錯誤。如果我們調用 :pop
並且堆棧為空,它將崩潰,因為沒有子句匹配:
GenServer.call(Stack, :pop)
** (exit) exited in: GenServer.call(Stack, :pop, 5000)
幸運的是,由於服務器由主管監督,主管將自動啟動一個新的,初始堆棧為 [:hello]
:
GenServer.call(Stack, :pop)
#=> :hello
主管支持不同的策略;在上麵的示例中,我們選擇了 :one_for_one
。此外,每個主管可以有許多工人和/或主管作為孩子,每個人都有自己的配置(如"Child specification" 部分所述)。
本文檔的其餘部分將介紹如何指定子進程、如何啟動和停止它們、不同的監督策略等等。
子規格
子規範說明了主管如何啟動、關閉和重新啟動子進程。
子規範是一個最多包含 6 個元素的映射。以下列表中的前兩個鍵是必需的,其餘的是可選的:
-
:id
- 主管內部用於標識子規範的任何術語;默認為給定的模塊。在:id
值衝突的情況下,主管將拒絕初始化並要求明確的 ID。此 key 是必需的。 -
:start
- 帶有 module-function-args 的元組將被調用以啟動子進程。此 key 是必需的。 -
:restart
- 定義終止子進程何時應該重新啟動的原子(參見下麵的"Restart values" 部分)。此鍵是可選的,默認為:permanent
。 -
:shutdown
- 定義子進程應如何終止的整數或原子(請參閱下麵的 "Shutdown values" 部分)。此鍵是可選的,如果類型為:worker
則默認為5_000
,如果類型為:supervisor
則默認為:infinity
。 -
:type
- 指定子進程是:worker
或:supervisor
。此鍵是可選的,默認為:worker
。
還有第六個鍵 :modules
,它是可選的,很少更改。它是根據:start
值自動設置的。
讓我們了解:shutdown
和:restart
選項控製的內容。
關機值 (:shutdown)
:shutdown
選項支持以下關閉值:
-
:brutal_kill
- 使用Process.exit(child, :kill)
無條件立即終止子進程。 -
任何整數 >= 0 - 主管在發出
Process.exit(child, :shutdown)
信號後等待其子級終止的時間量(以毫秒為單位)。如果子進程沒有捕獲退出,初始的:shutdown
信號將立即終止子進程。如果子進程正在捕獲退出,它有給定的時間來終止。如果它沒有在指定時間內終止,則子進程將由主管通過Process.exit(child, :kill)
無條件終止。 -
:infinity
- 作為整數工作,但主管將無限期地等待子進程終止。如果子進程是主管,則推薦值為:infinity
,以給主管及其子進程足夠的時間關閉。此選項可用於普通工人,但不鼓勵這樣做並且需要格外小心。如果不小心使用,子進程將永遠不會終止,從而阻止您的應用程序也終止。
重啟值 (:restart)
:restart
選項控製主管應該認為什麽是成功終止。如果終止成功,主管將不會重新啟動子進程。如果子進程崩潰,主管將啟動一個新進程。
:restart
選項支持以下重啟值:
-
:permanent
- 子進程總是重新啟動。 -
:temporary
- 無論監督策略如何,子進程都不會重新啟動:任何終止(甚至異常)都被認為是成功的。 -
:transient
- 僅當子進程異常終止時才會重新啟動,即,退出原因不是:normal
、:shutdown
或{:shutdown, term}
。
要更全麵地了解退出原因及其影響,請參閱“退出原因和重新啟動”部分。
child_spec/1
在啟動主管時,我們傳遞一個子規範列表。這些規範是說明主管應該如何啟動、停止和重新啟動其每個子項的映射:
%{
id: Stack,
start: {Stack, :start_link, [[:hello]]}
}
上麵的映射定義了一個具有 Stack
的 :id
的子節點,該子節點通過調用 Stack.start_link([:hello])
來啟動。
但是,將每個子節點的子規範指定為映射可能很容易出錯,因為我們可能會更改 Stack 實現並忘記更新其規範。這就是為什麽 Elixir 允許您傳遞帶有模塊名稱和 start_link
參數的元組而不是規範的原因:
children = [
{Stack, [:hello]}
]
然後主管將調用Stack.child_spec([:hello])
來檢索子規範。現在Stack
模塊負責構建自己的規範,例如,我們可以這樣寫:
def child_spec(arg) do
%{
id: Stack,
start: {Stack, :start_link, [arg]}
}
end
幸運的是,use GenServer
已經定義了一個與上麵完全相同的 Stack.child_spec/1
。如果您需要自定義
,可以將選項直接傳遞給 GenServer
use GenServer
:
use GenServer, restart: :transient
最後,請注意,也可以簡單地將 Stack
模塊作為子模塊傳遞:
children = [
Stack
]
僅給出模塊名稱時,它等效於 {Stack, []}
。通過將Map規範替換為 {Stack, [:hello]}
或 Stack
,我們使用 use GenServer
定義的默認實現將子規範封裝在 Stack
模塊中。我們現在可以與其他開發人員共享我們的Stack
worker,他們可以直接將其添加到他們的監督樹中,而不必擔心 worker 的底層細節。
總體而言,子規範可以是以下之一:
- 表示子規範本身的映射 - 如 "Child specification" 部分所述
- 一個元組,其中一個模塊作為第一個元素,開始參數作為第二個元素 - 例如
{Stack, [:hello]}
。在這種情況下,調用Stack.child_spec([:hello])
來檢索子規範 - 一個模塊 - 例如
Stack
。在這種情況下,調用Stack.child_spec([])
來檢索子規範
如果需要將元組或模塊子規範轉換為映射或修改子規範,可以使用
函數。例如,要使用不同的 Supervisor.child_spec/2
:id
和 10 秒(10_000 毫秒)的 :shutdown
值運行堆棧:
children = [
Supervisor.child_spec({Stack, [:hello]}, id: MyStack, shutdown: 10_000)
]
基於模塊的主管
在上麵的示例中,通過將監督結構傳遞給
來啟動監督者。但是,也可以通過顯式定義監督模塊來創建監督者:start_link/2
defmodule MyApp.Supervisor do
# Automatically defines child_spec/1
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
{Stack, [:hello]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
這兩種方法之間的區別在於,基於模塊的監督者可以讓您更直接地控製監督者的初始化方式。我們不是使用自動初始化的子項列表調用
,而是通過在其Supervisor.start_link/2
回調中調用init/1
來手動初始化子項。Supervisor.init/2
use Supervisor
還定義了一個 child_spec/1
函數,它允許我們將 MyApp.Supervisor
作為另一個主管的孩子或在您的監督樹的頂部運行:
children = [
MyApp.Supervisor
]
Supervisor.start_link(children, strategy: :one_for_one)
一般準則是僅在監督樹的頂部使用不帶回調模塊的監督者,通常在
回調中。我們建議對應用程序中的任何其他主管使用基於模塊的主管,以便它們可以作為樹中另一個主管的子級運行。 Application.start/2
自動生成的Supervisor
child_spec/1
可以使用以下選項進行自定義:
:id
- 子規範標識符,默認為當前模塊:restart
- 當應重新啟動主管時,默認為:permanent
緊接在use Supervisor
之前的@doc
注釋將附加到生成的child_spec/1
函數。
start_link/2
、 init/2
和策略
start_link/2
init/2
到目前為止,我們已經啟動了主管將單個孩子作為元組傳遞以及一個名為 :one_for_one
的策略:
children = [
{Stack, [:hello]}
]
Supervisor.start_link(children, strategy: :one_for_one)
或從
回調內部:init/1
children = [
{Stack, [:hello]}
]
Supervisor.init(children, strategy: :one_for_one)
和start_link/2
的第一個參數是上麵"child_spec/1" 部分中定義的子規範列表。init/2
第二個參數是選項的關鍵字列表:
-
:strategy
- 監督策略選項。它可以是:one_for_one
、:rest_for_one
或:one_for_all
。必需的。請參閱"Strategies" 部分。 -
:max_restarts
- 在一個時間範圍內允許的最大重啟次數。默認為3
。 -
:max_seconds
-:max_restarts
適用的時間範圍。默認為5
。 -
:name
- 注冊主管進程的名稱。支持的值在GenServer
策略
監督者支持不同的監督策略(通過 :strategy
選項,如上所示):
-
:one_for_one
- 如果子進程終止,則僅重新啟動該進程。 -
:one_for_all
- 如果子進程終止,則所有其他子進程都將終止,然後所有子進程(包括終止的子進程)將重新啟動。 -
:rest_for_one
- 如果子進程終止,則終止的子進程和在它之後啟動的其餘子進程將終止並重新啟動。
上麵的進程終止是指終止不成功,由:restart
選項決定。
要動態監督孩子,請參閱
。DynamicSupervisor
名稱注冊
主管與
綁定到相同的名稱注冊規則。在 GenServer
的文檔中閱讀有關這些規則的更多信息。GenServer
啟動和關閉
當主管啟動時,它會遍曆所有子規範,然後按照定義的順序啟動每個子規範。這是通過調用子規範中 :start
鍵下定義的函數來完成的,通常默認為 start_link/1
。
然後為每個子進程調用start_link/1
(或自定義)。 start_link/1
函數必須返回 {:ok, pid}
其中 pid
是鏈接到主管的新進程的進程標識符。子進程通常通過執行
回調來開始其工作。一般來說,init/1
init
回調是我們初始化和配置子進程的地方。
關閉過程以相反的順序發生。
當主管關閉時,它會以與列出的相反順序終止所有孩子。終止是通過 Process.exit(child_pid, :shutdown)
向子進程發送關閉退出信號,然後等待子進程終止的時間間隔來實現的。此時間間隔默認為 5000 毫秒。如果子進程在此時間間隔內沒有終止,則主管突然終止子進程,原因是 :kill
。可以在子規範中配置關閉時間,這將在下一節中詳細介紹。
如果子進程沒有捕獲退出,它會在收到第一個退出信號時立即關閉。如果子進程正在捕獲退出,則調用terminate
回調,並且子進程必須在合理的時間間隔內終止,然後才被主管突然終止。
換句話說,如果一個進程在你的應用程序或監督樹關閉時自行清理很重要,那麽這個進程必須捕獲出口,並且它的子規範應該指定正確的 :shutdown
值,確保它在合理的範圍內終止間隔。
退出原因和重啟
主管根據其:restart
配置重新啟動子進程。例如,當 :restart
設置為 :transient
時,如果子進程因 :normal
、:shutdown
或 {:shutdown, term}
原因退出,則主管不會重新啟動子進程。
那麽有人可能會問:退出的時候應該選擇哪個退出原因呢?有三個選項:
-
:normal
- 在這種情況下,不會記錄退出,在瞬態模式下不會重新啟動,並且鏈接的進程不會退出 -
:shutdown
或{:shutdown, term}
- 在這種情況下,不會記錄退出,在瞬態模式下不會重新啟動,並且鏈接進程以相同的原因退出,除非它們正在捕獲退出 -
任何其他術語-在這種情況下,將記錄退出,在瞬態模式下重新啟動,並且鏈接的進程以相同的原因退出,除非它們正在捕獲退出
請注意,達到最大重啟強度的主管將退出 :shutdown
原因。在這種情況下,隻有在將 :restart
選項設置為 :permanent
(默認值)的情況下定義其子規範時,才會重新啟動主管。
相關用法
- Elixir Supervisor.Spec.worker用法及代碼示例
- Elixir Supervisor.child_spec用法及代碼示例
- Elixir Supervisor.init用法及代碼示例
- Elixir Supervisor.Spec用法及代碼示例
- Elixir Supervisor.Spec.supervise用法及代碼示例
- Elixir Supervisor.Spec.supervisor用法及代碼示例
- Elixir StringIO.flush用法及代碼示例
- Elixir Stream用法及代碼示例
- Elixir String.contains?用法及代碼示例
- Elixir String.reverse用法及代碼示例
- Elixir Stream.drop_while用法及代碼示例
- Elixir String.to_integer用法及代碼示例
- Elixir System.stop用法及代碼示例
- Elixir Stream.map_every用法及代碼示例
- Elixir Stream.iterate用法及代碼示例
- Elixir String.pad_trailing用法及代碼示例
- Elixir Stream.dedup_by用法及代碼示例
- Elixir Stream.interval用法及代碼示例
- Elixir String.split_at用法及代碼示例
- Elixir String.valid?用法及代碼示例
- Elixir String.replace_prefix用法及代碼示例
- Elixir String.printable?用法及代碼示例
- Elixir Stream.zip用法及代碼示例
- Elixir System.fetch_env!用法及代碼示例
- Elixir String.replace_trailing用法及代碼示例
注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 Supervisor。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。