Erlang 基礎ポイント7 - 分散処理

久しぶりのErlangですね。
今日は分散処理について勉強したいと思います。

Erlangで分散処理?

今までは一つのサーバで一つのErlangシェルを立ち上げて勉強しましたが、
実はErlang複数のサーバがネットワークを作って、
大規模な動作をするのに適した仕組みを持っています。
簡単に複数のサーバをつなげて何かができるってことは楽しいですね。

Erlangノード

今マシンで動いている一つのErlangプログラムをErlangノードといいます。
正確にいうと、Erlangプログラムを動かしているErlang仮想マシンをいいます。
JavaJVMのようなものが立ち上がっていると考えればいいでしょう。

JVMは同じマシンで複数実行できますね。
同様にErlangも同じマシンで複数実行できます。
同じマシンでErlangプログラム2個実行した?
つまり、erlコマンドを2個実行したといったら、
"このマシンにはErlangノードが2つ立ち上がっている"といえます。

但し、erlコマンド実行し、Erlangプロセスが終了せず、ずっと動いていなければ、
立ち上がっているとはいえません。あくまで実行中のことが前提です。

Erlangノードについて説明したので、分散処理ノードの形態について
説明します。

分散処理ノードの形態

Erlangノードがどのネットワークのどのマシンにあるかによって、
分散処理ノードの形態は以下のように三つあります。

Erlangノードが同じマシン上にある

一つのマシン上に複数Erlangを実行して分散処理を行います。
一つマシンのリソースを分けて処理をするので、あまりパワーは出せないでしょう。
テスト用に分散処理を確認したい時にいいと思います。

Erlangノードがプライベートネットーワーク(LAN)上にある

社内ネットワークなので、セキュリティが安全ですし、複数のマシンを有効に活用できるので、
これが一番実用性があるのではないかと思います。

Erlangノードがインターネット上にある

インターネット上にある無数のマシンを束ねられることは魅力的ですが、
セキュリティの確保が難しいと思います。
でも、セキュリティにあまり気をつけなくてもいいすごく時間がかかる計算を
小さい処理単位に分けて、インターネット上のマシンで計算して結果を結合するといった用途に
合うかもしれない...と思います。

サンプル

準備

ここでは、プライベートネットーワーク(LAN)上にErlangノードをおくことを前提で、サンプルを作ってみます。
私は実PCを一台しか持っていないので、無料の仮想化ソフト「VirtualBox」を利用し、
Ubuntu 12.05が入った仮想PCを二台作ってから、サンプルを作成及び実行します。

Ubuntu 12.05はHomepage | Ubuntu Japanese Teamでダウンロードできます。
VirtualBox用のイメージが既にあるので、自分でインストールしなくていいです。
仮想PCを二つ作るにはVirtualBoxのクローン機能を使って、入手したイメージをコピーすればすぐです。

以下のように仮想PCを二つ作りました。
f:id:jeongman7:20140831161913j:plain

しかし、ソースをそれぞれの仮想PCで編集すると不便なので、
SSHで各仮想PCに接続してソースの編集を行います。
SSHクライアントはRLoginを使いました。
f:id:jeongman7:20140831200919j:plain

理解しやすくするため、サンプルは、echoサーバのようなものにします。
具体的には送信側がメッセージを受信側に送ると、
受信側は受信したメッセージに特定文字列を付加して受信側に返すものです。

以下、ソースです。

-module(echo).
-export([start/0, send/1]).

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

send(Msg) ->
    echo ! {self(), {server, Msg}},
    receive
        {client, Res} ->
            Res
    end.

loop() ->
    receive
        {From, {server, Msg}} ->
            From ! {client, string:concat("Echo response : ", Msg)},
            loop()
    end.

分散処理を行う前にローカルでstart()関数とsend()関数を呼び出して動作確認をしてみましょう。

6> duke1@duke1-VirtualBox:~/erlang/blog$ erl
Erlang R14B04 (erts-5.8.5) [source] [rq:1] [async-threads:0] [kernel-poll:false]

Eshell V5.8.5  (abort with ^G)
1> c(echo).
{ok,echo}
2> echo:start().
true
3> echo:send("Hello Erlang").
"Echo response : Hello Erlang"
4> echo:send("Hello Erlang").
"Echo response : Hello Erlang"
5> echo:send("Hello Erlang1").
"Echo response : Hello Erlang1"
6> 

問題ないですね。
次は別々の仮想PCでErlangを動かして、分散処理をやってみます。

別々のマシンで分散処理

