UP | HOME

分布式编程

Table of Contents

  用Erlang编写分布式程序和编写并发程序只有一步之遥

在分布式Erlang里,可以在远程节点和机器上创建进程。创建出远程进程之后,会看到其他所有的基本函数( send 、 receive和 link 等)都能透明运作在网络中,就像在单个节点上一样

  在本章将介绍用于编写分布式Erlang程序的库与Erlang基本函数

分布式 程序是那些被设计运行在 计算机网络 上的程序,并且可以仅靠 传递消息 来协调彼此的活动。下面是一些想要编写分布式应用程序的原因:

分布式模型

这里将讨论两种主要的分布式模型:

  1. 分布式Erlang:在分布式Erlang里,编写的程序会在Erlang的 节点 ( node )上运行
    • 节点:一个独立的Erlang系统,包含一个 自带地址空间进程组完整虚拟机
      • 可以在任何节点上创建进程,所有消息传递和错误处理基本函数也都能像在单节点上那样工作
    • 分布式Erlang应用程序运行在一个 可信环境 中。因为任何节点都可以在其他Erlang节点上执行任意操作,所以这涉及高度的信任
      • 虽然分布式Erlang应用程序可以运行在开放式网络上,但它们通常是运行在属于同一个 局域网的集群 上,并受 防火墙 保护
  2. 基于套接字:可以用TCP/IP套接字来编写运行在不可信环境中的分布式应用程序
    • 这个编程模型不如分布式Erlang那样强大,但是更安全
    如果回想一下前面的内容,就一定还记得构建程序的基本单位是进程。编写分布式Erlang程序是很容易的,要做的就是在正确的机器上创建出进程,然后一切就能像之前那样运作了

    人们都习惯了编写顺序程序,而编写分布式程序通常会困难得多。下面将介绍编写简单分布式程序的若干技巧。这些程序很简单,但是非常有用

接下来将从一些小范例起步。只需先学习两件事,就可以开始创建第一个分布式程序了:

  • 如何启动一个Erlang节点
  • 如何在远程Erlang节点上执行远程过程调用

编写分布式程序

当开发一个分布式应用程序时,总是会按照特定的顺序来编写它:

  1. 在一个 常规的非分布式 会话里编写和测试我的程序:这是我们到目前为止一直在做的,所以不会有什么新问题
  2. 在运行于 同一台计算机 上的 两个不同的Erlang节点 里测试程序
  3. 在运行于 两台物理隔离计算机 上的 两个不同的Erlang节点 里测试程序,这两台计算机或者属于同一个局域网,或者来自互联网的任何地方
  4. 如果所运行的机器属于 相同的管理域 ,就很少会出问题
    • 但当相关节点属于 不同域上 的机器时,就可能会遇到 连接性 问题,而且必须确保系统 防火墙安全设置 都已得到正确配置
    为了演示这些步骤,将制作一个简单的名称服务器(name server)

具体而言,将执行下列步骤:

  • 第1阶段:在一个常规的非分布式Erlang系统上编写和测试名称服务器
  • 第2阶段:在同一台机器的两个节点上测试名称服务器
  • 第3阶段:在同一局域网内分属两台不同机器的节点上测试名称服务器
  • 第4阶段:在分属两个不同国家和域的两台机器上测试名称服务器

创建名称服务器

名称服务器 这种程序会返回一个给定名称的关联值。也可以修改某个名称所关联的值

     我们的第一个名称服务器极其简单。它不是容错式的,所以如果它崩溃了,保存的数据就会全部丢失

     这个练习的目的不是创建一个容错式名称服务器,而是开始运用分布式编程的技巧

简单的名称服务器

我们的名称服务器 kvs 是一个简单的 Key –> Value 服务器,它的接口如下:

  • 启动服务器,它将创建一个注册名为 kvs 的服务器:
-spec kvs:start() -> true 
  • 关联 Key 和 Value
-spec kvs:store(Key, Value) -> true 
  • 查询 Key 的值:如果 Key 带有关联值就返回 {ok, Value} ,否则返回 undefined
