#分佈式任務與配置node
本章,咱們將回到:kv
應用並添加一個路由層,它能讓咱們根據桶名來在節點之間分佈請求.算法
路由層將會以以下格式收到一個路徑表格:服務器
[{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}]
路由器將會檢查桶名的第一個字節是否在表格中,並依此將其派遣到合適的節點.例如,一個以字母"a"(?a
表明字母a的Unicode代碼點)開頭的桶將會被派遣到foo@computer-name
節點.cookie
若是匹配到的入口指向了能評估該請求的節點,那麼咱們就完成了尋路,而後這個節點將會執行所請求的操做.若是匹配到的入口指向了不一樣的節點,咱們將傳送請求到該節點,它會搜尋本身的路由表格(也許會和第一個節點中的不一樣)並作出相應的動做.若是沒有入口被匹配到,將會拋出一個錯誤.網絡
你可能會想知道爲何咱們不直接讓咱們在路由表格中找到的節點去執行所請求的操做,而是將路由請求傳遞到該節點來處理.由於像上面這種簡單的路由表格能夠合理的被全部節點分享,以這種方式傳遞路由請求可以很容易地在咱們的應用成長時將路徑表格分解成更小的塊.也許是同一個緣由,foo@computer-name
將只會對路由桶請求負責,它處理的桶將會被派遣到不一樣的節點.這樣,bar@computer-name
就不須要知道這些變化.app
注意:本章中咱們將在同一個機器上使用兩個節點.你能夠在同個網絡下使用兩個或更多不一樣的機器,可是你須要作一些準備工做.首先,你須要確認全部機器都有一個有着相同值得
~/.erlang.cookie
文件.其次,你須要保證epmd運行在一個未阻塞的端口(你能夠運行epmd -d
獲取調式信息).而後,若是你想學習更多關於分佈的內容,咱們推薦Learn You Some Erlang中的Distribunomicon這一章.async
#咱們第一個分佈式代碼分佈式
Elixir裝載了許多用來鏈接節點並交換信息的工具.事實上,咱們使用了和進程相同的概念,可以在分佈式環境中發送和接受信息,是由於Elixir進程是_位置透明_的.意思是當發送信息時,收件人是否在同一個節點不重要,VM在這兩種情形下都可以傳送信息.函數
爲了運行分佈式代碼,咱們須要啓動一個具名VM.名字可短(當在同一個網絡)可長(須要完整的電腦地址).讓咱們開啓一個新的IEx會話:工具
$ iex --sname foo
你會發現提示符有些不一樣,它顯示了節點名稱,後面跟着電腦名稱:
Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help) iex(foo@jv)1>
個人電腦名是jv
,因此我在上面的例子中看到的是foo@jv
,但你的會不一樣.咱們將在下面的例子中使用foo@computer-name
,當輸入這些代碼時你須要按狀況更新.
讓咱們在這個殼中定義一個名爲Hello
的模塊:
iex> defmodule Hello do ...> def world, do: IO.puts "hello world" ...> end
若是在同一個網絡中你有另外一臺安裝了Erlang和Elixir的電腦,你能夠在上面啓動另外一個殼.若是沒有,你能夠簡單地在另外一個終端中啓動另外一個IEx會話.一樣地,給它一個短名叫作bar
:
$ iex --sname bar
注意在這個新的IEx會話中,咱們不能訪問Hello.world/0
:
iex> Hello.world ** (UndefinedFunctionError) undefined function: Hello.world/0 Hello.world()
然而咱們能夠在bar@computer-name
上爲foo@computer-name
生成一個新的進程!讓咱們來試一試:
iex> Node.spawn_link :"foo@computer-name", fn -> Hello.world end #PID<9014.59.0> hello world
Elixir在另外一個節點生成了一個進程並返回了它的pid.而後代碼在Hello.world/0
存在的節點執行,並調用那個函數.注意其結果hello world
打印在當前節點bar
上,而不是foo
.換句話說,被打印的信息是從foo
發送到了bar
.這是由於在另外一個節點(foo
)生成的進程仍然有一個當前節點(bar
)的羣首領.咱們曾在IO章節中簡短地討論過羣首領.
咱們能夠像往常同樣使用由Node.spawn_link/2
返回的pid來收發信息.讓咱們來嘗試一個快速乒乓的例子:
iex> pid = Node.spawn_link :"foo@computer-name", fn -> ...> receive do ...> {:ping, client} -> send client, :pong ...> end ...> end #PID<9014.59.0> iex> send pid, {:ping, self} {:ping, #PID<0.73.0>} iex> flush :pong :ok
由此,咱們能夠得出結論,當咱們須要進行分佈式計算時,咱們應該使用Node.spawn_link/2
來在遠程節點生成進程.然而,在本教程中咱們已經學過,應當儘可能避免在監督樹以外生成進程,因此咱們須要尋找其它選項.
這裏有三個能在咱們的實現中使用的,Node.spawn_link/2
的替代品:
\1. 咱們可使用Erlang的:rpc模塊來在遠程節點執行函數.在bar@computer-name
殼中,你能夠調用:rpc.call(:"foo@computer-name", Hello, :world, [])
,而後它將打印"hello world"
\2. 咱們能夠有一個運行在其它節點上的服務器,並經過GenServer API向該節點發送請求.例如,你可使用GenServer.call({name, node}, arg)
來調用一個遠程具名服務器,或者簡單地將遠程進程的PID做爲第一個參數傳送.
\3. 咱們可使用在上一章中所學到的tasks,由於它們在本地和遠程節點均可以被生成.
上述選項有着不一樣的特性.:rpc
和使用GenServer都會在一個服務器上將你的請求序列化,而tasks能夠高效地同步運行在遠程節點,並由主管來生成序列點.
對於咱們的路由層,咱們將使用tasks,但你能夠自由地探索其它替代品.
#同步/等待(async/await)
目前咱們已經探索了獨立啓動和運行的tasks,不考慮它們的返回值.然而,運行一個task來計算一個值並讀取它的結果有時是頗有用的.因此,tasks也提供了async/await
模式:
task = Task.async(fn -> compute_something_expensive end) res = compute_something_else() res + Task.await(task)
async/await
提供了一個很是簡單的機制來同時計算值.不只如此,async/await
還可用於咱們在上一章中提到的Task.Supervisor
.咱們只須要用Task.Supervisor.async/2
替代Task.Supervisor.start_child/2
,並使用Task.await/2
在稍後讀取結果.
#分佈式任務(Distributed tasks)
分佈式任務和受監督任務幾乎徹底同樣.惟一的不一樣點是當咱們在主管上生成task時,咱們傳送的是節點名.打開:kv
應用中的lib/kv/supervisor.ex
.讓咱們添加一個task主管,做爲樹的最後一個孩子:
supervisor(Task.Supervisor, [[name: KV.RouterTasks]]),
如今,讓咱們再次啓動兩個具名節點,可是在:kv
應用中:
$ iex --sname foo -S mix $ iex --sname bar -S mix
在bar@computer-name
之中,咱們如今能夠利用主管直接生成一個其它節點內的task:
iex> task = Task.Supervisor.async {KV.RouterTasks, :"foo@computer-name"}, fn -> ...> {:ok, node()} ...> end %Task{pid: #PID<12467.88.0>, ref: #Reference<0.0.0.400>} iex> Task.await(task) {:ok, :"foo@computer-name"}
咱們的第一個分佈式task簡單地檢索了正在運行的節點名.注意,咱們給了Task.Supervisor.async/2
一個匿名函數,可是在分佈式的狀況下,更推薦明確地給定模塊,函數和參數:
iex> task = Task.Supervisor.async {KV.RouterTasks, :"foo@computer-name"}, Kernel, :node, [] %Task{pid: #PID<12467.88.0>, ref: #Reference<0.0.0.400>} iex> Task.await(task) :"foo@computer-name"
區別在於匿名函數要求目標節點的代碼版本要和調用者徹底同樣.使用模塊,函數和參數會更健壯,由於你只須要在給定的模塊中找到一個可以匹配參數的函數.
利用已學的知識,讓咱們來編寫路由代碼吧.
#路由層(Routing layer)
建立lib/kv/router.ex
文件:
defmodule KV.Router do @doc """ Dispatch the given `mod`, `fun`, `args` request to the appropriate node based on the `bucket`. """ def route(bucket, mod, fun, args) do # Get the first byte of the binary first = :binary.first(bucket) # Try to find an entry in the table or raise entry = Enum.find(table, fn {enum, _node} -> first in enum end) || no_entry_error(bucket) # If the entry node is the current node if elem(entry, 1) == node() do apply(mod, fun, args) else {KV.RouterTasks, elem(entry, 1)} |> Task.Supervisor.async(KV.Router, :route, [bucket, mod, fun, args]) |> Task.await() end end defp no_entry_error(bucket) do raise "could not find entry for #{inspect bucket} in table #{inspect table}" end @doc """ The routing table. """ def table do # Replace computer-name with your local machine name. [{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}] end end
讓咱們編寫一個測試來驗證路由器的工做.建立一個名爲test/kv/router_test.exs
的文件:
defmodule KV.RouterTest do use ExUnit.Case, async: true test "route requests across nodes" do assert KV.Router.route("hello", Kernel, :node, []) == :"foo@computer-name" assert KV.Router.route("world", Kernel, :node, []) == :"bar@computer-name" end test "raises on unknown entries" do assert_raise RuntimeError, ~r/could not find entry/, fn -> KV.Router.route(<<0>>, Kernel, :node, []) end end end
第一個測試簡單地調用了Kernel.node/0
,它會基於桶名"hello"和"world"來返回當前節點的名字.依據咱們的路由表格,咱們應當會分別獲得foo@computer-name
和bar@computer-name
做爲回覆.
第二個測試只是檢查對於未知入口的報錯.
爲了運行第一個測試,咱們須要運行兩個節點.進入apps/kv
,並重啓節點bar
.
$ iex --sname bar -S mix
以以下命令運行測試:
$ elixir --sname foo -S mix test
咱們將成功經過測試.優秀!
#測試過濾器與標籤
儘管咱們的測試經過了,咱們的測試結構卻變得更復雜了.特別地,使用mix test
運行測試將致使失敗,由於咱們的測試要求鏈接到另外一個節點.
幸運的是,ExUnit裝載了測試標籤的功能,讓咱們能運行特定的回調或者基於那些標籤來過濾測試.在以前的章節咱們已經使用了:captrue_log
標籤,它是由ExUnit本身定義的.
這一次讓咱們添加一個:distributed
標籤到test/kv/router_test.exs
:
@tag :distributed test "route requests across nodes" do
@tag :distributed
等同於@tag distributed: true
.
當測試被合適地標上標籤後,咱們能夠檢查網絡上的節點是否活着,若是沒有,咱們能夠排除全部分佈式測試.打開:kv
應用中的test/test_helper.exs
,並添加:
exclude = if Node.alive?, do: [], else: [distributed: true] ExUnit.start(exclude: exclude)
如今,用mix test
運行測試:
$ mix test Excluding tags: [distributed: true] ....... Finished in 0.1 seconds (0.1s on load, 0.01s on tests) 7 tests, 0 failures, 1 skipped
這一次全部的測試都經過了,並且ExUnit警告咱們分佈式測試被排除了.若是你使用$ elixir --sname foo -S mix test
運行測試,另外一個額外的測試就會執行併成功經過,只要bar@computer-name
節點可用.
mix test
命令也容許咱們動態地包含和排除標籤.例如,咱們能夠運行$ mix test --include distributed
來運行分佈式測試,無論test/test_helper.exs
中的設置是怎樣.咱們也能夠傳送--exclude
來排除特定的標籤.最後,--only
能夠被用於只運行特定標籤的測試:
$ elixir --sname foo -S mix test --only distributed
#應用環境與配置
如今咱們是直接在代碼中將路由表格寫入KV.Router
模塊.然而,咱們但願將表格變爲動態的.這使得咱們不僅僅要配置開發/測試/生產模式,還要讓不一樣的節點使用不一樣入口的路由表格.這就是OTP的特性之一:應用環境.
每一個應用都有一個環境,其中用鍵存儲了應用的特定配置.例如,咱們能夠將路由表格存儲在:kv
應用的環境中,給它一個默認值,並讓其它應用按需修改表格.
打開apps/kv/mix.exs
並修改application/0
函數:
def application do [applications: [], env: [routing_table: []], mod: {KV, []}] end
咱們添加了一個新的:env
鍵到應用中.它返回的是應用的默認環境,它有一個入口鍵:routing_table
和一個空列表做爲值.應用環境中裝載一個空表格是有意義的,由於特定的路由表格依賴於測試/部署的結構.
爲了在咱們的代碼中使用應用環境,咱們只須要修改KV.Router.table/0
的定義:
@doc """ The routing table. """ def table do Application.fetch_env!(:kv, :routing_table) end
咱們使用Application.fetch_env!/2
來從:kv
環境中的:routing_table
裏讀取入口.
因爲咱們的路由表格目前是空的,咱們的分佈式測試將會失敗.重啓應用並再次運行測試:
$ iex --sname bar -S mix $ elixir --sname foo -S mix test --only distributed
關於應用環境的一件有趣的事情是它不只是爲當前應用配置,而是爲全部應用.這些配置是由config/config.exs
文件完成的.例如,咱們能夠配置IEx的默認提示符.只須要打開apps/kv/config/config.exs
並添加以下內容到末尾:
config :iex, default_prompt: ">>>"
使用iex -S mix
來啓動IEx,你會發現IEx的提示符改變了.
這意味着咱們也能夠在apps/kv/config/config.exs
文件中直接配置咱們的:routing_table
:
# Replace computer-name with your local machine nodes. config :kv, :routing_table, [{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}]
重啓節點並再次運行分佈式測試.如今它們都經過了.
從Elixir v1.2開始,全部雨傘應用會共用它們的配置,多虧了雨傘根目錄中config/config.exs
文件中的這行代碼,載入了全部孩子的配置:
import_config "../apps/*/config/config.exs"
mix run
命令也接受一個--config
旗幟,它容許咱們按需提供配置文件.這能夠用於開啓不一樣的節點,每一個有着它本身的配置(例如,不一樣的路由表格).
內置的應用配置能力和雨傘應用的結構給了咱們不少的選項,在部署咱們的軟件時.咱們能:
- 部署一個雨傘應用到一個節點,它既是一個TCP服務器,又是一個鍵值存儲器
- 部署一個:kv_server
應用,只要路由表格只指向其它節點,它就只做爲一個TCP服務器
- 部署一個:kv
應用,讓一個節點只做爲存儲器(沒有TCP入口)
咱們將在將來添加更多的應用,咱們能夠繼續以相同的粒度控制咱們的部署,應用與配置的最佳選擇都取決於產品.
你也能夠考慮使用像exrm這樣的工具來構建多個版本,它將封裝你所選擇的應用和配置,包括當前安裝的的Erlang和Elixir,因此咱們能夠在沒有預先安裝好runtime的目標系統上部署應用.
最後,本章咱們已經學習了一些新的東西,它們能夠被應用於:kv_server
.如下的步驟將做爲練習:
- 使:kv_server
應用從它的應用環境中讀取端口,而不是使用硬代碼的4040值
- 讓:kv_server
應用使用路由功能,替代直接分發到本地的KV.Registry
.在:kv_server
測試中,你可讓路由表格簡單地指向當前節點
#總結
本章咱們構建了一個簡單的路由器,做爲探索Elixir和Erlang VM的分佈式特性的方法,還學習瞭如何配置它的路由表格.這是咱們的Mix和OTP教程的最後一章.
在整個教程中,咱們構建了一個很是簡單的分佈式鍵值存儲,做爲一個探索各類結構的機會,例如通用服務器,主管,任務,代理,應用等等.不只如此,咱們還爲整個應用編寫了測試,熟悉了ExUnit,還學習瞭如何使用Mix構建工具來完成大範圍的任務.
若是你正在尋找一個能在生產中使用的分佈式鍵值存儲,你絕對應該考慮Riak,它也運行在Erlang VM上.在Riak中,桶是可複製的,爲了不數據丟失,他們使用了一致性散列來將桶映射到節點上,而不是使用路由.一致性散列算法有助於減小須要遷移的數據,當新的用來存儲桶的節點被添加到你的基礎設施中時.
這裏還有更多的課程要學習,咱們但願你學的開心!