檢視原始碼 監督者行為

建議同時閱讀此章節與 STDLIB 中的 supervisor

監督原則

監督者的職責是啟動、停止和監控其子進程。監督者的基本概念是透過在必要時重新啟動子進程來保持它們的運作。

要啟動和監控哪些子進程是由子規格清單指定的。子進程會按照此清單指定的順序啟動,並以相反的順序終止。

範例

gen_server 行為啟動伺服器的監督者的回呼模組可能如下所示

-module(ch_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(ch_sup, []).

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [#{id => ch3,
                    start => {ch3, start_link, []},
                    restart => permanent,
                    shutdown => brutal_kill,
                    type => worker,
                    modules => [ch3]}],
    {ok, {SupFlags, ChildSpecs}}.

init/1 的回傳值中的 SupFlags 變數代表監督者旗標

init/1 的回傳值中的 ChildSpecs 變數是子規格的清單。

監督者旗標

這是監督者旗標的類型定義

sup_flags() = #{strategy => strategy(),           % optional
                intensity => non_neg_integer(),   % optional
                period => pos_integer(),          % optional
                auto_shutdown => auto_shutdown()} % optional
    strategy() = one_for_all
               | one_for_one
               | rest_for_one
               | simple_one_for_one
    auto_shutdown() = never
                    | any_significant
                    | all_significant

重新啟動策略

重新啟動策略由回呼函式 init 回傳的監督者旗標映射中的 strategy 鍵指定

SupFlags = #{strategy => Strategy, ...}

此映射中的 strategy 鍵是可選的。如果未提供,則預設為 one_for_one

注意

為簡化起見,本節中顯示的圖表顯示了一個假設所有描繪的子進程的重新啟動類型permanent 的設定。

one_for_one

如果子進程終止,則只會重新啟動該進程。

---
title: One For One Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        p(( )) ~~~ l2[Process Restarted by the Supervisor]
    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p2,p restarted;
    class l1,l2 legend;

one_for_all

如果子進程終止,則會終止所有剩餘的子進程。隨後,所有子進程(包括終止的子進程)都會重新啟動。

