5-監督者和應用程序
======================
到目前為止,我們的程序已經實現了注冊表(registry)來對成百上千的bucket進程進行監視。
你是不是覺得這個還不錯?沒有軟件是bug-free的,掛掉那是必定會發生滴。
當有東西掛了,我們的第一反應是:“快拯救這些錯誤”。但是,像在《入門》中學到的那樣,
不同于其它多數語言,Elixir不太做“防御性編程”。
相反,我們說“要掛快點掛”,或是“就讓它掛”。
如果有bug要讓我們的注冊表進程掛掉,啥也別怕,因為我們即將實現用監督者來啟動新的注冊表進程副本。
本章我們將學習監督者(supervisor),還會講到些有關應用程序的知識。
一個不夠,我們要創建兩個監督者,用它們監督我們的進程。
## 5.1-第一個監督者
創建一個監督者跟創建通用服務器差不多。我們將定義一個名為```KV.Supervisor```的模塊,
使用[Supervisor](http://elixir-lang.org/docs/stable/elixir/Supervisor.html)行為。
代碼文件```lib/kv/supervisor.ex```內容如下:
```elixir
defmodule KV.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok)
end
def init(:ok) do
children = [
worker(KV.Registry, [KV.Registry])
]
supervise(children, strategy: :one_for_one)
end
end
```
我們的監督者目前只有一個孩子:注冊表進程。一個形式如
```elixir
worker(KV.Registry, [KV.Registry])
```
的worker,在調用:
```elixir
KV.Registry.start_link(KV.Registry)
```
時將啟動一個進程。
我們傳給```start_link```的參數是進程的名稱。給監督機制下得進程命名是常見的做法,
這樣別的進程就可以通過名稱訪問它們,而不需要知道它們的進程ID。
這很有用,因為當被監督的某進程掛掉被重啟后,它的進程ID可能會改變。但是用名稱就不一樣了。
我們可以保證一個掛掉新啟的進程,還會用同樣的名稱注冊進來。而不用顯式地先獲取之前的進程ID。
另外,通常會用定義的模塊名稱作為進程的名字,在將來對系統進行debug時非常直觀。
最后,我們調用了```supervisor/2```,給它傳遞了一個孩子列表以及策略:```:one_for_one```。
監督者的策略指明了當一個孩子進程掛了會發生什么。```:one_for_one```意思是如果一個孩子進程掛了,
只有一個“復制品”會啟動來替代它。我們現在要的就是這個策略,
因為我們只有一個孩子。```Supervisor```支持許多不同的策略,我們在本章中將會陸續討論。
因為```KV.Registry.start_link/1```現在期待一個參數,需要修改我們的實現來接受這一個參數。
打開文件```lib/kv/registry.ex```,覆蓋原來的```start_link/0```定義:
```elixir
@doc """
Starts the registry with the given `name`.
"""
def start_link(name) do
GenServer.start_link(__MODULE__, :ok, name: name)
end
```
我們還要修改測試,在注冊表進程啟動時給個名字。在文件```test/kv/registry_test.exs```中覆蓋
原```setup```函數代碼:
```elixir
setup context do
{:ok, registry} = KV.Registry.start_link(context.test)
{:ok, registry: registry}
end
```
類似```test/3```,函數```setup/2```也接受測試上下文(context)。不管我們給setup代碼中添加了啥,
上下文中包含著幾個關鍵變量:比如```:case```,```:test```,```:file```和```:line```。
上面代碼中,我們用了```context.test```作為捷徑取得當前運行著的測試名稱,生成一個注冊表進程。
現在,隨著測試通過,可以拉我們的監督者出去溜溜了。如果在工程中啟動命令行對話```iex -S mix```,
我們可以手動啟動監督者:
```elixir
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>}
```
當我們啟動監督者,注冊表worker會自動啟動,允許我們創建bucket而不需要手動啟動它們。
但是,在實戰中我們很少手動啟動應用程序的監督者。啟動監督者是應用程序回調過程的一部分。
## 5.2-理解應用程序
起始我們已經把所有時間都花在這個應用程序上了。每次修改了一個文件,執行```mix compile```,
我們都能看到```Generated kv app```消息在編譯信息中打印出來。
我們可以在```_build/dev/lib/kv/ebin/kv.app```找到```.app```文件。來看一下它的內容:
```elixir
{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```文件(里面是應用程序的定義),我們就可以將應用程序視作一個整體形式來啟動和停止。
到目前為止我們還沒有考慮過這個問題,這是因為:
1. Mix為我們自動啟動了應用程序
2. 即使Mix沒有自動啟動我們的程序,該程序啟動后也沒做啥特別的事兒
總之,讓我們看看Mix如何為我們啟動應用程序。先在工程下啟動命令行,然后試著執行:
```elixir
iex> Application.start(:kv)
{:error, {:already_started, :kv}}
```
擦,已經啟動了?Mix通常會啟動文件```mix.exs```中定義的整個應用程序結構。
遇到依賴的程序也會如此一并啟動。
我們可以給mix一個選項,讓它不要啟動我們的應用程序。
執行命令:```iex -S mix run --no-start```啟動命令行,然后執行:
```elixir
iex> Application.start(:kv)
:ok
```
我們可以停止```:kv```程序和```:logger```程序,后者是Elixir默認情況下自動啟動的:
```elixir
iex> Application.stop(:kv)
:ok
iex> Application.stop(:logger)
:ok
```
然后再次啟動我們的程序:
```elixir
iex> Application.start(:kv)
{:error, {:not_started, :logger}}
```
錯誤是由于```:kv```所依賴的應用程序(這里是```:logger```)沒有啟動導致的。
Mix一般會根據工程中的```mix.exs```啟動整個應用程序結構;
對其依賴的每個應用程序來說也是這樣(如果它們還依賴于其它應用程序)。
但是這次我們用了```--no-start```標志,因此我們需要手動 _按順序_ 啟動所有應用程序,
或者像這樣調用```Application.ensure_all_started```:
```elixir
iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}
```
沒什么激動人心的,這些只是演示了如何控制我們的應用程序。
>當你運行```iex -S mix```,它相當于執行```iex -S mix run```。
因此無論何時你啟動iex會話,傳遞參數給```mix run```,實際上是傳遞給```run```命令。
你可以在命令行中執行```mix help run```獲取關于```run```的更多信息。
### 5.2.2-應用程序的回調(callback)
因為我們幾乎都在講應用程序如何啟動和停止,你能猜到肯定有辦法能在啟動的當兒做點有意義的事情。
沒錯,有的!
我們可以定義應用程序的回調函數。在應用程序啟動時,該函數將被調用。
這個函數必須返回```{:ok, pid}```,其中```pid```是其內部監督者進程的標識符。
我們分兩步來定義這個回調函數。首先,打開```mix.exs```文件,修改```def application```部分:
```elixir
def application do
[applications: [:logger],
mod: {KV, []}]
end
```
選項```:mod```指出了“應用程序回調函數的模塊”,后面跟著該傳遞給它的參數。
這個回調函數的模塊可以是任意模塊,只要它實現了[Application](http://elixir-lang.org/docs/stable/elixir/Application.html)行為。
在這里,我們要讓```KV```作為它回調函數的模塊。因此在文件```lib/kv.ex```中做一些修改:
```elixir
defmodule KV do
use Application
def start(_type, _args) do
KV.Supervisor.start_link
end
end
```
當我們聲明```use Application```,(類似聲明了```GenServer```、```Supervisor```)
我們需要定義幾個函數。這里我們只需定義```start/2```函數。
如果我們想在應用程序停止時定義一個自定義的行為,我們也可以定義一個```stop/1```函數。
現在我們再次用```iex -S mix```啟動我們的工程對話。
我們將看到一個名為```KV.Registry```的進程已經在運行:
```elixir
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。應用程序是一個實體,它作為一個整體啟動或者停止。
你可以在[應用程序模塊文檔](http://elixir-lang.org/docs/stable/elixir/Application.html)
閱讀更多關于應用程序的知識。或者執行```mix help compile.app```
來學習```def application```中支持的更多選項。
## 5.3 簡單的一對一監督者
我們已經成功定義了我們的監督者,它作為我們應用程序生命周期的一部分自動啟動(和停止)。
回顧一下,我們的```KV.Registry```在```handle_cast/2```回調中,鏈接并且監視bucket進程:
```elixir
{:ok, pid} = KV.Bucket.start_link()
ref = Process.monitor(pid)
```
鏈接是雙向的,意味著一個bucket進程掛了會導致注冊表進程掛掉。
盡管現在我們有了監督者,它能保證一旦注冊表進程掛了還可以重啟。
但是注冊表掛掉仍然意味著我們會丟失用來匹配bucket名稱到其相應進程的數據。
換句話說,我們希望即使bucket進程掛了,注冊表進程也能夠保持運行。寫成測試就是:
```elixir
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
Process.exit(bucket, :shutdown)
# Wait until the bucket is dead
ref = Process.monitor(bucket)
assert_receive {:DOWN, ^ref, _, _, _}
assert KV.Registry.lookup(registry, "shopping") == :error
end
```
這個測試很像之前的“退出時移除bucket”,
只是我們的做法更加殘暴(用```:shutdown```代替了```:normal```)。
不像```Agent.stop/1```,```Process.exit/2```是一個異步的操作。
因此我們不能簡單地在剛發了退出信號之后就執行查詢```KV.Registry.lookup/2```,
那個時候也許bucket進程還沒有結束(也就不會造成系統問題)。
為了解決這個問題,我們仍然要在測試期間監視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進程即使在有人停止了```:kv```程序也一直活著。
不光如此,它的進程會變得不可觸及。而一個不可觸及的進程是難以在運行時內省的。
我們將定義一個新的監督者來解決這個問題。這個新監督者會派生和監督所有的bucket。
有一個簡單的一對一監督策略,叫做```:simple_one_for_one```,對于此情況是非常適用的:
他允許指定一個工人模板,而后監督基于那個模板創建的多個孩子。
在這個策略下,工人進程不會在監督者初始化時啟動。而是每次調用了```start_child/2```函數后,
才會創建一個新的工人進程。
讓我們在文件```lib/kv/bucket/supervisor.ex```中定義```KV.Bucket.Supervisor```:
```elixir
defmodule KV.Bucket.Supervisor do
use Supervisor
# A simple module attribute that stores the supervisor name
@name KV.Bucket.Supervisor
def start_link() do
Supervisor.start_link(__MODULE__, :ok, name: @name)
end
def start_bucket do
Supervisor.start_child(@name, [])
end
def init(:ok) do
children = [
worker(KV.Bucket, [], restart: :temporary)
]
supervise(children, strategy: :simple_one_for_one)
end
end
```
比起我們第一個監督者,這個監督者有三點改變。
相較于之前接受所注冊進程的名字作為參數,我們這里只簡單地將其命名為```KV.Bucket.Supervisor```
(代碼中用```__MODULE__```),因為我們不需要派生這個進程的多個版本。
我們還定義了函數```start_bucket/0```來啟動每個bucket,
作為這個名為```KV.Bucket.Supervisor```的監督者的孩子。
函數```start_bucket/0```代替了注冊表進程中直接調用的```KV.Bucket.start_link```。
最后,在```init/1```回調中,我們將工人進程標記為```:temporary```。
意思是如果bucket進程即使掛了也不回重啟。因為我們創建這個監督者,只是用來作為將bucket進程圈成組這么一種機制。
bucket進程的創建還應該通過注冊表進程。
執行```iex -S mix```來試用下這個新監督者:
```elixir
iex> {:ok, _} = KV.Bucket.Supervisor.start_link
{:ok, #PID<0.70.0>}
iex> {:ok, bucket} = KV.Bucket.Supervisor.start_bucket()
{:ok, #PID<0.72.0>}
iex> KV.Bucket.put(bucket, "eggs", 3)
:ok
iex> KV.Bucket.get(bucket, "eggs")
3
```
修改注冊表進程中啟動bucket的部分,來與bucket的監督者協同工作:
```elixir
def handle_cast({:create, name}, {names, refs}) do
if Map.has_key?(names, name) do
{:noreply, {names, refs}}
else
{:ok, pid} = KV.Bucket.Supervisor.start_bucket()
ref = Process.monitor(pid)
refs = Map.put(refs, ref, name)
names = Map.put(names, name, pid)
{:noreply, {names, refs}}
end
end
```
在做了這些修改之后,我們的測試還是會fail。因為bucket的監督者還沒有啟動。
但是我們將不會在每次測試啟動時啟動bucket的監督者,而是讓其作為我們主監督者樹的一部分自動啟動。
## 5.4-監督樹
為了在應用程序中使用bucket的監督者,我們要把它作為一個孩子加到```KV.Supervisor```中去。
注意,我們已經開始用一個監督者去監督另一個監督者了---正式的稱呼是“監督樹”。
打開```lib/kv/supervisor.ex```,添加一個新的模塊屬性存儲bucket監督者的名字,
并且修改```init/1```:
```elixir
def init(:ok) do
children = [
worker(KV.Registry, [KV.Registry]),
supervisor(KV.Bucket.Supervisor, [])
]
supervise(children, strategy: :one_for_one)
end
```
這里我們添加了一個監督者作為孩子(沒有傳遞啟動參數)。重新運行測試,測試將可以通過。
記住,聲明各個孩子的順序是很重要的。因為注冊表進程依賴于bucket監督者,
所以bucket監督者需要在孩子列表中排得靠前一些。
因為我們已為監督者添加了多個孩子,現在就需要考慮使用```:one_for_one```這個策略還是否正確。
一個顯現的問題就是注冊表進程和bucket監督者之間的關系。
如果注冊表進程掛了,bucket監督者也必須掛。
因為一旦注冊表進程掛了,所有關聯bucket名字和其進程的信息也就丟失了。
此時若bucket的監督者還活著,它掌管的眾多bucket將根本訪問不到,變成垃圾。
我們可以考慮使用其他的策略,如```:one_for_all```或```:rest_for_one```。
策略```:one_for_all```在任何時候,只要有一個孩子掛,它就會停止并且重啟所有孩子進程。
這個貌似符合現在的需求,但是有些簡單粗暴。因為如果bucket監督者進程掛了,
是沒必要同時掛掉注冊表進程的。因為注冊表進程本身就監控這每個bucket進程的狀態,
它會自己清理不需要的信息(掛掉的bucket)。因此,策略```:rest_for_one```是比較合適的。
它會單獨重啟掛掉的孩子進程,而不影響其它的。因此我們做如下修改:
```elixir
def init(:ok) do
children = [
worker(KV.Registry, [KV.Registry]),
supervisor(KV.Bucket.Supervisor, [])
]
supervise(children, strategy: :rest_for_one)
end
```
如果注冊表進程掛了,那么它和bucket監督者都會被重啟;
而如果只是bucket監督者進程掛了,那么只有它自己被重啟。
還有其它幾個策略或選項可以傳遞給```worker/2```,```supervisor/2```和```supervise/2```函數,
所以可別忘記閱讀[監督者](http://elixir-lang.org/docs/stable/elixir/Supervisor.html)
及[監督者.spec](http://elixir-lang.org/docs/stable/elixir/Supervisor.Spec.html)的文檔。
## 5.5 觀察者(Observer)
現在我們定義好了監督者樹,這是介紹觀察者工具(Observer tool)的最佳時機。
該工具和Erlang一同推出。使用```iex -S mix```啟動你的應用程序,輸入:
```elixir
iex> :observer.start
```
一個GUI窗口將彈出,里面包含了關于我們系統的各種信息:從總體統計信息到負載圖表,
還有運行中的所有進程和應用程序。
在“應用程序”Tab頁上,可以看到系統中運行的所有應用程序以及它們的監督者樹信息。
可以選擇```kv```查看它的詳細信息:

不但如此,如果你再命令行中創建新的bucket:
```elixir
iex> KV.Registry.create KV.Registry, "shopping"
:ok
```
你可以在觀察者工具中看到從監督者樹種派生出了新的進程。
觀察者工具就留給讀者自行探索。你可以雙擊進程查看其詳細信息,
還可以右擊發送停止信號(模擬進程失敗的完美方法)等等。
在每天辛苦工作快要結束的時候,一個像觀察者這樣的工具絕對是你還想著在監督者樹里創建幾條進程的主要原因之一。
即使創建的都是臨時的,你也可以看看整個工程里各個進程還是不是可觸及或是可內省的。
## 5.6 測試里共享的狀態
目前為止,我們是在每個測試中啟動一個注冊表進程,以確保它們是獨立的:
```elixir
setup context do
{:ok, registry} = KV.Registry.start_link(context.test)
{:ok, registry: registry}
end
```
因為我們已經將注冊表進程改成使用```KV.Bucket.Supervisor```了,而它是在全局注冊的,
因此現在我們的測試依賴于這個共享的、全局的監督者,即使每個測試仍使用自己的注冊表進程。
那么問題來了:我們是否應該這么做?
It depends。只要僅依賴于某一狀態的非共享部分,那么也還ok啦。比如,每次用一個名字注冊進程,
都是注冊在一個共享的注冊表中。盡管如此,只要確保每個名字用于不同的測試,
比如在創建時使用上下文參數```context.test```,就不會再測試間出現并行或者數據依賴的問題。
對我們的bucket監督者來說也是同樣的道理。盡管多個注冊表進程會在共享的bucket監督者上啟動bucket,
但這些bucket和注冊表進程之間是相互隔離的。我們唯一會遇到并發問題,是我們想要調用
函數```Supervisor.count_children(KV.Bucket.Supervisor)```的時候。
它統計所有注冊表進程下的所有bucket。當測試并行執行并調用它的時候,返回的結果可能不一樣。
因此,目前由于我們的測試依賴于共享的監督者中的非共享部分,我們不用擔心并發問題。
假如它成為問題了,我們可以給每個測試啟動一個監督者,并將其作為參數傳遞給注冊表進程的```start_link```函數。
至此,我們的應用程序已經被監督者監督著,而且也已測試通過。之后我們要想辦法提升一些性能。