分散処理するすべてのErlangノードは同じバージョンのErlangと同じプログラムコードを
持つ必要があります。Erlangのバージョンを合わせるためにはそれぞれのマシンに同じバージョンのErlangをインストールするしかないでしょう。
しかし、プログラムコードの共有方法は以下のように色々あるようです。

  1. いちいちプログラムコードをそれぞれのサーバにコピーする。
  2. NFSなどの共有ディスクを使う。
  3. コードサーバを利用する。
  4. Erlangシェルのコマンドnlを使う。

ここでは、NFSを利用して上記のechoプログラムを2台の仮想PCで共有します。
NFSについては、ググればすぐ出ます。
機会があれば、コードサーバに関してブログを書いてみたいと思います。

まず、仮想PC 1号機・2号機のhostsファイルにそれぞれのホスト名とIPアドレスの対応を追加します。
例えば、1号機をvpc1.com(192.168.56.101)、2号機をvpc2.com(192.168.56.103)とするなら、以下のようにします。

192.168.56.101  vpc1.com
192.168.56.103  vpc2.com

次に仮想PC 1号機で以下のようにerlを起動します。
(物理PCや仮想PCに関わらず、ネットワーク状況によって以下の起動オプションでサンプルが動かないかも知れません。私の環境でしか確認していません。)

$ erl -name echoserver@vpc1.com -setcookie echocookie
% -name サーバ名 : 呼出側がこのマシンで動くErlangプログラムを実行する時、識別のため、必要です。
%                  @の前にはErlangノード同士で識別する名前を書きます。
%                  @の後ろはhostsファイルに追加した名前(vpc1.com)を書きます。
% -setcookie クッキー名 : 分散処理をするErlang同士の認証に使われます。適当でいいと思いますが、すべてのErlangノードは同じクッキー名を使う必要があります。

Erlang R14B04 (erts-5.8.5) [source] [rq:1] [async-threads:0] [kernel-poll:false]

Eshell V5.8.5  (abort with ^G)
(echoserver@duke1-VirtualBox)1> c(echo). % コンパイルし、
{ok,echo}
(echoserver@duke1-VirtualBox)2> echo:start(). % エコーサーバを起動します。
true

仮想PC 2号機では、以下のようにerlを起動します。

erl -name echoclient@vpc2.com -setcookie echocookie
Erlang R14B04 (erts-5.8.5) [source] [rq:1] [async-threads:0] [kernel-poll:false]

Eshell V5.8.5  (abort with ^G)
(echoclient@vpc2.com)1> c(echo). % 2号機でもコンパイルします。
{ok,echo}

準備ができました。
仮想PC 2号機で仮想PC 1号機に対してリモート呼出をしてみましょう。

(echoclient@vpc2.com)1> rpc:call(echoserver@vpc1.com, echo, send, ["Hello"]). 
"Echo response : Hello"

できました!
他のマシンにあるErlangプログラムを呼び出す時は、rpcモジュールのcall関数を使います。

rpc:call(相手側のErlangノードのnameに指定した名前@相手側のErlangノードが入っているマシンのホスト名, モジュール名, 関数名, [引数1, 引数x...]). 

他の言語にもあるSOAPやRPCのようで、大したものではないように見えるかも知れませんが、
プログラムや手順が簡単で無駄がないのが分かります。
この簡潔さなら、本来の処理にもっと集中できるのではないでしょうか。

追加(2014/09/26)

rpc:callを使うと、呼び出しが煩雑ですが、
以下のようにsend関数を修正すれば、もっと簡単にリモート呼出ができます。

send(Msg, Receiver) ->
    Receiver ! {self(), {server, Msg}},
    receive
        {client, Res} ->
            Res
    end.

仮想PC 2号機から仮想PC 1号機への呼出は以下のようにすればいいです。

(echoclient@vpc2.com)2> echo:send("Hi VPC1", { echo, 'echoserver@vpc1.com' }).
"Echo response : Hi VPC1"
(echoclient@vpc2.com)3> 

自分自身へはただのechoでいいですね。
もちろん、自分のところでもecho:start()を呼び出す必要があります。

(echoclient@vpc2.com)3> echo:start().
true
(echoclient@vpc2.com)4> echo:send("Hi VPC1", echo).                           
"Echo response : Hi VPC1"

要は、メッセージを送る時、プロセス名 ! メッセージを使いますが、
メッセージの受け取り側がリモートの場合は、プロセス名が{ プロセス名, 'サーバ名' }のように
タプルになるということです。

今日はここまで。

他にソケット通信やファイル、OTPなどがありますが、
これらはAPIの使い方みたいなものなので、基礎ポイントでは取り上げないつもりです。
別のタイトルで勉強しようかと思います。

次回はちょっと面白そうな...ErlangJavaの接続についてやってみたいと思います。

参考

プログラミングErlang

プログラミングErlang