#分布式任务与配置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中,桶是可复制的,为了不数据丢失,他们使用了一致性散列来将桶映射到节点上,而不是使用路由.一致性散列算法有助于减小须要迁移的数据,当新的用来存储桶的节点被添加到你的基础设施中时.
这里还有更多的课程要学习,咱们但愿你学的开心!