-spec kvs:lookup(Key) -> {ok, Value} | undefined 

这个键-值服务器是用进程字典里的基本函数 get 和 put 实现的,它的代码如下:

-module(kvs).
-export([start/0, store/2, lookup/1]).

start() -> register(kvs, spawn(fun() -> loop() end)).

store(Key, Value) -> rpc({store, Key, Value}). 

lookup(Key) -> rpc({lookup, Key}). 

rpc(Q) ->
    kvs ! {self(), Q},
    receive
        {kvs, Reply} ->
            Reply
    end.

loop() ->  
    receive
        {From, {store, Key, Value}} ->  
            put(Key, {ok, Value}),
            From ! {kvs, true},
            loop();
        {From, {lookup, Key}} -> 
            From ! {kvs, get(Key)},
            loop()
    end.
  • 保存键值的消息在第6行发送,并在第19行接收
  • 主服务器在第17行的 loop 函数中启动:
    • 它调用了 receive 并等待一个保存或查询消息
    • 保存数据或从本地进程字典里取出被请求的数据
    • 向客户端发送回复

先在本地测试这个服务器,看看它是否能正常工作:

2> kvs:start() . 
true
3> 
3> kvs:store({location, joe}, "Stockholm") . 
true
4> 
4> kvs:store(weather, raining) . 
true
5> 
5> kvs:lookup(weather) . 
{ok,raining}
6> 
6> kvs:lookup({location, joe}) .  
{ok,"Stockholm"}
7>  
7> kvs:lookup({location, jane}) .  
undefined

客户端在一个节点,服务器在相同主机的另一个节点

      现在在同一台计算机上启动两个Erlang节点,为此,需要打开两个终端窗口,然后启动两套Erlang系统

首先,将开启一个终端shell,并在这个shell里启动一个名为 gandalf 的分布式Erlang节点。然后启动服务器:

