誰來監管監管者?
從壞到好

監管者是您會用到的 OTP 中最有用的部分之一。我們在錯誤與程序和設計並行應用程式中看過基本的監管者。我們將它們視為一種在發生錯誤時,透過重新啟動錯誤的程序來維持軟體運作的方法。
更詳細地說,我們的監管者會啟動一個工作者程序,連結到它,並使用 process_flag(trap_exit,true)
捕捉結束訊號,以了解程序何時終止並重新啟動它。當我們想要重新啟動時,這樣做沒問題,但它也很笨。讓我們想像一下,您正在使用遙控器打開電視。如果第一次沒用,您可能會嘗試一兩次,以防萬一您沒有按對或訊號出錯。我們的監管者,如果它正在嘗試打開那台電視,會一直嘗試下去,即使遙控器沒電或根本不適用於電視。這是一個相當愚蠢的監管者。
我們的監管者另一個愚蠢之處是,它們一次只能監視一個工作者。別誤會,有時為單一工作者配備一個監管者很有用,但在大型應用程式中,這表示您只能擁有監管者鏈,而不是樹狀結構。您要如何監管需要 2 或 3 個工作者同時執行的任務?透過我們的實作,根本無法做到。
幸運的是,OTP 監管者提供了處理這些情況(以及更多情況)的彈性。它們讓您可以定義工作者在放棄之前,在給定的時間內應該重新啟動多少次。它們讓您可以讓每個監管者擁有多個工作者,甚至讓您可以在幾種模式之間選擇,以決定它們在發生故障時應該如何相互依賴。
監管者概念
監管者是最容易使用和理解的行為之一,但也是最難以寫出良好設計的行為之一。有各種與監管者和應用程式設計相關的策略,但在那之前,我們需要了解更多基本概念,否則會很困難。
到目前為止,我在文中使用了「工作者」這個詞,但沒有太多定義。工作者的定義與監管者有點相反。如果監管者應該是除了確保其子程序在終止時重新啟動之外什麼都不做的程序,那麼工作者就是負責執行實際工作,並可能在執行過程中終止的程序。它們通常不被信任。
監管者可以監管工作者和其他監管者,而工作者絕不應在任何位置使用,除非在另一個監管者之下

為什麼每個程序都應該受到監管?嗯,這個想法很簡單:如果由於某些原因您正在產生不受監管的程序,您怎麼能確定它們已消失或沒有?如果您無法測量某件事,它就不存在。現在,如果一個程序存在於所有監管樹之外的虛空中,您怎麼知道它存在與否?它是怎麼到那裡的?它會再次發生嗎?
如果它真的發生了,您會發現自己非常緩慢地洩漏記憶體。慢到您的虛擬機器可能會突然因為不再有記憶體而終止,而且慢到您可能無法輕易地追蹤它,直到它一再發生。當然,您可能會說「如果我小心並知道自己在做什麼,情況會很好」。也許它們會很好,是的。也許它們不會。在生產系統中,您不想冒險,在 Erlang 的情況下,這就是您一開始就有垃圾收集的原因。保持事物的監管非常有用。
另一個它有用的原因是,它允許以良好的順序終止應用程式。您可能會編寫不打算永遠運行的 Erlang 軟體。您仍然會希望它乾淨俐落地終止。您怎麼知道一切都準備好關閉了?透過監管者,這很容易。每當您想要終止應用程式時,您就會讓虛擬機器的頂層監管者關閉(這會透過類似 init:stop/1
的函式為您完成)。然後,該監管者會要求其每個子程序終止。如果某些子程序是監管者,它們也會執行相同的操作