---
title: One For All Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        st(( )) ~~~ l2[Process Terminated by the Supervisor]
        p(( )) ~~~ l3[Process Restarted by the Supervisor]
        l4["Note:

           Processes are terminated right to left
           Processes are restarted left to right"]

    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef sterm fill:#ffaa00,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p1,p3,pn,st sterm;
    class p1,p2,p3,pn,p restarted;
    class l1,l2,l3,l4 legend;

rest_for_one

如果子進程終止,則會終止啟動順序中終止進程之後的子進程。隨後,終止的子進程和剩餘的子進程都會重新啟動。

---
title: Rest For One Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        st(( )) ~~~ l2[Process Terminated by the Supervisor]
        p(( )) ~~~ l3[Process Restarted by the Supervisor]
        l4["Note:

           Processes are terminated right to left
           Processes are restarted left to right"]

    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef sterm fill:#ffaa00,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p3,pn,st sterm;
    class p2,p3,pn,p restarted;
    class l1,l2,l3,l4 legend;

simple_one_for_one

請參閱simple-one-for-one 監督者

最大重新啟動強度

監督者具有內建機制來限制在給定時間間隔內可能發生的重新啟動次數。這由回呼函式 init 回傳的監督者旗標映射中的兩個鍵 intensityperiod 指定

SupFlags = #{intensity => MaxR, period => MaxT, ...}

如果在最近 MaxT 秒內發生超過 MaxR 次數的重新啟動,則監督者會終止所有子進程,然後終止自身。在這種情況下,監督者自身的終止原因是 shutdown

當監督者終止時,下一個較高層級的監督者會採取一些動作。它會重新啟動終止的監督者或終止自身。

重新啟動機制的目的是防止進程因相同原因重複死亡,然後又再次重新啟動的情況。

監督者旗標映射中的鍵 intensityperiod 是可選的。如果未提供,則預設值分別為 15

調整強度和週期

預設值經過選擇,對於大多數系統而言都是安全的,即使是具有深度監督階層的系統也是如此,但您可能需要針對您的特定用例調整設定。

首先,強度決定您想要容忍多大的重新啟動爆發。例如,如果重新啟動成功,您可能想要接受最多 5 或 10 次嘗試的爆發,即使在同一秒內也是如此。

其次,您需要考慮持續的故障率,如果崩潰不斷發生,但頻率不足以讓監督者放棄。如果您將強度設定為 10 並將週期設定為低至 1,則監督者將允許子進程每秒重新啟動最多 10 次,持續不斷,在您的日誌中填滿崩潰報告,直到有人手動介入。

因此,您應該將週期設定為足夠長,以便您可以接受監督者以該速率持續運行。例如,如果選擇強度值為 5,將週期設定為 30 秒,則在任何更長的時間內,您最多每 6 秒重新啟動一次,這表示您的日誌不會太快填滿,並且您將有機會觀察到故障並應用修復。

這些選擇很大程度取決於您的問題領域。如果您沒有即時監控和快速修復問題的能力,例如在嵌入式系統中,您可能想要在監督者放棄並升級到下一層級以嘗試自動清除錯誤之前,接受最多每分鐘重新啟動一次。另一方面,如果保持嘗試甚至以高故障率更為重要,您可能希望持續的速率達到每秒 1-2 次重新啟動。

避免常見錯誤

  • 請勿忘記考慮爆發速率。如果您將強度設定為 1,週期設定為 6,則會產生與 5/30 或 10/60 相同的持續錯誤率,但甚至不允許快速連續 2 次重新啟動嘗試。這可能不是您想要的。

  • 如果您想容忍爆發,請勿將週期設定為非常高的值。如果您將強度設定為 5,週期設定為 3600(一小時),則監督者將允許短暫的 5 次重新啟動爆發,但如果在大約一小時後看到另一次單次重新啟動,則會放棄。您可能希望將這些崩潰視為單獨的事件,因此將週期設定為 5 或 10 分鐘會更合理。

  • 如果您的應用程式具有多個監督層級,請勿在所有層級上將重新啟動強度設定為相同的值。請記住,在頂層監督者放棄並終止應用程式之前,重新啟動的總次數將是故障子進程之上所有監督者的強度值的乘積。

    例如,如果頂層允許 10 次重新啟動,而下一層也允許 10 次,則該層級下方的崩潰子進程將重新啟動 100 次,這可能過多。在這種情況下,允許頂層監督者最多重新啟動 3 次可能是一個更好的選擇。

自動關閉

可以將監督者設定為在重要的子進程終止時自動關閉自身。

當監督者代表協作子進程的工作單元時,這很有用,而不是獨立的工作人員。當工作單元完成其工作時,也就是說,當任何或所有重要的子進程終止時,監督者應根據各自的關閉規範,透過以相反的啟動順序終止所有剩餘的子進程,然後終止自身來關閉。

自動關閉由回呼函式 init 回傳的監督者旗標映射中的 auto_shutdown 鍵指定

SupFlags = #{auto_shutdown => AutoShutdown, ...}

此映射中的 auto_shutdown 鍵是可選的。如果未提供,則預設為 never

注意

自動關閉功能僅適用於重要的子進程自行終止時,而不適用於監督者導致其終止的情況。具體而言,在 one_for_allrest_for_one 策略中,子進程因同級進程終止而導致的終止,或透過 supervisor:terminate_child/2 手動終止子進程都不會觸發自動關閉。

never

自動關閉已停用。

在此模式下,不接受指定重要的子進程。如果從 init 回傳的子規格包含重要的子進程,則監督者將拒絕啟動。嘗試動態啟動重要的子進程將被拒絕。

這是預設設定。

any_significant

任何重要的子進程終止時,也就是說,當暫時性的重要子進程正常終止或當臨時的重要子進程正常或異常終止時,監督者將會自動關閉自身。

all_significant

所有重要的子進程都已終止時,也就是說,當最後一個活動的重要子進程終止時,監督者將會自動關閉自身。與 any_significant 相同的規則適用。

警告

自動關閉功能是在 OTP 24.0 中引入的,但使用此功能的應用程式也可以使用較舊的 OTP 版本進行編譯和執行。

然而,當這些應用程式使用早於自動關閉功能出現的 OTP 版本編譯時,會導致程序洩漏,因為它們所依賴的自動關閉功能將不會發生。

如果實作者預期他們的應用程式可能會使用較舊的 OTP 版本編譯,則必須採取適當的預防措施。

警告

應用程式的頂層監督者不應設定為自動關閉,因為當頂層監督者退出時,應用程式會終止。如果應用程式是 permanent,則所有其他應用程式和執行時系統也會終止。

警告

設定為自動關閉的監督者不應設為其各自父監督者的 permanent 子節點,因為它們會在自動關閉後立即重新啟動,過一會兒又再次自動關閉,因此可能會耗盡父監督者的最大重新啟動強度

子節點規格

子節點規格的類型定義如下:

child_spec() = #{id => child_id(),             % mandatory
                 start => mfargs(),            % mandatory
                 restart => restart(),         % optional
                 significant => significant(), % optional
                 shutdown => shutdown(),       % optional
                 type => worker(),             % optional
                 modules => modules()}         % optional
    child_id() = term()
    mfargs() = {M :: module(), F :: atom(), A :: [term()]}
    modules() = [module()] | dynamic
    restart() = permanent | transient | temporary
    significant() = boolean()
    shutdown() = brutal_kill | timeout()
    worker() = worker | supervisor
  • id 用於在監督者內部識別子節點規格。

    id 鍵是強制性的。

    請注意,此識別符偶爾會被稱為「name」。在可能的情況下,現在使用術語「識別符」或「id」,但為了保持向後相容性,仍然可以找到一些「name」的用法,例如在錯誤訊息中。

  • start 定義用於啟動子程序的功能呼叫。它是一個模組-函數-參數元組,用作 apply(M, F, A)

    它將是(或導致)以下任何一個的呼叫:

    start 鍵是強制性的。

  • restart 定義何時重新啟動已終止的子程序。

    • permanent 子程序始終會重新啟動。
    • temporary 子程序永遠不會重新啟動(即使在監督者的重新啟動策略是 rest_for_oneone_for_all 且同級程序終止導致臨時程序終止時也不會重新啟動)。
    • transient 子程序僅在其異常終止時重新啟動,也就是說,終止原因不是 normalshutdown{shutdown,Term}

    restart 鍵是可選的。如果未給定,則會使用預設值 permanent

  • significant 定義是否認為子節點對於監督者的 自動自我關閉很重要。

    對於 重新啟動類型permanent 的子節點,或在 auto_shutdown 設定為 never 的監督者中,將此選項設定為 true 是無效的。

  • shutdown 定義如何終止子程序。

    • brutal_kill 表示子程序會使用 exit(Child, kill) 無條件終止。
    • 整數逾時值表示監督者會透過呼叫 exit(Child, shutdown) 通知子程序終止,然後等待返回退出訊號。如果在指定時間內未收到退出訊號,則會使用 exit(Child, kill) 無條件終止子程序。
    • 如果子程序是另一個監督者,則應將其設定為 infinity,以便給子樹足夠的時間關閉。如果子程序是工作程序,也允許將其設定為 infinity

    警告

    對於類型為 supervisor 的子節點,將關閉時間設定為 infinity 以外的任何值都可能導致競爭條件,其中相關子節點會取消連結其自身的子節點,但無法在被終止之前終止它們。

    當子程序是工作程序時,請小心將關閉時間設定為 infinity。因為在這種情況下,監督樹的終止取決於子程序;它必須以安全的方式實作,且其清除程序必須始終返回。

    shutdown 鍵是可選的。如果未給定,且子節點類型為 worker,則會使用預設值 5000;如果子節點類型為 supervisor,則會使用預設值 infinity

  • type 指定子程序是監督者還是工作程序。

    type 鍵是可選的。如果未給定,則會使用預設值 worker

  • modules 必須是包含單一元素的列表。該元素的值取決於程序的行為

    • 如果子程序是 gen_event,則該元素必須是原子 dynamic
    • 否則,該元素應為 Module,其中 Module 是回呼模組的名稱。

    此資訊由發行處理常式在升級和降級期間使用;請參閱 發行處理

    modules 鍵是可選的。如果未給定,則預設為 [M],其中 M 來自子節點的起始 {M,F,A}

範例:先前範例中啟動伺服器 ch3 的子節點規格如下所示:

#{id => ch3,
  start => {ch3, start_link, []},
  restart => permanent,
  shutdown => brutal_kill,
  type => worker,
  modules => [ch3]}

