编写分布式应用程序的需求:

  1. 效率性: 可以通过将程序分成多个部分并行运行在多个机器上的方式来提高效率。
  2. 可靠性: 可以通过将程序分布在多个机器上以避免单点提高系统的容错性。
  3. 伸缩性: 可以通过动态的增加机器以提升整个系统的处理能力。
  4. 业务性: 像网络游戏、聊天软件等系统其业务本身就需要分布式的支持。
  5. 乐趣性: 这里作者提到了其自身的乐趣, 当然这应该也是很多程序员的乐趣。

分布式编程的两种基本模型:

  1. 分布式Erlang, 不管是同一个计算机集群还是同一个局域网下的不同计算机集群, 在安全性的保证下, 皆可使用与单节点编程时一致的消息传递和错误处理原语来完成分布式程序的编写。
  2. 基于套接字, 在广域网的网络环境中, 可以使用TCP/IP套接字来编写在非信任环境中的分布式应用。

编写分布式程序的步骤(作者推荐):

  1. 在非分布式环境中编写和测试程序。
  2. 在同一机器的两个不同Erlang节点上测试程序。
  3. 在不同机器上开启不同的Erlang节点来测试程序。

10.1 名字服务

编写一个名字服务程序, 它实现这样功能: 向服务器提交一个名字, 服务器返回与此名字相关联的值。

10.1.1 第一步: 一个简单的名字服务

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

%% 启动进程并对其注册
start() ->register(kvs, spawn(fun() ->loop() end)).

%% 封装rpc调用, 建立名字与值的关联
store(Key, Value) ->rpc({store, Key, Value}).

%% 封装rpc调用, 查询名字关联的值
lookup(Key) ->rpc({lookup, Key}).

%% 远程过程调用
%% 将接收的参数作为消息发送给以注册的进程kvs
rpc(Q) ->
    kvs ! {self(), Q},
    receive
        {kvs, Reply} ->Reply
    end.

%% kvs进程的处理函数
%% 循环接收消息, 执行建立关联和名字查询
loop() ->
    receive
        {From, {store, Key, Value}} ->
            put(Key, {ok, Value}),
            From ! {kvs, true},
            loop();
        {From, {lookup, Key}} ->
            From ! {kvs, get(Key)},
            loop()
    end.

调用结果:

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

10.1.2 第二步: 在同一台机器上, 客户端运行于一个节点而服务器运行于第二个节点

启动gandalf节点并启动服务(从这里的名字看得出来, 作者还对《指环王》感兴趣 _):

matrix@MBP:10 $ erl -sname gandalf
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
(gandalf@matrix)1> kvs:start().
true

启动biblo节点作为客户端并调用gandalf节点的服务:

matrix@MBP:10 $ erl -sname bilbo
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
(bilbo@matrix)1> rpc:call(gandalf@matrix, kvs, store, [weather, fine]).
true
(bilbo@matrix)2> rpc:call(gandalf@matrix, kvs, lookup, [weather]).      
{ok,fine}

在gandalf节点查看客户端提交的名字:

(gandalf@matrix)2> kvs:lookup(weather).
{ok,fine}

10.1.3 第三步: 让客户机和服务器运行于同一个局域网内的不同机器上

启动gandalf节点并启动服务(这里两台机器在同一个局域网下, 因此仍然使用sname参数):

[matrix@hadoop 10]$ erl -sname gandalf -setcookie abc
Erlang R15B03 (erts-5.9.3.1) [source] [smp:2:2] [async-threads:0] [kernel-poll:false]

Eshell V5.9.3.1  (abort with ^G)
(gandalf@hadoop)1> kvs:start().
true

启动biblo节点作为客户端并调用gandalf节点的服务:

matrix@MBP:10 $ erl -sname bilbo -setcookie abc
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
(bilbo@matrix)1> rpc:call(gandalf@hadoop, kvs, store, [weather, cold]).
true
(bilbo@matrix)2> rpc:call(gandalf@hadoop, kvs, lookup, [weather]).      
{ok,cold}

在gandalf节点查看客户端提交的名字:

