5-監督者和應用程式
5-監督者和應用程式
- 第一個監督者
- 理解應用程式
- 啟動應用程式
- 應用程式的回撥函式
- 工程還是應用程式?
- 簡單的一對一監督者
- 監督樹
到目前為止,我們的程式需要一個事件管理器和一個登錄檔程式。它還會有不是成百,就是上千的bucket程式。 你是不是覺得這個還不錯?沒有東西是完美的,也許馬上就要出現bug了。
當有東西掛了,我們的第一反應是:“快拯救這些錯誤”。但是,像在《入門》中學到的那樣,不同於其它多數語言,Elixir不太做“防禦性程式設計”。 相反,我們說“要掛快點掛”,或是“就讓它掛”。 如果有bug要讓我們的登錄檔程式掛掉,啥也不怕,因為我們即將用監督者啟動一個新的登錄檔程式。
本章,我們就要學習監督者,還會講到應用程式。一個不夠,我們要建立兩個監督者,用它們監督我們的程式。
5.1-第一個監督者
建立一個監督者跟建立GenServer比沒多少不同。
定義一個名為KV.Supervisor
的模組,這個模組使用Supervisor行為抽象。此檔案lib/kv/supervisor.ex
的內容:
defmodule KV.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok)
end
@manager_name KV.EventManager
@registry_name KV.Registry
def init(:ok) do
children = [
worker(GenEvent, [[name: @manager_name]]),
worker(KV.Registry, [@manager_name, [name: @registry_name]])
]
supervise(children, strategy: :one_for_one)
end
end
我們的監督者有兩個孩子:事件管理器和登錄檔程式。通常會給監督者旗下的程式起個名字,好讓別的程式用名字而不是pid訪問它們。
這很有用,因為一個被監督的程式可能會掛,一掛再重啟,pid就變了。
用@manager_name
和@registry_name
這兩個模組屬性給我們監督者的倆孩子宣告名字,然後在“工人(worker)”的定義中引用這兩個屬性值。儘管不是必須用模組屬性來定義名字,但很實用,因為這樣讀起來很醒目。
舉個例子,KV.Registry
工人接受兩個引數,第一個是事件管理器的名字,第二個是一個選項鍵值列表。
在這裡,我們設定一個名字選項[name: KV.Registry]
(使用之前定義的模組屬性:@registry_name
),確保我們可以在整個應用程式中通過名字KV.Registry
訪問登錄檔程式。用定義它們的模組名稱給監督者孩子起名的做法十分普遍,因為在除錯一個正則執行的系統時很有用。
監督者中孩子們宣告的順序也是有區別的。因為登錄檔依賴於事件管理器,我們必須先啟動前者。這也是在孩子列表中,GenEvent
工人的位置靠前的原因。
最後,我們呼叫了supervisor/2
,給它傳遞了一個孩子列表,以及策略::one_for_one
。
監督的策略指明瞭當一個孩子程式掛了會發生什麼。:one_for_one
意思是如果一個孩子程式掛了,只有一個它的“複製品”會啟動來替代它。
這個策略現在是說得通的。如果事件管理器掛了,沒理由連登錄檔程式也重啟。反之亦然。
但是如果監督者旗下的孩子越來越多時,這個策略就需要改變了。Supervisor
行為抽象支援許多不同的策略,我們在本章中將會討論其中三種。
如果我們在工程中啟動命令列對話iex -S mix
,我們可以手動啟動監督者:
iex> KV.Supervisor.start_link
{:ok, #PID<0.66.0>}
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.70.0>}
當我們啟動監督樹,事件管理器和登錄檔程式都自動被啟動,允許我們建立bucket。不再需要手動啟動它們。
儘管在實戰中,我們很少手動啟動應用程式的監督者。相反,它的啟動是應用程式回撥的一部分。
5.2-理解應用程式
我們已經在這個應用程式上花了很多時間。每次修改了一個檔案,執行mix compile
,我們都能看到Generated kv.app
訊息列印出來。
我們可以在_build/dev/lib/kv/ebin/kv.app
找到.app
檔案。我們來看一下它的內容:
{application,kv,
[{registered,[]},
{description,"kv"},
{applications,[kernel,stdlib,elixir,logger]},
{vsn,"0.0.1"},
{modules,['Elixir.KV','Elixir.KV.Bucket',
'Elixir.KV.Registry','Elixir.KV.Supervisor']}]}.
該檔案包含Erlang的語句(使用Erlang的語法寫的)。即使我們不熟悉Erlang,也能很容易猜到這個檔案儲存的是我們應用程式的定義。
它包括應用程式的版本,定義的所有模組,還有它依賴的應用程式列表,如Erlang的Kernel,elixir本身,logger(我們在mix.exs
裡新增的)。
要是每次我們新增一個新的模組就要手動修改這個檔案,是很討厭的。這也是為啥把它交給mix來自動維護的原因。
我們還可以通過修改mix.exs
工程檔案中的函式application/0
的返回值,來配置產生的.app
檔案。
我們會在接下來幾章講到這個。
5.2.1-啟動應用程式
定義了.app
檔案(裡面是應用程式的定義),我們就可以將應用程式視作一個整體形式來啟動和停止。
到目前為止我們還沒有考慮過這個問題,這是因為:
- Mix為我們自動啟動了應用程式
- 即使Mix不自動啟動我們的程式,要啟動該程式時也不需要做啥特別的事兒
總之,讓我們看看Mix如何為我們啟動應用程式,先啟動工程下的命令列,然後試著:
iex> Application.start(:kv)
{:error, {:already_started, :kv}}
擦,已經啟動了?
我們可以給mix一個選項,讓它不要啟動我們的應用程式。命令:iex -S mix run --no-start
:
iex> Application.start(:kv)
{:error, {:not_started, :logger}}
這次我們得到的錯誤是由於:kv
所依賴的應用程式(這裡是:logger
)沒有啟動。
Mix一般會根據工程中的mix.exs
啟動整個應用程式結構;對其依賴的每個應用程式來說也是這樣(如果它們還依賴於其它應用程式)。
但是這次我們用了--no-start
標誌,因此我們需要手動按順序啟動所有應用程式,或者像這樣呼叫Application.ensure_all_started
:
iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}
iex> Application.stop(:kv)
18:12:10.698 [info] Application kv exited :stopped
:ok
沒什麼激動人心的,但是這演示瞭如何控制我們的應用程式。
當你執行
iex -S mix
,它相當於iex -S mix run
。因此無論何時你啟動iex會話,傳遞引數給mix run
,實際上是傳遞給run
命令。你可以在命令列中執行mix help run
獲取關於run
的更多資訊。
5.2.2-應用程式的回撥函式
因為我們幾乎都在講應用程式如何啟動和停止,你能猜到肯定有某個方法能在啟動的當兒做點有意義的事情。沒錯,有的!
我們可以定義應用程式的回撥函式。在應用程式啟動時,該函式將被呼叫。
這個函式必須返回{:ok, pid}
,其中pid
是其內部監督者程式的識別符號。
我們分兩步來定義這個回撥函式。首相,開啟mix.exs
檔案,修改def application
部分為:
def application do
[applications: [],
mod: {KV, []}]
end
選項:mod
指出了“應用程式回撥函式的模組”,後面跟著個在應用程式啟動是會被傳遞過來的引數。
這個回撥函式的模組可以是任意的,只要它實現了Application行為。
現在,我們讓KV
來做這個回撥函式的模組。在檔案lib/kv.ex
中,做一下修改:
defmodule KV do
use Application
def start(_type, _args) do
KV.Supervisor.start_link
end
end
當我們use Application
,我們僅僅需要定義一個start/2
函式。
而如果我們想在應用程式停止時定義一個自定義的行為,我們也可以定義一個stop/1
函式。
在這裡,我們不這麼做,就用其預設的、自動定義在use Application
中的行為。
現在我們再次用iex -S mix
啟動我們的工程對話。我們將開到一個名為KV.Registry
的程式已經在執行:
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.88.0>}
好牛逼!
5.2.3-工程還是應用程式?
Mix是區分工程(projects)和應用程式(applications)的。
基於目前的mix.exs
,我們可以說,我們有一個Mix 工程,該工程定義了:kv
應用程式。
在後面章節我們會看到,有些工程一個應用程式也沒定義。
當我們講“工程”時,你應該想到Mix。Mix是管理工程的工具。 它知道如何去編譯、測試你的工程,等等。它還知道如何編譯和啟動你的工程的相關應用程式。
當我們講“應用程式”時,我們討論的是OTP。應用程式是一個實體,它作為一個整體啟動或者停止。 你可以在應用程式模組文件閱讀更多關於應用程式的知識。
5.3 簡單的一對一監督者
我們已經成功定義了我們的監督者,它作為我們應用程式生命週期的一部分自動啟動(和停止)。
回一下,我們的KV.Registry
在handle_cast/2
回撥中,連結並且監視bucket程式:
{:ok, pid} = KV.Bucket.start_link()
ref = Process.monitor(pid)
連結是雙向的,意味著一個bucket程式掛了會導致登錄檔程式掛掉。儘管現在我們有了監督者,它能保證一旦登錄檔程式掛了還可以重啟, 但是我們儲存在bucket相應程式的資料還是會丟失。
換句話說,我們希望即使bucket程式掛了,登錄檔程式也能夠保持執行。寫個測試:
test "removes bucket on crash", %{registry: registry} do
KV.Registry.create(registry, "shopping")
{:ok, bucket} = KV.Registry.lookup(registry, "shopping")
# Kill the bucket and wait for the notification
Process.exit(bucket, :shutdown)
assert_receive {:exit, "shopping", ^bucket}
assert KV.Registry.lookup(registry, "shopping") == :error
end
這個測試很像之前的“退出時移除bucket”,只是我們的做法更加有點問題。
我們沒有使用Agent.stop/1
,而是傳送了一個退出訊號來關閉bucket程式。因為bucket是連結在登錄檔程式的,而登錄檔程式是連線著測試程式,讓bucket掛回導致連測試程式都掛掉:
1) test removes bucket on crash (KV.RegistryTest)
test/kv/registry_test.exs:52
** (EXIT from #PID<0.94.0>) shutdown
一個可行的解決方法是提供KV.Bucket.start/0
,讓它執行Agent.start/1
。
在登錄檔程式中使用這個方法啟動bucket,從而去掉了它們之間的連結。
但是這不是個好辦法,因為這樣bucket程式就連結不到任何程式。這意味著所有bucket程式即使在不可訪問的狀態下也一直活著。
我們將定義一個新的監督者來解決這個問題。這個新監督者來派生和監督所有的bucket。
有一個簡單的一對一監督策略,叫做:simple_one_for_one
,對於此情況是非常適用的:
他允許指定一個工人模板,而後監督基於那個模板的多個孩子。
讓我們定義KV.Bucket.Supervisor
:
defmodule KV.Bucket.Supervisor do
use Supervisor
def start_link(opts \\ []) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
def start_bucket(supervisor) do
Supervisor.start_child(supervisor, [])
end
def init(:ok) do
children = [
worker(KV.Bucket, [], restart: :temporary)
]
supervise(children, strategy: :simple_one_for_one)
end
end
比起之前的,這個監督者有兩點改變。
第一,我們定義了start_bucket/1
函式,接受一個監督者id,並且啟動一個bucket程式作為該監督者的孩子。start_bucket/1
代替了之前在登錄檔程式中直接呼叫的KV.Bucket.start_link
。
第二,在init/1
回撥中,我們將工人標記為:temporary
。意思是如果bucket掛了,它不會重啟!
這是因為我們只是想用監督者將bucket組織起來。建立bucket還得通過登錄檔程式。
執行iex -S mix
,試一試我們的新監督者:
iex> {:ok, sup} = KV.Bucket.Supervisor.start_link
{:ok, #PID<0.70.0>}
iex> {:ok, bucket} = KV.Bucket.Supervisor.start_bucket(sup)
{:ok, #PID<0.72.0>}
iex> KV.Bucket.put(bucket, "eggs", 3)
:ok
iex> KV.Bucket.get(bucket, "eggs")
3
現在來對登錄檔程式稍作修改,以配合bucket的監督者。
我們會沿用和處理事件管理器時相同的策略,會顯式地傳遞bucket的監督者的pid給KV.Registery.start_link/3
。
讓我們先從修改test/kv/registry_test.exs
中的Setup回撥開始:
setup do
{:ok, sup} = KV.Bucket.Supervisor.start_link
{:ok, manager} = GenEvent.start_link
{:ok, registry} = KV.Registry.start_link(manager, sup)
GenEvent.add_mon_handler(manager, Forwarder, self())
{:ok, registry: registry}
end
接下來開始修改KV.Registry
中的函式,將新監督者投入使用:
## Client API
@doc """
Starts the registry.
"""
def start_link(event_manager, buckets, opts \\ []) do
# 1. Pass the buckets supevisor as argument
GenServer.start_link(__MODULE__, {event_manager, buckets}, opts)
end
## Server callbacks
def init({events, buckets}) do
names = HashDict.new
refs = HashDict.new
# 2. Store the buckets supevisor in the state
{:ok, %{names: names, refs: refs, events: events, buckets: buckets}}
end
def handle_cast({:create, name}, state) do
if HashDict.get(state.names, name) do
{:noreply, state}
else
# 3. Use the buckets supervisor instead of starting buckets directly
{:ok, pid} = KV.Bucket.Supervisor.start_bucket(state.buckets)
ref = Process.monitor(pid)
refs = HashDict.put(state.refs, ref, name)
names = HashDict.put(state.names, name, pid)
GenEvent.sync_notify(state.events, {:create, name, pid})
{:noreply, %{state | names: names, refs: refs}}
end
end
改這些基本上就能讓測試通過了。要完成任務,只需再修改下這個監督者,讓原來bucket的那個監督者也成為它的孩子。
5.4-監督樹
為了使用bucket的監督者,我們要把它作為一個孩子加到KV.Supervisor
中去。
注意,我們已經開始用一個監督者去監督另一個監督者了。正式的稱呼是“監督樹”。
開啟lib/kv/supervisor.ex
,新增一個新的模組屬性儲存bucket監督者的名字,並且修改init/1
:
@manager_name KV.EventManager
@registry_name KV.Registry
@bucket_sup_name KV.Bucket.Supervisor
def init(:ok) do
children = [
worker(GenEvent, [[name: @manager_name]]),
supervisor(KV.Bucket.Supervisor, [[name: @bucket_sup_name]]),
worker(KV.Registry, [@manager_name, @bucket_sup_name, [name: @registry_name]])
]
supervise(children, strategy: :one_for_one)
end
這一次,我們新增了一個監督者作為孩子,並且給了它一個名字KV.Bucket.Supervisor
(和它的模組名相同)。
我們還更新了KV.Registry
這個工人,使它接受bucket監督者的名字作為引數。
記住,宣告各個孩子的順序是很重要的。因為登錄檔程式依賴於bucket監督者,所以bucket監督者需要在孩子列表中排得靠前一些。
因為我們已為監督者新增了多個孩子,現在就需要考慮使用:one_for_one
這個策略還是否正確。
一個顯現的問題就是登錄檔程式和bucket監督者之間的關係。如果登錄檔程式掛了,bucket監督者也必須掛。
因為一旦登錄檔程式掛了,所有關聯bucket名字和其程式的資訊也就丟失了。此時若bucket的監督者還活著,它掌管的眾多bucket根本訪問不到,變成垃圾。
我們可以考慮使用其他的策略,如:one_for_all
。這個策略在任何時候,只要有一個孩子掛,它就會停止並且重啟所有孩子程式。
這也許不是最理想的,因為登錄檔程式掛了不應該影響到事件管理器。
事實上,這麼做也是有害的,因為要是事件管理器掛了,所有註冊的事件處理者也都跟著會被移除。
一個可行的方案是再新建一個監督者,讓它來監督登錄檔程式和bucket監督者,使用:one_for_all
策略。
讓我們的根監督者監督這個新建的,以及事件管理器這兩個孩子,使用:one_for_one
策略。
這個監督樹大概是一下這個樣子:
* root supervisor [one_for_one]
* event manager
* supervisor [one_for_all]
* buckets supervisor [simple_one_for_one]
* buckets
* registry
你可以試著構建這個監督樹,但我們不會再對它進行改進了。
因為下一章我們會修改登錄檔程式,讓它可以持久化(persist)它的註冊資訊,使得我們用的:one_for_one
策略成為最合適的策略,省得新增監督樹結構或者改變策略了。
記住,還有幾個策略可以傳遞給worker/2
,supervisor/2
和supervise/2
函式,所以可別忘記閱讀監督者模組的文件.
相關文章
- FindBug,Java程式碼的監督者 (轉)Java
- 有監督學習和無監督學習
- 一圖看懂監督學習、無監督學習和半監督學習
- 什麼是有監督學習和無監督學習
- 【ML吳恩達】3 有監督學習和無監督學習吳恩達
- 機器學習——監督學習&無監督學習機器學習
- 自監督、半監督和有監督全涵蓋,四篇論文遍歷對比學習的研究進展
- 監督式機器學習演算法的應用:擇時【附原始碼】機器學習演算法原始碼
- 監聽者模式實戰應用模式
- 監督學習
- 什麼是計劃程式代理監督程式模式?模式
- 專家:網路運營者應接受社會監督、受理公眾舉報
- 基於自編碼器的表徵學習:如何攻克半監督和無監督學習?
- 機器學習:監督學習機器學習
- 自監督學習
- 監督學習,無監督學習常用演算法集合總結,引用scikit-learn庫(監督篇)演算法
- 使用 Elastic Stack 來監控和調優 Golang 應用程式ASTGolang
- 自監督學習概述
- 因果推理和監督學習的統一概念框架:兩者並不是對立的框架
- 監督學習or無監督學習?這個問題必須搞清楚
- Hinton新作!越大的自監督模型,半監督學習需要的標籤越少模型
- 使用 Prometheus 監控 SAP ABAP 應用程式Prometheus
- GAN用於無監督表徵學習,效果依然驚人……
- 監督學習基礎概念
- 003.00 監督式學習
- 監督學習之迴歸
- 有監督學習——梯度下降梯度
- python 無監督生成模型Python模型
- 應用程式和Activity
- 因果推理和監督學習的統一概念框架框架
- 監督學習之支援向量機
- 非監督學習最強攻略
- 無監督學習之降維
- 【機器學習】李宏毅——自監督式學習機器學習
- 有監督學習——高斯過程
- ERP專案的監督(轉)
- 銀行和監管機構試用R3 Corda平臺的應用程式
- 吳恩達《Machine Learning》精煉筆記 1:監督學習與非監督學習吳恩達Mac筆記