或簡化,依賴預設值:

#{id => ch3,
  start => {ch3, start_link, []},
  shutdown => brutal_kill}

範例:從關於 gen_event 的章節啟動事件管理員的子節點規格:

#{id => error_man,
  start => {gen_event, start_link, [{local, error_man}]},
  modules => dynamic}

伺服器和事件管理員都是已註冊的程序,預期可以始終存取。因此,它們被指定為 permanent

ch3 在終止之前不需要進行任何清理。因此,不需要關閉時間,但 brutal_kill 就足夠了。error_man 可能需要一些時間讓事件處理常式進行清理,因此關閉時間設定為 5000 毫秒(這是預設值)。

範例:啟動另一個監督者的子節點規格:

#{id => sup,
  start => {sup, start_link, []},
  restart => transient,
  type => supervisor} % will cause default shutdown=>infinity

啟動監督者

在先前的範例中,監督者是透過呼叫 ch_sup:start_link() 啟動的。

start_link() ->
    supervisor:start_link(ch_sup, []).

ch_sup:start_link 呼叫函數 supervisor:start_link/2,它會產生並連結到一個新的程序,即監督者。

  • 第一個引數 ch_sup 是回呼模組的名稱,也就是 init 回呼函數所在的模組。
  • 第二個引數 [] 是一個術語,會原封不動地傳遞給回呼函數 init。在此,init 不需要任何資料,並忽略該引數。