$ erl -sname gandalf 
Erlang/OTP 23 [erts-11.1.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.7  (abort with ^G)
(gandalf@gentoo)1> kvs:start() . 
true

参数 -sname gandalf 的意思是“在本地主机上启动一个名为 gandalf 的Erlang节点”

      注意以下Erlang shell是如何把Erlang节点名打印在命令提示符前面的

      节点名的形式是 Name@Host ,Name 和 Host 都是原子,所以如果它们包含任何非原子的字符,就必须加上引号

接下来将开启第二个终端会话,然后启动一个名为 bilbo 的Erlang节点。这样就可以用库模块 rpc 来调用 kvs 里的函数了

$ erl -sname bilbo 
Erlang/OTP 23 [erts-11.1.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.7  (abort with ^G)
(bilbo@gentoo)1> rpc:call(gandalf@gentoo, kvs, store, [weather, fine]) . 
true
(bilbo@gentoo)2> 
(bilbo@gentoo)2> rpc:call(gandalf@gentoo, kvs, lookup, [weather]) .      
{ok,fine}
      虽然看起来不太起眼,但实际上已经执行了我们的第一次分布式计算:服务器运行在我们启动的第一个节点上,客户端则运行在第二个节点上 

rpc:call(Node, Mod, Func, [Arg1, Arg2, …, ArgN]) 会在 Node 上执行一次 远程过程调用 ,调用的函数是 Mod:Func(Arg1, Arg2, …, ArgN)

      请注意, rpc 是一个标准的Erlang库模块,和之前编写的 rpc 函数不是一回事

设置 weather 值的调用是由 bilbo 节点发出的 ,可以切换回 gandalf 来检查一下天气 (weather) 的值:

(gandalf@gentoo)2> kvs:lookup(weather) . 
{ok,fine}
      如你所见,这个程序的工作方式和非分布式Erlang一致

      唯一的区别在于客户端运行在一个节点上,而服务器运行在另一个不同的节点上

同一局域网内不同机器上的客户端和服务器

下一步是在不同的机器上运行客户端和服务器:

      第一个名为 gandalf 的节点在 gentoo.klose.com 上,第二个名为 bilbo 的节点在 raspberrypi.klose.com 上

      开始工作之前,我们先用ssh或vnc等工具在两台不同的机器上各启动一个终端

      我们把这两个窗口称为doris和george。做完这些之后,就可以在两台机器上轻松输入命令了

首先是在 gentoo 上启动一个Erlang节点:

gentoo$ erl -name gandalf@gentoo.klose.com --setcookie abc 
Erlang/OTP 23 [erts-11.1.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.7  (abort with ^G)
(gandalf@gentoo.klose.com)1> kvs:start() . 
true

接着在 raspberrypi 上启动一个Erlang节点并向 gandalf 发送一些命令:

raspberrypi$ erl -name bilbo@raspberrypi.klose.com --setcookie abc 
Erlang/OTP 21 [erts-10.2.4] [source] [smp:4:4] [ds:4:4:10] [async-threads:1]

Eshell V10.2.4  (abort with ^G)
(bilbo@raspberrypi.klose.com)1> rpc:call('gandalf@gentoo.klose.com', kvs, store, [weather, cold]) . 
true
(bilbo@raspberrypi.klose.com)2> 
(bilbo@raspberrypi.klose.com)2> rpc:call('gandalf@gentoo.klose.com', kvs, lookup, [weather]) .       
{ok,cold}
      它们的行为和同一机器上两个不同节点的情况完全一致

要实现这一切,我们的操作会比在同一台机器上运行两个节点时略微复杂一些。必须分4步走:

  1. -name 参数启动Erlang:
    • 在同一台机器上运行两个节点时使用了“短”名称(通过 -sname 标识体现),当两台机器位于同一个子网时也可以使用 -sname
    • 但如果它们属于不同的网络,就要使用 -name,如果没有DNS服务, -sname 就是唯一可行的方式
  2. 确保两个节点拥有 相同的 cookie :这正是启动两个节点时都使用命令行参数 -setcookie abc 的原因
  3. 确保相关节点的 完全限定主机名 (fully qualified hostname)可以 被DNS解析
    • 对于我来说,域名 klose.com 完全属于我的家庭网络,通过在 /etc/hosts 里添加一个条目来实现本地解析
  4. 确保两个系统拥有 相同版本的代码相同版本的Erlang
      如果不这么做,就可能会得到严重而离奇的错误

      避免问题的最简单的方法是在所有地方都运行相同版本的Erlang

      不同版本的Erlang可以一起运行,但是无法保证能正常工作,所以最好事先检查一下 

跨互联网不同主机上的客户端和服务器

      原则上,这和第3阶段是一样的,但现在我们必须更加关注安全性。运行同一局域网内的两个节点时,多半不会过于担心安全性

      在大多数机构里,局域网都是通过防火墙与互联网隔离的,可以在防火墙后面自由分配临时IP地址,对机器的设置也很随意

当跨互联网连接Erlang集群里的几台机器时,可以预料到会出现防火墙不允许传入连接的问题,必须正确 配置防火墙 ,让它接受传入连接

      这一点没有通用的做法,因为每一种防火墙都是不同的

要让系统准备好运行分布式Erlang,需执行以下步骤:

  • 确保 4369 端口对 TCPUDP 流量都开放。这个端口会被一个名为 epmd 的程序使用(它是Erlang Port Mapper Daemon的缩写,即Erlang端口映射守护进程)
  • 选择一个或一段连续端口给分布式Erlang使用,并确保这些端口是开放的。如果这些端口位于 Min 和 Max 之间(只想用一个端口就让 Min=Max ),就用以下命令启动 Erlang:
$ erl -name ... --setcookie ... -kernerl inet_dist_listen_min MIN \
                                         inet_dist_listen_max MAX
      现在,已经了解了如何在一组Erlang节点上运行程序,以及如何通过局域网和互联网运行它们

      下面来看看操作节点的基本函数

分布式编程的库和内置函数

    编写分布式程序时很少从头开始。标准库里有许多模块可以用于编写分布式程序

    虽然这些模块是用内置分布式函数编写的,但是它们能对程序员隐藏大量繁琐的细节

标准分发套装里的两个模块能够满足大多数需求。

  • rpc : 提供了许多 远程过程调用 服务
  • global : 里的函数可以用来在分布式系统里 注册名称加锁 ,以及维护一个全连接网络

rpc 模块

rpc 模块里最重要的函数就是下面这个。

-spec call(Node, Mod, Func, Args) -> Result | {badrpc, Reason}

它会在 Node 上执行 apply(Mod, Func, Args) ,然后:

  • 如果调用成功返回结果 Result
  • 如果调用失败则返回 {badrpc, Reason}

以下是编写分布式程序的基本函数:

远程创建进程

-spec spawn(Node, Func) -> Pid 

它的工作方式和 spawn(Func) 完全一致,只是新进程是在 Node 上创建的

-spec spawn(Node, Mod, Func, Args) -> Pid

它的工作方式和 spawn(Mod, Func, Args) 完全一致,只是新进程是在 Node 上创建的

      这种形式的 spawn 比 spawn(Node, Func) 更加健壮

      如果运行在多个分布式节点上的特定模块不是完全相同的版本, spawn(Node, Func) 就可能会出错

远程连接

-spec spawn_link(Node, Fun) -> true 

它的工作方式和 spawn_link(Fun) 完全一致,只是新进程是在 Node 上创建的

-spec spawn_link(Node, Mod, Fun, Args) -> true

它的工作方式类似 spawn(Node, Mod, Fun, Args) ,但是新进程会与当前进程相连

断开连接

-spec disconnect_node(Node) -> bool() | ignored 

它会强制断开与某个节点的连接

远程监视

-spec monitor_node(Node, Flag) -> true
  • 如果 Flag 是 true 就会 开启监视
    • 如果开启了监视,那么当Node 加入或离开Erlang互连节点组时,执行这个内置函数的进程就会收到 {nodeup, Node}{nodedown, Node} 的消息
  • 如果 Flag 是 false 就会 关闭监视

节点管理

-spec node() -> Node 

它会返回本地节点的名称:

  • 如果节点不是分布式的则会返回 nonode@nohost
-spec node(Arg) -> Node 

它会返回 Arg 所在的节点:

  • Arg 可以是 PID引用 或者 端口
  • 如果本地节点不是分布式的,则会返回 nonode@nohost
-spec nodes() -> [Nodes] 

它会返回一个列表,内含网络里其他所有与我们相连的节点

-spec is_alive() -> bool() 

如果本地节点是活动的,并且可以成为分布式系统的一部分,就返回 true ,否则返回 false

远程消息

send 可以用来向一组分布式Erlang节点里的某个本地注册进程发送消息:

{Regname, Node} ! Msg 

把消息 Msg 发送给节点 Node 上的注册进程 RegName

实例

     作为一个简单的示例,将展示如何在某个远程节点上创建进程

从下面这个程序开始:

-module(dist_demo).

-export([rpc/4, start/1]).

start(Node) ->
    spawn(Node, fun() -> loop() end).

rpc(Pid, M, F, A) ->
    Pid ! {rpc, self(), M, F, A},
    receive
        {Pid, Response} ->
            Response
    end.

loop() ->
    receive
        {rpc, Pid, M, F, A} ->
            Pid ! {self(), (catch apply(M, F, A))},
            loop()
    end.

然后启动两个节点,它们都必须能够载入这段代码

     如果这两个节点在同一台主机上,这就不成问题。只需从同一个目录里启动两个Erlang节点就可以了

     如果节点分别属于两台物理隔离且文件系统不同的主机,这个程序就必须被复制到所有节点上,编译之后才能启动节点(或者也可以把 .beam 文件复制到所有节点上)

     在这个例子里,我假定这一切都已完成

在主机 gentoo 上,启动一个名为 gandalf 的节点:

$ erl -name gandalf@gentoo.klose.com -setcookie abc 
Erlang/OTP 23 [erts-11.1.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.7  (abort with ^G)
(gandalf@gentoo.klose.com)1> 

在主机 raspberrypi 上,启动一个名为 bilbo 的节点,要记得使用同一个cookie:

$ erl -name bilbo@raspberrypi.klose.com -setcookie abc 
Erlang/OTP 21 [erts-10.2.4] [source] [smp:4:4] [ds:4:4:10] [async-threads:1]

Eshell V10.2.4  (abort with ^G)

现在(在 bilbo 上),让远程节点( gandalf )创建一个进程:

(bilbo@raspberrypi.klose.com)1> Pid = dist_demo:start('gandalf@gentoo.klose.com') . 
<8209.92.0>

Pid 是这个 远程节点 进程的标识符,接着再调用 dist_demo:rpc/4 ,在远程节点(gandalf)上执行一次 远程过程 调用:

(bilbo@raspberrypi.klose.com)2> dist_demo:rpc(Pid, erlang, node, []) . 
'gandalf@gentoo.klose.com'

它在远程节点上执行 erlang:node() 并返回一个值

文件操作

下面这些操作是上一个示例的延续:

(bilbo@raspberrypi.klose.com)4> dist_demo:rpc(Pid, file, get_cwd, []) .  
{ok,"/home/klose/tmp"}

(bilbo@raspberrypi.klose.com)6> dist_demo:rpc(Pid, file, list_dir, ["."]) .    
{ok,["dist_demo.erl","dist_demo.beam","hello.txt"]}

(bilbo@raspberrypi.klose.com)9> dist_demo:rpc(Pid, file, read_file, ["dist_demo.erl"]) .     
{ok,<<"%% ---\n%%  Excerpted from \"Programming Erlang, Second Edition\",\n%%  published by The Pragmatic Bookshelf.\n%%"...>>}

在 bilbo 上发起的一些请求形成了对 gandalf 上标准库的远程过程调用。使用 file 模块里的三个函数来访问 gandalf 的文件系统 :

  • get_cwd(): 返回文件服务器的当前工作目录
  • list_dir(Dir): 返回 Dir 里所有文件的列表
  • read_file(File): 读取文件 File
      仔细回味一下,你会意识到刚才所做的相当神奇

      没有编写任何代码就创建了一个文件服务器,只是重用了 file 模块里的库代码,并使它可以通过一个简单的远程过程调用接口访问

cookie 保护系统

cookie 系统让访问单个或一组节点变得更安全。每个节点都有一个cookie,如果它想与其他任何节点通信,它的cookie就必须和对方节点的cookie相同

为了确保cookie相同,分布式Erlang系统里的所有节点都必须以相同的“神奇”(magic)cookie启动,或者通过执行 erlang:set_cookie 把它们的cookie修改成相同的值 

Erlang集群的定义就是一组带有相同cookie的互连节点

设置 Cookie

可以用以下三种方法设置cookie

文件系统

在文件 $HOME/.erlang.cookie 里存放相同的cookie。这个文件包含一个随机字符串,是Erlang第一次在你的机器上运行时自动创建的:

  • 这个文件可以被复制到所有想要参与分布式Erlang会话的机器上
  • 也可以显式设置它的值
      注意:.erlang.cookie 文件只能被它的所有者访问,它的权限必须设置为 400 

启动参数

当Erlang启动时,可以用命令行参数 -setcookie C

内置函数

erlang:set_cookie(node(), C) 能把本地节点的cookie设成原子 C

      如果你的环境不够安全,那么方法1和3要优于方法2

      因为Unix系统里的任何用户都可以用 ps 命令来查看你的cookie,启动参数只适用于测试

安全性

cookie保护系统被设计用来创建运行在局域网(LAN)上的分布式系统,LAN本身应该受防火墙保护,与互联网隔开

跨互联网运行的分布式Erlang应用程序应该先在主机之间建立安全连接,然后再使用cookie保护系统

cookie从不会在网络中明文传输,它只用来对某次会话进行初始认证

     此外,分布式Erlang会话不是加密的,但可以被设置成在加密通道中运行

Next:OTP库

Previous:错误处理

Home:目录