# 第6章 分布式編程
| 翻譯: | Ken Zhao |
|-----|-----|
| 校訂: | 連城 |
本章描述如何編寫運行于Erlang**節點**網絡上的分布式Erlang程序。我們描述了用于實現分布式系統的語言原語。Erlang進程可以自然地映射到分布式系統之中;同時,之前章節所介紹的Erlang并發原語和錯誤檢測原語在分布式系統和單節點系統中仍保持原有屬性。
### 動機
我們有很多理由去編寫分布式應用,比如:
**速度**
> > 我們可以把我們的程序切分成能夠分別運行于多個不同節點的幾個部分。比如,某個編譯器可以將一個模塊里的各個函數分發到不同節點分別編譯,編譯器本身則負責協調各節點的活動。
> 在例如一個具備一個節點池的實時系統,作業以round-robin的方式指派給不同的節點,以此降低系統的響應延遲。
**可靠性和容錯**
> 為了增加系統的可靠性,我們可以部署多個互相協作的節點,以求一個或多個節點的失敗不致影響整個系統的運作。
**訪問其他節點上的資源**
> 某些軟硬件資源可能只可被特定的計算機訪問。
**秉承應用固有的分布式特質**
> 會議系統、訂票系統以及許多多計算機實時系統都屬于這類應用。
**可擴展性**
> 系統可以被設計成能夠通過添加額外節點來增加系統的容量的形式。如果系統太慢,購買更多的處理器便可提高性能。
### 分布式機制
以下的BIF可用于分布式編程:
spawn(Node,Mod,Func,Args)
> 在遠程節點產生一個新的進程。
spawn_link(Node,Mod,Func,Args)
> 在遠程節點產生一個新的進程并創建一個指向這個進程的鏈接。
monitor_node(Node,Flag)
> 若Flag為true,該BIF令當前進程監視節點Node。如果Node出錯或消失,一個{nodedown,Node}消息將被發送給當前進程,若Flag為false,則關閉監視。
node()
> 返回當前節點名稱。
nodes()
> 返回已知的所有其他節點的名稱列表。
node(Item)
> 返回Item所處節點的名稱。Item可以是Pid、引用或端口。
disconnect_node(Nodename)
> 斷開與節點Nodename的連接。
**節點**是分布式Erlang的一個核心概念。在分布式Erlang系統中,術語**節點**指一個可參與分布式Erlang事務的運行著的Erlang系統。獨立的Erlang可通過啟動一個稱為網絡內核的特殊進程來加入一個分布式Erlang系統。這個進程將計算BIFalive/2。網絡內核將在??詳述。一旦啟動了網絡內核,系統就處于**活動**狀態。
處于活動狀態的系統會被分配一個節點名稱,該名稱可以通過BIF node(Item)獲得。該名稱是一個全局唯一的原子式。不同的Erlang實現中節點名稱的格式可能不同,但總是一個被@分為兩部分的原子式。
BIF node(Item)返回創建Item的節點的名稱,其中Item是一個Pid、端口或引用。
BIF nodes/0返回網絡中與當前節點連接的所有其他節點的名稱列表。
BIF monitor_node(Node,Flag)可用于監視節點。當節點Node失敗或到Node的網絡連接失敗時,執行了monitor_node(Node,true)的進程將收到消息{nodedown,Node}。不幸的是,我們無法區分節點失敗和網絡失敗。例如,以下代碼會一直掛起到節點Node失敗為止:
~~~
.....
monitor_node(Node, true),
receive
{nodedown, Node} ->
.....
end,
.....
~~~
如果連接不存在,且monitor_node/2被調用,系統將嘗試建立連接;若連接建立失敗則投遞一個nodedown消息。若針對同一節點連續兩次調用monitor_node/2則在節點失敗時將投遞**兩條**nodedown消息。
對monitor_node(Node,false)的調用只是遞減一個計數器,該計數器用于記錄Node失敗時需要向調用進程發送的nodedown消息的數量。之所以這么做,是因為我們往往會用一對匹配的monitor_node(Node,true)和monitor_node(Node,false)來封裝遠程過程調用。
BIF spawn/3和spawn_link/3用于在本地節點創建新進程。要在任意的節點創建進程,需要使用BIF spawn/4,所以:
~~~
Pid = spawn(Node, Mod, Func, Args),
~~~
將在Node產生一個進程,而spawn_link/4會在遠程節點產生一個進程并建立一個與當前進程的鏈接。
這兩個BIF各自會返回一個Pid。若節點不存在,也會返回一個Pid,當然由于沒有實際的進程被執行,這個Pid沒什么用處。對于spawn_link/4,在節點不存在的情況下當前進程會收到一個“EXIT”消息。
幾乎所有針對本地Pid的操作同樣都對遠程Pid有效。消息可以被發送至遠程進程,也可以在本地進程和遠程進程間建立鏈接,就好像遠程進程執行于本地節點一樣。這意味著,比方說,發送給遠程進程的消息總是按發送順序傳送、不會受損也不會丟失。這些都是由運行時系統來保障的。消息接收的唯一可能的錯誤控制,就是由程序員掌控的link機制,以及消息發送方和接收方的顯式同步。
### 注冊進程
BIF register/2用于在本地節點上為進程注冊一個名稱。我們可以這樣向遠程節點的注冊進程發送消息:
~~~
{Name, Node} ! Mess.
~~~
若在節點Node上存在一個注冊為名稱Name的進程,則Mess將被發送到該進程。若節點或注冊進程不存在,則消息被丟棄。
### 連接
Erlang節點間存在一個語言層面的連接概念。系統初被啟動時,系統無法“覺察”任何其他節點,對nodes()求值將返回[]。與其他節點間的連接不是由程序員顯式建立的。到遠程節點N的連接是在N首次被引用時建立的。如下所示:
~~~
1> nodes().
[]
2> P = spawn('klacke@super.eua.ericsson.se', M, F, A).
<24.16.1>
3> nodes().
['klacke@super.eua.ericsson.se']
4> node(P).
'klacke@super.eua.ericsson.se'
~~~
要想建立到遠程節點的連接,我們只需要在任意涉及遠程節點的表達式中引用到節點的名稱即可。檢測網絡錯誤的唯一手段就是使用鏈接BIF或monitor_node/2。要斷開與某節點的連接可使用BIF disconnect_node(Node)。
節點之間是松散耦合的。節點可以像進程一樣動態地被創建或消失。耦合不那么松散的系統可以通過配置文件和配置數據來實現。在生產環境下,通常只會部署固定數目個具備固定名稱的節點。
### 銀行業務示例
這一節我們將展示如何結合BIF monitor_node/2和向遠程節點的注冊進程發送消息的能力。我們將實現一個非常簡單的銀行服務,用以處理遠程站點的請求,比如ATM機上存款、取款業務。
程序6.1
~~~
-module(bank_server).
-export([start/0, server/1]).
start() ->
register(bank_server, spawn(bank_server, server, [[]])).
server(Data) ->
receive
{From, {deposit, Who, Amount}} ->
From ! {bank_server, ok},
server(deposit(Who, Amount, Data));
{From, {ask, Who}} ->
From ! {bank_server, lookup(Who, Data)},
server(Data);
{From, {withdraw, Who, Amount}} ->
case lookup(Who, Data) of
undefined ->
From ! {bank_server, no},
server(Data);
Balance when Balance > Amount ->
From ! {bank_server, ok},
server(deposit(Who, -Amount, Data));
_ ->
From ! {bank_server, no},
server(Data)
end
end.
lookup(Who, [{Who, Value}|_]) -> Value;
lookup(Who, [_|T]) -> lookup(Who, T);
lookup(_, _) -> undefined.
deposit(Who, X, [{Who, Balance}|T]) ->
[{Who, Balance+X}|T];
deposit(Who, X, [H|T]) ->
[H|deposit(Who, X, T)];
deposit(Who, X, []) ->
[{Who, X}].
~~~
程序6.1的代碼運行于銀行總部。而在出納機(或分行)中執行的是程序6.2,該程序完成與總行服務器的交互。
程序6.2
~~~
-module(bank_client).
-export([ask/1, deposit/2, withdraw/2]).
head_office() -> 'bank@super.eua.ericsson.se'.
ask(Who) -> call_bank({ask, Who}).
deposit(Who, Amount) -> call_bank({deposit, Who, Amount}).
withdraw(Who, Amount) -> call_bank({withdraw, Who, Amount}).
call_bank(Msg) ->
Headoffice = head_office(),
monitor_node(Headoffice, true),
{bank_server, Headoffice} ! {self(), Msg},
receive
{bank_server, Reply} ->
monitor_node(Headoffice, false),
Reply;
{nodedown, Headoffice} ->
no
end.
~~~
客戶端程序定義了三個訪問總行服務器的接口函數:
ask(Who)
> 返回客戶Who的余額
deposit(Who,Amount)
> 給客戶Who的帳戶里面存入資金數Amount
withdraw(Who,Amount)
> 嘗試從客戶Who的帳戶里面取出資金數Amount
函數call_bank/1實現了遠程過程調用。一旦總行節點停止運作,call_bank/1將會及時發現,并返回no。
總行節點的名稱是硬編碼在源碼中的。在后續章節中我們將展示集中隱藏該信息的手段。