在這種情況下,監督者不會註冊。而是必須使用其 pid。可以透過呼叫 supervisor:start_link({local, Name}, Module, Args)supervisor:start_link({global, Name}, Module, Args) 來指定名稱。

新的監督者程序會呼叫回呼函數 ch_sup:init([])init 必須返回 {ok, {SupFlags, ChildSpecs}}

init(_Args) ->
    SupFlags = #{},
    ChildSpecs = [#{id => ch3,
                    start => {ch3, start_link, []},
                    shutdown => brutal_kill}],
    {ok, {SupFlags, ChildSpecs}}.

隨後,監督者會根據起始規格中的子節點規格啟動其子程序。在此案例中,有一個名為 ch3 的子程序。

supervisor:start_link/3 是同步的。在所有子程序都已啟動之前,它不會返回。

新增子程序

除了由子節點規格定義的靜態監督樹之外,還可以透過呼叫 supervisor:start_child(Sup, ChildSpec) 將動態子程序新增至現有的監督者。

Sup 是監督者的 pid 或名稱。ChildSpec子節點規格

使用 start_child/2 新增的子程序的行為與其他子程序相同,但有一個重要的例外:如果監督者終止並重新建立,則會遺失動態新增至該監督者的所有子程序。

停止子程序

可以透過呼叫 supervisor:terminate_child(Sup, Id),依照關閉規格停止任何子程序,無論是靜態或動態的。

停止設定為 自動關閉的監督者的重要子節點,不會觸發自動關閉。

透過呼叫 supervisor:delete_child(Sup, Id) 來刪除已停止的子程序的子節點規格。

Sup 是監督者的 pid 或名稱。Id 是與子節點規格中的 id 鍵相關聯的值。

與動態新增的子程序一樣,如果監督者本身重新啟動,則刪除靜態子程序的影響會遺失。

簡化的 one_for_one 監督者