(gandalf@hadoop)2> kvs:lookup(weather).  
{ok,cold}

需要注意的是:如果远程调用时提示{badrpc,nodedown}, 则应确保两台机器互通及进行防火墙设置。

10.1.4 第四步: 在因特网上的不同主机上分别运行客户机和服务器

相比于在第三步中可以直接关闭防火墙进行测试, 在这里就需要进行相关的配置:

  1. 确保4396端口的TCP和UDP通信正常。
  2. 使用指定的端口范围用于分布式Erlang的通信。
    启动方式为:
$ erl -name ... -setcookie ... -kernel inet_dist_kusten_min Min inet_dist_kusten_max Max

10.2 分布式原语

在Erlang集群中通过cookie识别来保证多个节点在同一个集群中进行通信。
常用的分布式原语:

  • spawn(Node, Fun)
    在指定的节点上通过执行相应函数创建进程
  • spawn(Node, Mod, Func, ArgList)
    在指定的节点上通过以指定参数执行指定模块的指定函数来创建进程
  • spawn_link(Node, Fun)
    在指定节点上创建使主进程随被创建进程销毁而销毁的进程
  • spawn_link(Node, Mod, Func, ArgList)
    在指定的节点上通过以指定参数执行指定模块的指定函数来创建与当前进程关联的进程
  • disconnect_node(Node, Flag)
    强制断开与节点的连接
  • monitor_node(Node, Flag)
    根据Flag参数值打开或关闭对节点的监视
  • node()
    返回本地节点的名字
  • node(Arg)
    返回参数所指定的节点, 参数可以为进程号、引用或端口
  • nodes()
    返回网络上与当前节点连接的所有其它节点的列表
  • is_alive()
    获取本地节点的状态
  • {RegName, Node} ! Msg
    向指定节点的某个注册进程发送消息

一个启动远程进程的样例

-module(dlist_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.

调用结果:
首先启动gandalf节点, 查看节点名称

[matrix@hadoop 10]$ erl -sname gandalf -setcookie abc
Erlang R15B03 (erts-5.9.3.1) [source] [smp:2:2] [async-threads:0] [kernel-poll:false]

Eshell V5.9.3.1  (abort with ^G)
(gandalf@hadoop)1> erlang:node(). 
gandalf@hadoop

然后启动bilbo节点, 执行远程调用

matrix@MBP:10 $ erl -sname bilbo -setcookie abc
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
(bilbo@matrix)1> Pid = dlist_demo:start('gandalf@hadoop').
<5998.43.0>
(bilbo@matrix)2> dlist_demo:rpc(Pid, erlang, node, []).
gandalf@hadoop

10.3 分布式编程中使用的库

同Java一样, Erlang提供了编写分布式应用的库, 屏蔽了直接使用分布式原语的复杂性。

  1. rpc模块提供了一系列远程调用服务。
  2. global模块提供了名称注册函数和分布式系统中的锁定功能, 除此之外还有完整的网络连接维护函数。

10.4 有cookie保护的系统

两个互相通信的Erlang节点必须使用相同的magic cookie。

  1. 把相同的cookie存放于$HOME/.erlang/cookie文件中
  2. 启动Erlang时通过 -setcookie 参数设置
  3. 使用erlang:set_cookie(node(), C) 对本地节点进程设置

10.5 基于套接字的分布式模式

因为Erlang内置的分布式模式假设在一个Erlang集群中任何机器都可信, 因此可以从一台机器上控制所有的机器(可以运行任何进程), 如果并不希望如此, 则可使用基于套接字的分布式模式。

10.5.1 lib_chan

运行用户显式的控制可以运行哪些进程的模块。主要的接口包括:

  1. start_server(), 根据$HOME/.erlang/lib_chan.conf中的配置(配置格式为{Key, Value})启动一个服务
  2. start_server(Conf), 以指定的配置(配置格式为{Key, Value})启动服务
  3. connect(Host, Port, S, P, ArgsC), 尝试在指定主机打开指定端口, 然后尝试激活由密码P保护的服务S

10.5.2 服务端代码