檢視原始碼 監督動態子代程
我們現在已成功定義我們的監督程式,它會在我們的應用程式生命週期中自動啟動(和停止)。
不過,請記住,我們的 KV.Registry
在 handle_cast/2
回呼中透過 start_link
連結,並透過 monitor
監控 bucket
程序。
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
連結是雙向的,這表示 bucket 中的崩潰會導致註冊表崩潰。儘管我們現在有監督程式,它保證註冊表會備份並執行,但註冊表崩潰仍表示我們會遺失將 bucket 名稱與其各自程序關聯的所有資料。
換句話說,我們希望即使 bucket 崩潰,註冊表也能繼續執行。我們來撰寫一個新的註冊表測試
test "removes bucket on crash", %{registry: registry} do
KV.Registry.create(registry, "shopping")
{:ok, bucket} = KV.Registry.lookup(registry, "shopping")
# Stop the bucket with non-normal reason
Agent.stop(bucket, :shutdown)
assert KV.Registry.lookup(registry, "shopping") == :error
end
這個測試類似於「在結束時移除 bucket」,只不過我們更嚴苛,傳送 :shutdown
作為結束原因,而不是 :normal
。如果程序以 :normal
以外的原因終止,所有連結的程序都會收到 EXIT 訊號,導致連結的程序也會終止,除非它正在捕捉結束。
由於 bucket 已終止,註冊表也已停止,當我們嘗試 GenServer.call/3
它時,我們的測試會失敗
1) test removes bucket on crash (KV.RegistryTest)
test/kv/registry_test.exs:26
** (exit) exited in: GenServer.call(#PID<0.148.0>, {:lookup, "shopping"}, 5000)
** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
code: assert KV.Registry.lookup(registry, "shopping") == :error
stacktrace:
(elixir) lib/gen_server.ex:770: GenServer.call/3
test/kv/registry_test.exs:33: (test)
我們將透過定義一個新的監督程式來解決這個問題,它會產生並監督所有 bucket。與我們先前定義的監督程式相反,子代程並非事先已知,而是動態啟動的。對於這些情況,我們使用針對此類使用案例最佳化的監督程式,稱為 DynamicSupervisor
。DynamicSupervisor
在初始化期間並不需要子代程清單;相反地,每個子代程都是透過 DynamicSupervisor.start_child/2
手動啟動的。
bucket 監督程式
由於 DynamicSupervisor
在初始化期間未定義任何子項,因此 DynamicSupervisor
也允許我們略過使用一般 start_link
函數和 init
回呼定義一個完全獨立模組的工作。反之,我們可以直接在監督樹狀結構中定義 DynamicSupervisor
,方法是給予它一個名稱和一個策略。
開啟 lib/kv/supervisor.ex
,並新增動態監督程式為子項,如下所示
def init(:ok) do
children = [
{KV.Registry, name: KV.Registry},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}
]
Supervisor.init(children, strategy: :one_for_one)
end
請記住,處理程序的名稱可以是任何原子。到目前為止,我們已使用與定義其實作的模組相同的名稱來命名處理程序。例如,由 KV.Registry
定義的處理程序被給予 KV.Registry
的處理程序名稱。這只是一個慣例:如果稍後您的系統中出現錯誤訊息,指出「名為 KV.Registry 的處理程序因原因而崩潰」,我們便確切知道該在哪裡進行調查。
在這個情況下,沒有模組,因此我們選擇了名稱 KV.BucketSupervisor
。它可以是任何其他名稱。我們也選擇了 :one_for_one
策略,這目前是動態監督程式唯一可用的策略。
執行 iex -S mix
,以便我們可以試試我們的動態監督程式
iex> {:ok, bucket} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
{:ok, #PID<0.72.0>}
iex> KV.Bucket.put(bucket, "eggs", 3)
:ok
iex> KV.Bucket.get(bucket, "eggs")
3
DynamicSupervisor.start_child/2
預期監督程式的名稱和要啟動的子項的子項規格。
最後一個步驟是變更註冊表以使用動態監督程式
def handle_cast({:create, name}, {names, refs}) do
if Map.has_key?(names, name) do
{:noreply, {names, refs}}
else
{:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
ref = Process.monitor(pid)
refs = Map.put(refs, ref, name)
names = Map.put(names, name, pid)
{:noreply, {names, refs}}
end
end
這足以讓我們的測試通過,但我們的應用程式中存在資源外洩。當一個儲存區終止時,監督程式將在它的位置啟動一個新的儲存區。畢竟,這就是監督程式的角色!
然而,當監督程式重新啟動新的儲存區時,註冊表並不知道它。因此,我們將在監督程式中有一個空的儲存區,而沒有人可以存取!為了解決這個問題,我們希望表示儲存區實際上是暫時的。如果它們崩潰,無論原因為何,都不應該重新啟動它們。
我們可以透過在 KV.Bucket
中將 restart: :temporary
選項傳遞給 use Agent
來做到這一點
defmodule KV.Bucket do
use Agent, restart: :temporary
我們也來新增一個測試到 test/kv/bucket_test.exs
,以保證儲存區是暫時的
test "are temporary workers" do
assert Supervisor.child_spec(KV.Bucket, []).restart == :temporary
end
我們的測試使用 Supervisor.child_spec/2
函數從模組中擷取子規格,然後斷言其重新啟動值為 :temporary
。此時,你可能會疑惑,如果它從未重新啟動其子項,為何要使用監督者。事實上,監督者提供的功能不只重新啟動,它們還負責保證適當的啟動和關閉,特別是在監督樹中發生崩潰時。
監督樹
當我們將 KV.BucketSupervisor
加入為 KV.Supervisor
的子項時,我們開始擁有監督其他監督者的監督者,形成所謂的「監督樹」。
每次你將新的子項加入監督者時,評估監督者策略是否正確以及子程序的順序非常重要。在這個案例中,我們使用 :one_for_one
,且 KV.Registry
在 KV.BucketSupervisor
之前啟動。
一個立即出現的缺陷是順序問題。由於 KV.Registry
呼叫 KV.BucketSupervisor
,因此 KV.BucketSupervisor
必須在 KV.Registry
之前啟動。否則,可能會發生註冊表在它啟動之前嘗試存取儲存區監督者的情況。
第二個缺陷與監督者策略有關。如果 KV.Registry
發生錯誤,所有將 KV.Bucket
名稱連結至儲存區程序的資訊都會遺失。因此,KV.BucketSupervisor
和所有子項也必須終止,否則我們將會有孤立的程序。
根據這個觀察,我們應該考慮改用其他監督者策略。另外兩個候選者為 :one_for_all
和 :rest_for_one
。使用 :rest_for_one
策略的監督者會終止並重新啟動在崩潰子項之後啟動的子程序。在這個案例中,我們希望 KV.Registry
終止時 KV.BucketSupervisor
也終止。這需要將儲存區監督者放在註冊表之後,這會違反我們在上面兩段落中建立的順序限制。
因此,我們最後的選項是全力以赴,選擇 :one_for_all
策略:當任何一個子項發生錯誤時,監督者會終止並重新啟動所有子程序。這對我們的應用程式來說是一個完全合理的做法,因為沒有儲存區監督者,註冊表無法運作,而沒有註冊表,儲存區監督者應該終止。讓我們重新實作 KV.Supervisor
中的 init/1
來編碼這些屬性
def init(:ok) do
children = [
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{KV.Registry, name: KV.Registry}
]
Supervisor.init(children, strategy: :one_for_all)
end
在我們進入下一章節之前,還有兩個主題。
測試中的共用狀態
到目前為止,我們已經為每個測試啟動一個註冊表,以確保它們是孤立的
setup do
registry = start_supervised!(KV.Registry)
%{registry: registry}
end
由於我們已變更我們的登錄檔以使用 KV.BucketSupervisor
,我們的測試現在依賴此共用監督程式,即使每個測試都有自己的登錄檔。問題是:我們應該嗎?
這取決於情況。只要我們僅依賴此狀態的非共用分割區,依賴共用狀態是沒問題的。儘管多個登錄檔可能會在共用儲存區監督程式上啟動儲存區,但這些儲存區和登錄檔彼此隔離。如果我們使用 DynamicSupervisor.count_children(KV.BucketSupervisor)
等函式,則我們只會遇到並行問題,該函式會計算所有登錄檔中的所有儲存區,當測試並行執行時可能會產生不同的結果。
由於到目前為止我們僅依賴儲存區監督程式的非共用分割區,因此我們不必擔心我們的測試套件中的並行問題。如果它曾經變成問題,我們可以為每個測試啟動一個監督程式,並將它作為參數傳遞給登錄檔 start_link
函式。
觀察者
現在我們已經定義了我們的監督樹,這是介紹 Erlang 附帶的觀察者工具的絕佳機會。使用 iex -S mix
啟動您的應用程式並輸入
iex> :observer.start()
缺少相依性
在使用
iex -S mix
在具有iex
的專案內執行時,observer
將無法作為相依性使用。為此,您需要在之前呼叫下列函式iex> Mix.ensure_application!(:wx) iex> Mix.ensure_application!(:runtime_tools) iex> Mix.ensure_application!(:observer) iex> :observer.start()
如果上述任何呼叫失敗,以下是可能發生的事情:某些套件管理員預設安裝精簡的 Erlang,而沒有 GUI 支援的 WX 繫結。在某些套件管理員中,您可能可以使用更完整的套件取代無頭 Erlang(在 Debian/Ubuntu/Arch 上尋找名稱為
erlang
與erlang-nox
的套件)。在其他管理員中,您可能需要安裝單獨的erlang-wx
(或類似名稱)套件。有討論在未來版本中改善此體驗。
應該會彈出一個 GUI,其中包含有關我們系統的各種資訊,從一般統計資料到負載圖表,以及所有正在執行的程序和應用程式的清單。
在應用程式標籤中,您將看到目前在您的系統中執行的所有應用程式及其監督樹。您可以選擇 kv
應用程式以進一步探索它

不僅如此,當你在終端機上建立新的儲存區時,你應該會看到在 Observer 中顯示的監控樹狀結構中產生新的程序
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
我們會讓你進一步探索 Observer 提供的內容。請注意,你可以對監控樹狀結構中的任何程序按兩下以取得更多資訊,也可以對程序按右鍵以傳送「終止訊號」,這是模擬故障並查看你的監控程式是否如預期般反應的完美方式。
最後,像 Observer 這樣的工具是你始終想在監控樹狀結構中啟動程序的原因之一,即使它們是暫時的,以確保它們始終可存取且可內省。
現在我們的儲存區已適當地連結並受到監控,讓我們看看如何加快速度。