具有重新啟動策略的監督者 simple_one_for_one 是一種簡化的 one_for_one 監督者,其中所有子程序都是相同程序的動態新增實例。

以下是一個 simple_one_for_one 監督者的回呼模組範例

-module(simple_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(simple_sup, []).

init(_Args) ->
    SupFlags = #{strategy => simple_one_for_one,
                 intensity => 0,
                 period => 1},
    ChildSpecs = [#{id => call,
                    start => {call, start_link, []},
                    shutdown => brutal_kill}],
    {ok, {SupFlags, ChildSpecs}}.

當啟動時,監督者不會啟動任何子程序。相反地,所有子程序都需要透過呼叫 supervisor:start_child(Sup, List) 來動態新增。

Sup 是監督者的 PID 或名稱。List 是一個任意的項列表,它們會被添加到子程序規格中指定的參數列表中。如果啟動函數被指定為 {M, F, A},則會呼叫 apply(M, F, A++List) 來啟動子程序。

例如,將一個子程序新增到上面的 simple_sup

supervisor:start_child(Pid, [id1])

結果是,子程序會呼叫 apply(call, start_link, []++[id1]),或者實際上是

call:start_link(id1)

可以使用以下方法來終止 simple_one_for_one 監督者下的子程序

supervisor:terminate_child(Sup, Pid)

Sup 是監督者的 PID 或名稱,而 Pid 是子程序的 PID。

由於 simple_one_for_one 監督者可以有多個子程序,它會以非同步方式關閉它們。這表示子程序將會並行執行它們的清理,因此它們停止的順序沒有被定義。

啟動、重新啟動和手動終止子程序是在監督者程序上下文中執行的同步操作。這表示監督者程序在執行任何這些操作時會被封鎖。子程序負責盡可能縮短其啟動和關閉階段的時間。

停止

由於監督者是監督樹的一部分,它會自動被其監督者終止。當被要求關閉時,監督者會根據各自的關閉規格,以相反的啟動順序終止所有子程序,然後再終止自身。

如果監督者被設定為在任何或所有重要子程序終止時自動關閉,則當任何或最後一個活動的重要子程序終止時,它將會關閉自身。關閉本身遵循與上述相同的程序,也就是說,監督者會先以相反的啟動順序終止所有剩餘的子程序,然後再終止自身。

手動停止與自動關閉

由於一些原因,不應從其自身樹中的子程序透過 supervisor:terminate_child/2 來手動停止監督者。

  1. 子程序將不僅需要知道它想要停止的監督者的 PID 或已註冊名稱,還需要知道監督者父監督者的 PID 或名稱,以便告訴父監督者停止它想要停止的監督者。這可能會使重組監督樹變得困難。
  2. supervisor:terminate_child/2 是一個阻塞呼叫,它只有在父監督者完成應該停止的監督者的關閉後才會返回。除非該呼叫是從產生的程序進行的,否則會導致死鎖,因為監督者會等待子程序作為其關閉程序的一部分退出,而子程序會等待監督者關閉。如果子程序正在捕捉退出,則此死鎖將持續到子程序的關閉逾時時間到期為止。
  3. 當監督者停止子程序時,它會等待關閉完成,然後才接受其他呼叫,也就是說,監督者在此之前將沒有反應。如果終止需要一些時間才能完成,尤其是在沒有仔細考慮前一點中概述的事項時,該監督者可能會長時間沒有反應。

相反地,通常更好的方法是依賴自動關閉

  1. 子程序不需要知道有關其監督者及其父級的任何資訊,甚至不需要知道它首先是監督樹的一部分。相反地,只有託管子程序的監督者必須知道其哪些子程序是重要的子程序,以及何時關閉自身。
  2. 子程序不需要執行任何特殊操作來關閉其所屬的工作單元。它所需要做的就是在完成其啟動時執行的任務後正常終止。
  3. 自動關閉自身的監督者將完全獨立於其父監督者執行所需的關閉步驟。父監督者最後才會注意到其子監督者已終止。由於父監督者沒有參與關閉過程,因此不會被封鎖。