這為您提供了良好順序的虛擬機器關閉,這在沒有讓您的所有程序都成為樹狀結構的一部分的情況下很難做到。
當然,有時候您的程序會因為某些原因而卡住,並且無法正確終止。當這種情況發生時,監管者有一種方法可以殘酷地終止程序。
這就是監管者基本理論的全部內容。我們有工作者、監管者、監管樹、指定依賴關係的不同方式、告訴監管者何時放棄嘗試或等待其子程序的方式等等。這並不是監管者可以做的全部,但就目前而言,這將讓我們涵蓋實際使用它們所需的基本內容。
使用監管者
到目前為止,這是一個非常暴力的章節:父母花時間將他們的孩子綁在樹上,強迫他們工作,然後殘酷地殺死他們。如果沒有實際執行這一切,我們就不會是真正的虐待狂。
當我說監管者很容易使用時,我不是在開玩笑。有一個單一的回呼函式需要提供:init/1
。它會採用一些引數,僅此而已。問題是它會傳回相當複雜的東西。以下是監管者傳回值的範例
{ok, {{one_for_all, 5, 60}, [{fake_id, {fake_mod, start_link, [SomeArg]}, permanent, 5000, worker, [fake_mod]}, {other_id, {event_manager_mod, start_link, []}, transient, infinity, worker, dynamic}]}}.
這是什麼?是的,這非常複雜。一個更通用的定義可能更容易使用
{ok, {{RestartStrategy, MaxRestart, MaxTime},[ChildSpecs]}}.
其中 ChildSpec 代表子規格。RestartStrategy 可以是 one_for_one
、rest_for_one
、one_for_all
和 simple_one_for_one
中的任何一個。
one_for_one
One for one 是一個直觀的重新啟動策略。它基本上表示,如果您的監管者監管許多工作者,並且其中一個失敗,則應該只重新啟動那一個。每當被監管的程序是獨立的並且彼此之間沒有真正的關聯,或者當程序可以重新啟動並在不影響其同級的情況下遺失其狀態時,您應該使用 one_for_one
。

one_for_all
One for all 與火槍手無關。每當您在單一監管者下的所有程序都非常依賴彼此才能正常運作時,都應該使用它。假設您已決定在我們在怒火對抗有限狀態機章節中實作的交易系統之上新增一個監管者。如果其中一個交易者崩潰,只重新啟動其中一個交易者實際上沒有意義,因為它們的狀態會不同步。同時重新啟動它們兩者將是一個更明智的選擇,而 one_for_all
將是該策略。

rest_for_one
這是一種更特定的策略。每當您必須啟動以鏈式方式相互依賴的程序(A 啟動 B,B 啟動 C,C 啟動 D 等)時,您可以使用 rest_for_one
。它在您有類似依賴關係的服務中也很有用(X 單獨運作,但 Y 依賴 X,而 Z 依賴兩者)。rest_for_one
重新啟動策略基本上會做到,如果一個程序終止,則所有在其之後啟動的程序(依賴它)都會重新啟動,反之則不然。

