當前位置: 首頁>>代碼示例 >>用法及示例精選 >>正文


Elixir Supervisor用法及代碼示例


Elixir語言中 Supervisor 相關用法介紹如下。

用於實現監督者的行為模塊。

監督者是監督其他進程的進程,我們稱之為child processes。主管用於構建稱為 supervision tree 的分層流程結構。監督樹提供fault-tolerance 並封裝我們的應用程序如何啟動和關閉。

主管可以通過 start_link/2 直接使用子列表啟動,或者您可以定義一個基於模塊的主管來實現所需的回調。下麵的部分在大多數示例中使用 start_link/2 來啟動supervisor,但它也包括一個關於基於模塊的特定部分。

例子

為了啟動監督者,我們需要首先定義一個將被監督的子進程。作為示例,我們將定義一個表示堆棧的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 函數。

到目前為止,我們已經啟動了主管將單個孩子作為元組傳遞以及一個名為 :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 init/2 的第一個參數是上麵"child_spec/1" 部分中定義的子規範列表。

第二個參數是選項的關鍵字列表:

  • :strategy - 監督策略選項。它可以是 :one_for_one:rest_for_one:one_for_all 。必需的。請參閱"Strategies" 部分。

  • :max_restarts - 在一個時間範圍內允許的最大重啟次數。默認為 3

  • :max_seconds - :max_restarts 適用的時間範圍。默認為 5

  • :name - 注冊主管進程的名稱。支持的值在 GenServer 文檔的 "Name registration" 部分進行了說明。可選的。

策略

監督者支持不同的監督策略(通過 :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-lang.org大神的英文原創作品 Supervisor。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。