simple_one_for_one
simple_one_for_one
重新啟動策略並不是最簡單的策略。當我們使用它時,我們將更詳細地了解它,但它基本上會使其僅採用一種子程序,並且當您想要將它們動態新增到監管者時使用它,而不是讓它們靜態啟動。
換句話說,simple_one_for_one
監管者只是在那裡,它知道它只能產生一種子程序。每當您想要新的子程序時,您就會要求它,然後您就會得到它。理論上,可以使用標準的 one_for_one
監管者來完成這種事情,但是使用簡單版本有實際的優勢。
注意:one_for_one
和 simple_one_for_one
之間的一個很大差異是,one_for_one
保留它擁有的所有子程序(以及如果您不清除它,則曾經擁有的所有子程序)的清單,依序啟動,而 simple_one_for_one
則為其所有子程序保留單一定義,並使用 dict
來保存其資料。基本上,當程序崩潰時,當您有大量子程序時,simple_one_for_one
監管者會快得多。
重新啟動限制
RestartStrategy 元組的最後一部分是 MaxRestart 和 MaxTime 這對變數。這個想法基本上是,如果 MaxTime(以秒為單位)內發生超過 MaxRestart 次重新啟動,監管者就會放棄您的程式碼,關閉它,然後殺死自己,永不返回(情況就是這麼糟糕)。幸運的是,該監管者的監管者可能仍然對其子程序抱有希望,並重新啟動它們。
子規格
現在是傳回值的 ChildSpec 部分。ChildSpec 代表子規格。先前我們有以下兩個子規格
[{fake_id, {fake_mod, start_link, [SomeArg]}, permanent, 5000, worker, [fake_mod]}, {other_id, {event_manager_mod, start_link, []}, transient, infinity, worker, dynamic}]
子規格可以更抽象的形式描述為
{ChildId, StartFunc, Restart, Shutdown, Type, Modules}.
ChildId
ChildId 只是監管者內部使用的內部名稱。您很少需要自己使用它,儘管它可能對偵錯很有用,有時當您決定實際取得監管者的所有子程序清單時也很有用。任何術語都可用於 Id。
StartFunc
StartFunc 是一個元組,說明如何啟動子程序。這是我們之前已使用過幾次的標準 {M,F,A}
格式。請注意,這裡的啟動函數非常重要,必須符合 OTP 規範,並在執行時連結至其呼叫者(提示:請一直使用包裝在您自己模組中的 gen_*:start_link()
)。
重新啟動 (Restart)
Restart 告訴監督者在特定子程序終止時該如何反應。它可以採用三個值:
- 永久 (permanent)
- 暫時 (temporary)
- 瞬時 (transient)
永久程序無論如何都應該重新啟動。我們在之前的應用程式中實作的監督者僅使用了此策略。這通常用於在您的節點上執行的重要、長時間運行的程序(或服務)。
另一方面,暫時程序是不應該重新啟動的程序。它們適用於預期會失敗且依賴它們的程式碼片段較少的短暫工作者。
瞬時程序則介於兩者之間。它們旨在運行直到正常終止,然後將不會重新啟動。但是,如果它們因異常原因(退出原因不是 normal
)而終止,則會重新啟動。此重新啟動選項通常用於需要成功完成其任務,但在完成任務後將不再使用的工作者。
您可以讓所有三種類型的子程序混合在單一監督者下。這可能會影響重新啟動策略:臨時程序的終止不會觸發 one_for_all
重新啟動,但是如果永久程序先終止,則該臨時程序可能會在同一監督者下重新啟動!
關閉 (Shutdown)
在本文的前面,我提到能夠藉由監督者的協助來關閉整個應用程式。這是它的做法。當被要求終止頂層監督者時,它會在每個 Pids 上呼叫 exit(ChildPid, shutdown)
。如果子程序是工作者並捕捉退出,它會呼叫自己的 terminate
函數。否則,它將會直接終止。當監督者收到 shutdown
訊號時,它會以相同的方式將其轉發給自己的子程序。
因此,子程序規範的 Shutdown 值用於給予終止的截止時間。對於某些工作者,您知道您可能需要執行一些操作,例如正確關閉檔案、通知您正在離開的服務等等。在這些情況下,您可能會希望使用特定的截止時間,以毫秒為單位,或者如果真的很有耐心,可以使用 infinity
。如果時間過去而沒有任何反應,則該程序會被 exit(Pid, kill)
強行終止。如果您不在乎子程序,並且它幾乎可以在不需要任何逾時的情況下終止而不會產生任何後果,則原子 brutal_kill
也是可接受的值。brutal_kill
會使子程序以 exit(Pid, kill)
的方式終止,這是無法捕捉且立即的。
選擇一個好的 Shutdown 值有時會很複雜或棘手。如果您有一個具有 Shutdown 值的監督者鏈,例如:5000 -> 2000 -> 5000 -> 5000
,則最後兩個監督者很可能會被強行終止,因為第二個監督者的截止時間較短。這完全取決於應用程式,並且在此主題上很少有通用的提示。
注意:請務必注意,simple_one_for_one
子程序不遵守 Shutdown 時間的此規則。在 simple_one_for_one
的情況下,監督者只會退出,並且在監督者消失後,將由每個工作者自行終止。
類型 (Type)
類型只是讓監督者知道子程序是工作者還是監督者。當使用更進階的 OTP 功能升級應用程式時,這將非常重要,但是您目前並不需要太過擔心 — 只需說實話,一切都應該沒問題。您必須相信您的監督者!
模組 (Modules)
Modules 是一個包含一個元素的列表,即子程序行為所使用的回呼模組名稱。例外情況是當您有事先不知道其身分的回呼模組時(例如事件管理員中的事件處理程式)。在這種情況下,Modules 的值應該是 dynamic
,以便整個 OTP 系統在使用更進階的功能(例如發布)時知道要聯繫誰。
更新 (update)
自 18.0 版起,監督者結構可以以映射的形式提供,形式為 {#{strategy => RestartStrategy, intensity => MaxRestart, period => MaxTime}, [#{id => ChildId, start => StartFunc, restart => Restart, shutdown => Shutdown, type => Type, modules => Module}}
。
這與現有的結構幾乎相同,但使用映射而不是元組。supervisor
模組定義了一些預設值,但為了清楚易讀,讓維護您程式碼的人能夠理解,最好明確地寫出完整的規格。
太棒了,我們現在已經具備啟動受監管程序所需的基本知識了。您可以休息一下並消化一下,或者繼續學習更多內容!

測試一下 (Testing it Out)
需要一些練習。在練習方面,完美的例子就是樂團練習。好吧,不是那麼完美,但請耐心聽我說一下,因為我們將以相當類比的方式來嘗試編寫監督者等等。
我們正在管理一個名為 *RSYNC 的樂團,該樂團由一些程式設計師組成,他們演奏一些常見的樂器:一位鼓手、一位歌手、一位貝斯手和一位鍵盤吉他手,以紀念所有被遺忘的 80 年代榮耀。儘管翻唱了一些復古熱門歌曲,例如「Thread Safety Dance」和「Saturday Night Coder」,但該樂團很難找到場地。我對整個情況感到惱火,又帶著另一個因糖分衝擊而產生的想法衝進您的辦公室,要在 Erlang 中模擬一個樂團,因為「至少我們不會聽到我們這些傢伙的聲音」。您很累,因為您和鼓手住在同一間公寓裡(鼓手是這個樂團中最弱的一環,但他們和他黏在一起,因為他們真的不認識其他鼓手),所以您接受了。
音樂家 (Musicians)
我們可以做的第一件事是編寫個別的樂團成員。對於我們的用例,musicians 模組將實現 gen_server
。每位音樂家都會將樂器和技能等級作為參數(因此我們可以說鼓手很爛,而其他人都還不錯)。一旦音樂家產生,他/她將開始演奏。如果需要,我們也可以選擇停止他們。這給了我們以下模組和介面:
-module(musicians). -behaviour(gen_server). -export([start_link/2, stop/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -record(state, {name="", role, skill=good}). -define(DELAY, 750). start_link(Role, Skill) -> gen_server:start_link({local, Role}, ?MODULE, [Role, Skill], []). stop(Role) -> gen_server:call(Role, stop).
我定義了一個 ?DELAY
巨集,我們將其用作音樂家每次顯示自己正在演奏的標準時間間隔。如記錄定義所示,我們還必須給他們每個人一個名字。
init([Role, Skill]) -> %% To know when the parent shuts down process_flag(trap_exit, true), %% sets a seed for random number generation for the life of the process %% uses the current time to do it. Unique value guaranteed by now() random:seed(now()), TimeToPlay = random:uniform(3000), Name = pick_name(), StrRole = atom_to_list(Role), io:format("Musician ~s, playing the ~s entered the room~n", [Name, StrRole]), {ok, #state{name=Name, role=StrRole, skill=Skill}, TimeToPlay}.
在 init/1
函數中進行兩件事。首先,我們開始捕捉退出。如果您回想一下 通用伺服器章節中 terminate/2
的描述,如果我們希望在伺服器的父程序關閉其子程序時呼叫 terminate/2
,則需要執行此操作。init/1
函數的其餘部分是設定一個隨機種子(以便每個程序獲得不同的隨機數),然後為自己建立一個隨機名稱。建立名稱的函數是:
%% Yes, the names are based off the magic school bus characters' %% 10 names! pick_name() -> %% the seed must be set for the random functions. Use within the %% process that started with init/1 lists:nth(random:uniform(10), firstnames()) ++ " " ++ lists:nth(random:uniform(10), lastnames()). firstnames() -> ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha", "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"]. lastnames() -> ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin", "Terese", "Tennelli", "Jamal", "Li", "Perlstein"].
好的!我們可以繼續進行實作。對於 handle_call
和 handle_cast
來說,這將非常簡單。
handle_call(stop, _From, S=#state{}) -> {stop, normal, ok, S}; handle_call(_Message, _From, S) -> {noreply, S, ?DELAY}. handle_cast(_Message, S) -> {noreply, S, ?DELAY}.
我們唯一的呼叫是停止音樂家伺服器,我們同意很快就這樣做。如果我們收到意外訊息,我們不會回覆它,並且呼叫者將會崩潰。這不是我們的問題。我們將逾時設定在 {noreply, S, ?DELAY}
元組中,原因很簡單,我們馬上就會看到。
handle_info(timeout, S = #state{name=N, skill=good}) -> io:format("~s produced sound!~n",[N]), {noreply, S, ?DELAY}; handle_info(timeout, S = #state{name=N, skill=bad}) -> case random:uniform(5) of 1 -> io:format("~s played a false note. Uh oh~n",[N]), {stop, bad_note, S}; _ -> io:format("~s produced sound!~n",[N]), {noreply, S, ?DELAY} end; handle_info(_Message, S) -> {noreply, S, ?DELAY}.
每次伺服器逾時,我們的音樂家都會彈奏一個音符。如果他們很棒,一切都會完全沒問題。如果他們很爛,他們有五分之一的機會會彈錯並彈出一個糟糕的音符,這會使他們崩潰。再次,我們在每次非終止呼叫結束時設定 ?DELAY
逾時。
然後,我們新增一個空的 code_change/3
回呼,這是「gen_server」行為所要求的。
code_change(_OldVsn, State, _Extra) -> {ok, State}.
我們可以設定終止函數:
terminate(normal, S) -> io:format("~s left the room (~s)~n",[S#state.name, S#state.role]); terminate(bad_note, S) -> io:format("~s sucks! kicked that member out of the band! (~s)~n", [S#state.name, S#state.role]); terminate(shutdown, S) -> io:format("The manager is mad and fired the whole band! " "~s just got back to playing in the subway~n", [S#state.name]); terminate(_Reason, S) -> io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]).

我們這裡有很多不同的訊息。如果我們以 normal
原因終止,則表示我們呼叫了 stop/1
函數,因此我們顯示音樂家是自願離開的。在 bad_note
訊息的情況下,音樂家將會崩潰,我們會說這是因為經理(我們稍後將加入的監督者)把他踢出局了。
然後,我們會有來自監督者的 shutdown
訊息。無論何時發生這種情況,都表示監督者決定終止其所有子程序,或者在我們的案例中,解雇所有音樂家。然後,我們為其餘訊息新增一個通用錯誤訊息。
以下是音樂家的簡單用例:
1> c(musicians). {ok,musicians} 2> musicians:start_link(bass, bad). Musician Ralphie Franklin, playing the bass entered the room {ok,<0.615.0>} Ralphie Franklin produced sound! Ralphie Franklin produced sound! Ralphie Franklin played a false note. Uh oh Ralphie Franklin sucks! kicked that member out of the band! (bass) 3> =ERROR REPORT==== 6-Mar-2011::03:22:14 === ** Generic server bass terminating ** Last message in was timeout ** When Server state == {state,"Ralphie Franklin","bass",bad} ** Reason for termination == ** bad_note ** exception error: bad_note
所以我們看到 Ralphie 在彈奏並在一個糟糕的音符後崩潰了。太棒了。如果您對「優秀」的音樂家嘗試相同的操作,您將需要呼叫我們的 musicians:stop(Instrument)
函數來停止所有演奏。
樂團監督者 (Band Supervisor)
我們現在可以使用監督者。我們將有三個等級的監督者:寬容的、憤怒的和混帳的。它們之間的區別在於,寬容的監督者雖然仍然是一個脾氣很差的人,但一次只會解僱一名樂團成員 (one_for_one
),也就是說解僱失敗的那個,直到他不耐煩了,就解僱所有人並放棄樂團。另一方面,憤怒的監督者會在每次錯誤時解僱其中的一些人 (rest_for_one
),並且會等待更短的時間,然後解僱所有人並放棄。然後,混帳監督者會在每次有人犯錯時解僱整個樂團,並且即使樂團失敗的次數減少,也會放棄。
-module(band_supervisor). -behaviour(supervisor). -export([start_link/1]). -export([init/1]). start_link(Type) -> supervisor:start_link({local,?MODULE}, ?MODULE, Type). %% The band supervisor will allow its band members to make a few %% mistakes before shutting down all operations, based on what %% mood he's in. A lenient supervisor will tolerate more mistakes %% than an angry supervisor, who'll tolerate more than a %% complete jerk supervisor init(lenient) -> init({one_for_one, 3, 60}); init(angry) -> init({rest_for_one, 2, 60}); init(jerk) -> init({one_for_all, 1, 60});
init 定義並非到此結束,但這讓我們可以為我們想要的每種類型的監督者設定基調。寬容的監督者只會重新啟動一名音樂家,並且會在 60 秒內第四次失敗時失敗。第二個只接受 2 次失敗,而混帳監督者將有非常嚴格的標準!
現在讓我們完成該函數,並實際實作樂團的啟動函數等等:
init({RestartStrategy, MaxRestart, MaxTime}) -> {ok, {{RestartStrategy, MaxRestart, MaxTime}, [{singer, {musicians, start_link, [singer, good]}, permanent, 1000, worker, [musicians]}, {bass, {musicians, start_link, [bass, good]}, temporary, 1000, worker, [musicians]}, {drum, {musicians, start_link, [drum, bad]}, transient, 1000, worker, [musicians]}, {keytar, {musicians, start_link, [keytar, good]}, transient, 1000, worker, [musicians]} ]}}.
所以我們可以看到,我們將有 3 位優秀的音樂家:歌手、貝斯手和鍵盤吉他手。鼓手很糟糕(這讓您非常生氣)。音樂家有不同的 Restart(永久、瞬時或暫時),因此,即使目前的歌手自願離開,樂團也永遠無法在沒有歌手的情況下運作,但仍然可以在沒有貝斯手的情況下正常演奏,因為老實說,誰在乎貝斯手?
這給了我們一個功能性的 band_supervisor 模組,我們現在可以嘗試一下:
3> c(band_supervisor). {ok,band_supervisor} 4> band_supervisor:start_link(lenient). Musician Carlos Terese, playing the singer entered the room Musician Janet Terese, playing the bass entered the room Musician Keesha Ramon, playing the drum entered the room Musician Janet Ramon, playing the keytar entered the room {ok,<0.623.0>} Carlos Terese produced sound! Janet Terese produced sound! Keesha Ramon produced sound! Janet Ramon produced sound! Carlos Terese produced sound! Keesha Ramon played a false note. Uh oh Keesha Ramon sucks! kicked that member out of the band! (drum) ... <snip> ... Musician Arnold Tennelli, playing the drum entered the room Arnold Tennelli produced sound! Carlos Terese produced sound! Janet Terese produced sound! Janet Ramon produced sound! Arnold Tennelli played a false note. Uh oh Arnold Tennelli sucks! kicked that member out of the band! (drum) ... <snip> ... Musician Carlos Frizzle, playing the drum entered the room ... <snip for a few more firings> ... Janet Jamal played a false note. Uh oh Janet Jamal sucks! kicked that member out of the band! (drum) The manager is mad and fired the whole band! Janet Ramon just got back to playing in the subway The manager is mad and fired the whole band! Janet Terese just got back to playing in the subway The manager is mad and fired the whole band! Carlos Terese just got back to playing in the subway ** exception error: shutdown
太神奇了!我們可以看見只有鼓手被解僱,過了一段時間,其他所有人也都被解僱了。然後他們就前往地鐵(英國讀者是搭乘地鐵)了!
您可以嘗試其他類型的監督者,結果也會相同。唯一的區別將是重新啟動策略。
5> band_supervisor:start_link(angry). Musician Dorothy Frizzle, playing the singer entered the room Musician Arnold Li, playing the bass entered the room Musician Ralphie Perlstein, playing the drum entered the room Musician Carlos Perlstein, playing the keytar entered the room ... <snip> ... Ralphie Perlstein sucks! kicked that member out of the band! (drum) ... The manager is mad and fired the whole band! Carlos Perlstein just got back to playing in the subway
對於憤怒的監督者,當鼓手犯錯時,鼓手和鍵盤吉他手都會被解僱。這與混帳的行為相比根本不算什麼。
6> band_supervisor:start_link(jerk). Musician Dorothy Franklin, playing the singer entered the room Musician Wanda Tennelli, playing the bass entered the room Musician Tim Perlstein, playing the drum entered the room Musician Dorothy Frizzle, playing the keytar entered the room ... <snip> ... Tim Perlstein played a false note. Uh oh Tim Perlstein sucks! kicked that member out of the band! (drum) The manager is mad and fired the whole band! Dorothy Franklin just got back to playing in the subway The manager is mad and fired the whole band! Wanda Tennelli just got back to playing in the subway The manager is mad and fired the whole band! Dorothy Frizzle just got back to playing in the subway
對於非動態的重新啟動策略來說,這就是大部分內容。
動態監督 (Dynamic Supervision)
到目前為止,我們所看到的監督類型是靜態的。我們在原始程式碼中指定了我們將擁有的所有子程序,然後讓一切正常執行。這是在實際應用程式中設定大多數監督者的方式;它們通常用於監管架構元件。另一方面,您有針對不確定工作者採取行動的監督者。它們通常是根據需求存在的。想像一下,一個 Web 伺服器會針對其收到的每個連線產生一個程序。在這種情況下,您會希望使用動態監督者來監管您將擁有的所有不同程序。
每次使用 one_for_one
、rest_for_one
或 one_for_all
策略將工作者新增至監督者時,子程序規範會與 pid 和其他一些資訊一起新增至監督者中的列表。然後可以使用子程序規範來重新啟動子程序等等。由於事情是這樣運作的,因此存在以下介面:
- start_child(SupervisorNameOrPid, ChildSpec)
- 這會在列表中新增一個子規範,並以此啟動子程序。
- terminate_child(SupervisorNameOrPid, ChildId)
- 終止或強行終止子程序。子規範會留在監督者中。
- restart_child(SupervisorNameOrPid, ChildId)
- 使用子規範來重新啟動程序。
- delete_child(SupervisorNameOrPid, ChildId)
- 移除指定子程序的 ChildSpec。
- check_childspecs([ChildSpec])
- 確保子規範有效。您可以在使用 'start_child/2' 之前使用此方法進行嘗試。
- count_children(SupervisorNameOrPid)
- 計算監督者下的所有子程序,並提供一個簡略的比較列表,包含哪些是活動的、有多少規範、有多少是監督者,以及有多少是工作者。
- which_children(SupervisorNameOrPid)
- 提供監督者下所有子程序的列表。
讓我們看看這如何與音樂家一起運作,並移除輸出(您需要動作快一點才能超越故障的鼓手!)
1> band_supervisor:start_link(lenient). {ok,0.709.0>} 2> supervisor:which_children(band_supervisor). [{keytar,<0.713.0>,worker,[musicians]}, {drum,<0.715.0>,worker,[musicians]}, {bass,<0.711.0>,worker,[musicians]}, {singer,<0.710.0>,worker,[musicians]}] 3> supervisor:terminate_child(band_supervisor, drum). ok 4> supervisor:terminate_child(band_supervisor, singer). ok 5> supervisor:restart_child(band_supervisor, singer). {ok,<0.730.0>} 6> supervisor:count_children(band_supervisor). [{specs,4},{active,3},{supervisors,0},{workers,4}] 7> supervisor:delete_child(band_supervisor, drum). ok 8> supervisor:restart_child(band_supervisor, drum). {error,not_found} 9> supervisor:count_children(band_supervisor). [{specs,3},{active,3},{supervisors,0},{workers,3}]
您可以觀察如何動態管理子程序。這對於任何需要管理(我想啟動這個、終止它等等)且數量不多的動態事物都非常有效。由於內部表示是一個列表,當您需要快速存取許多子程序時,這將不太好用。

在這種情況下,您需要的是 simple_one_for_one
。simple_one_for_one
的問題在於它不允許您手動重新啟動子程序、刪除它或終止它。這種彈性的損失幸運地伴隨著一些優點。所有子程序都保存在字典中,這使得查找速度很快。監督者下的所有子程序也只有一個子規範。這將節省您的記憶體和時間,因為您永遠不需要自己刪除子程序或儲存任何子規範。
在大多數情況下,編寫 simple_one_for_one
監督者與編寫任何其他類型的監督者類似,但有一件事例外。{M,F,A}
元組中的引數列表不是全部,而是會附加到您在使用 supervisor:start_child(Sup, Args)
呼叫它時所傳遞的引數。沒錯,supervisor:start_child/2
變更了 API。因此,不再是執行 supervisor:start_child(Sup, Spec)
,這會呼叫 erlang:apply(M,F,A)
,現在是執行 supervisor:start_child(Sup, Args)
,這會呼叫 erlang:apply(M,F,A++Args)
。
以下是我們如何為我們的 band_supervisor 編寫它。只需將以下子句加入其中的某處即可
init(jamband) -> {ok, {{simple_one_for_one, 3, 60}, [{jam_musician, {musicians, start_link, []}, temporary, 1000, worker, [musicians]} ]}};
我在這種情況下將它們全部設為暫時性的,並且監督者非常寬容。
1> supervisor:start_child(band_supervisor, [djembe, good]). Musician Janet Tennelli, playing the djembe entered the room {ok,<0.690.0>} 2> supervisor:start_child(band_supervisor, [djembe, good]). {error,{already_started,<0.690.0>}}
糟糕!這會發生是因為我們在啟動呼叫我們的 gen_server
時,將非洲鼓手註冊為 djembe
。如果我們不為它們命名或為每個使用不同的名稱,就不會造成問題。實際上,這裡有一個名稱為 drum
的替代方案
3> supervisor:start_child(band_supervisor, [drum, good]). Musician Arnold Ramon, playing the drum entered the room {ok,<0.696.0>} 3> supervisor:start_child(band_supervisor, [guitar, good]). Musician Wanda Perlstein, playing the guitar entered the room {ok,<0.698.0>} 4> supervisor:terminate_child(band_supervisor, djembe). {error,simple_one_for_one}
對。就像我說的,無法以這種方式控制子程序。
5> musicians:stop(drum). Arnold Ramon left the room (drum) ok
這樣運作得更好。
作為一個通用(有時會錯誤的)提示,我會告訴您僅在確定您要監管的子程序很少,並且/或者不需要以任何速度或頻繁地操作它們時,才動態使用標準監督者。對於其他類型的動態監督,請盡可能使用 simple_one_for_one
。
更新 (update)
自 R14B03 版本以來,可以使用函式 supervisor:terminate_child(SupRef, Pid)
終止子程序。現在可以使簡單的一對一監督機制完全動態化,並且對於運行單一程序類型的多個程序來說,已成為一個全面的有趣選擇。
關於監督策略和子規範就差不多了。現在您可能對「我到底要如何從中取得一個可用的應用程式?」感到懷疑,如果是這樣,您會很高興進入下一章,它實際上會建構一個具有簡短監督樹的簡單應用程式,以了解如何在現實世界中完成它。