Rubyで分散オブジェクト (dRuby)

(2002.04.27 加筆。)

ネットワーク越しにオブジェクト(のメソッド)を呼び出せる分散オブジェクト技術。pure Rubyな実装であるdRubyで遊んでみる。

I like Ruby.

何が嬉しいの?

一つのプロセス内だとオブジェクトを操作するのは単にメソッドを呼び出すだけ。プロセスを跨ごうとすると,とたんにソケットだの何だのと,オブジェクトを分解して送信し,受信したら再びオブジェクトに復元しないといけなくなる。ネットワークの先にあるオブジェクトに直接アクセスできたらいいのに,と思う。これができるのが分散オブジェクトの嬉しさ。

分散オブジェクトの構図を図にするとこんな感じ。

    [クライアント]                [オブジェクト実装]
         |                      (オブジェクトサーバー)
         |                                |
      [スタブ]                       [スケルトン]
         |                                |
[Object Request Broker (ORB)]           [ORB]
         |
    [ネットワーク]-----[リクエスト]----->

ネットワーク向こうにあるオブジェクト(リモートオブジェクト)を操作したいクライアントと,オブジェクトを実装するオブジェクト・サーバーとがある。

Object Request Broker (ORB) が仲介する。ORBは,クライアントではスタブを,サーバーではスケルトンを生成する。スタブへのアクセスが発生すると,ORBはメソッド呼び出しを直列化してサーバーへ送る。

オブジェクト自体をやり取りするときも,ORBが適切に変換・復元してくれる。

スタンドアロン・プログラム

まずはネットワークを介しないスクリプトを書いてみる。

sample-1.1.rb
  5: class Account
  6:   def initialize
  7:     @balance = 0
  8:   end
  9:   def deposit(amount)
 10:     @balance += amount
 11:   end
 12:   def withdraw(amount)
 13:     @balance -= amount
 14:   end
 15:   attr_reader :balance
 16: end
 17: 
 18: if __FILE__ == $0
 19:   a = Account.new
 20:   a.deposit 100
 21:   a.withdraw 50
 22:   puts a.balance
 23: end
実行結果:
 50

クラスAccountは,口座を模したクラス。Account#depositメソッドでamountだけ預け入れ,Account#withdrawでamountだけ引き出す。Account#balanceでその時点の残高を得る。

このスクリプトでは残高がマイナスになっても気にしない。

分散オブジェクト化する

これをdRubyを利用して,ネットワークを介して呼び出すようにする。

まずはサーバー。

sample-2.1.rb
  6: require "drb/drb"
  7: require "sample-1.1.rb"
  8: 
  9: class Account
 10:   def host
 11:     return [Socket.gethostname, Process.pid]
 12:   end
 13: end
 14: 
 15: DRb.start_service(nil, Account.new)
 16: puts DRb.uri
 17: DRb.thread.join

sample-1.1のAccountクラスを流用する。動作するためには必要ないが,テストのためにどのプロセスで実行されたか知るためにメソッドhostを追加する。

dRubyを使うときは,DRb.start_serviceクラスメソッドを最初に呼び出す。frontとしてオブジェクトを渡すと,それをリモートオブジェクトとして登録できる。DRb.start_serviceは,内部でDRbServerオブジェクトを生成し,それをプライマリサーバーとする。

DRb.uriは,このオブジェクトにアクセスするためのURIを返す。クライアントでは,このURIによって,サーバーを識別する。

上のサンプルは,AccountオブジェクトサーバーのURIを表示した後,アクセスを待つためにループに入る。

DRbモジュール;
  DRb.start_service(uri = nil, front = nil, acl = nil)
  DRb.uri()

次はクライアント。

sample-2.1-cli.rb
  6: require 'drb/drb'
  7: 
  8: DRb.start_service
  9: remote = DRbObject.new(nil, ARGV.shift)
 10: remote.deposit 100
 11: remote.withdraw 50
 12: puts remote.balance
 13: host = remote.host
 14: print "remote.hostname = #{host[0]}, pid = #{host[1]}\n"
 15: print "local.hostname = #{Socket.gethostname}, pid = #{Process.pid}\n"

クライアントでも,最初にDRb.start_serviceを呼び出す。

DRbObject.newで,スタブを生成する。DRbObjectインスタンスへのアクセスは,dRubyによってリモートオブジェクトへ送信される。

実行結果:
~/ruby/druby$ ruby sample-2.1-cli.rb druby://orange.fruits:1061
50
remote.hostname = orange.fruits, pid = 19459
local.hostname = orange.fruits, pid = 19460

~/ruby/druby$ ruby sample-2.1-cli.rb druby://orange.fruits:1061
100
remote.hostname = orange.fruits, pid = 19459
local.hostname = orange.fruits, pid = 19461

サーバーオブジェクトはずっと生きているので,異なるクライアントからのアクセスであっても,同じオブジェクトが使い回される。

オブジェクトをやり取りする

Accountオブジェクトは,オブジェクトサーバーで活性化され,クライアントはサーバーにあるオブジェクトにアクセスできた。今度はオブジェクト自体をネットワーク越しに渡してみる。

BalloonStoreクラスのインスタンスはサーバーで動かす。そのメソッドgetでBalloonクラスのインスタンスを生成し,Balloonインスタンス自体をクライアントに渡したい。

まずはサーバー。

sample-3.1.rb
  4| require "drb/drb"
  5| 
  6| module Info
  7|   def host
  8|     return [Socket.gethostname, Process.pid]
  9|   end
 10| end
 11| 
 12| class Balloon
 13|   include Info
 14|   def initialize(s)
 15|     @size = s
 16|   end
 17|   def fill(s)
 18|     @size += s
 19|   end
 20|   attr_reader :size
 21| end
 22| 
 23| class BalloonStore
 24|   include Info
 25|   def get(s)
 26|     return Balloon.new(s)
 27|   end
 28| end
 29| 
 30| if __FILE__ == $0
 31|   DRb.start_service(nil, BalloonStore.new)
 32|   puts DRb.uri
 33|   DRb.thread.join
 34| end

BalloonStore#getメソッドは,単にBalloonインスタンスを生成し,それを返す。

で,クライアント。

sample-3.1-cli.rb
  4| require "drb/drb"
  5| require "sample-3.1.rb"
  6| 
  7| DRb.start_service
  8| stub = DRbObject.new(nil, ARGV.shift)
  9| host = stub.host
 10| print "hostname = #{host[0]}, pid = #{host[1]}\n"
 11| balloon = stub.get(5)
 12| balloon.fill(10)
 13| balloon.fill(50)
 14| puts balloon.size
 15| host = balloon.host
 16| print "hostname = #{host[0]}, pid = #{host[1]}\n"

リモートオブジェクトのgetメソッドを呼び出し,Balloonインスタンスを取得する。BalloonStore, Balloon両方のホスト名などを表示する。

5行目のrequire "sample-3.1.rb"をコメントアウトすると,NameErrorが発生する。オブジェクトをやり取りするときは,クライアントでもそのオブジェクトのクラスの定義が必要。

実行結果;
hostname = orange.fruits, pid = 1661
sample-3.1-cli.rb:12: undefined method `fill' for 
    #<DRb::DRbUnknown:0x4023a56c> (NameError)

Balloonの定義があれば,正常に動く。

実行結果;
hostname = orange.fruits, pid = 321
65
hostname = orange.fruits, pid = 323

BalloonStoreインスタンスはサーバーで動作し,Balloonインスタンスはクライアントで動作しているのが分かる。

リモートオブジェクトは,サーバーのURIさえ分かれば,クライアントではクラスの定義を取り込む必要はない。しかし,オブジェクトをやり取りするときは,インスタンス変数などのみがやり取りされ,メソッドはやり取りされないので,クライアントの方でもそのオブジェクトのクラスの定義が必要となる。

ひとつのサーバーで複数のリモートオブジェクト

2002.04.27 この節を追加。

dRubyではURIでサーバーを識別するが,多くのリモートオブジェクトを使うときにも一つのURIで済ませたい。

目的のリモートオブジェクトを生成するためだけのリモートオブジェクトを用意し,これを窓口にする。

sample-4.1.rb
  1| 
  2| # -*- encoding:euc-jp -*-
  3| 
  4| require "drb/drb"
  5| 
  6| class Counter
  7|   include DRbUndumped
  8|   
  9|   def initialize(v = 0)
 10|     @value = v
 11|   end
 12|   attr_reader :value
 13| 
 14|   def incr() @value += 1 end
 15|   def decr() @value -= 1 end
 16| end
 17| 
 18| class ReverseCounter
 19|   include DRbUndumped
 20| 
 21|   def initialize(v = 1000)
 22|     @value = v
 23|   end
 24|   attr_reader :value
 25| 
 26|   def incr() @value -= 1 end
 27|   def decr() @value += 1 end
 28| end
 29| 
 30| class Factory
 31|   def createCounter
 32|     return Counter.new
 33|   end
 34| 
 35|   def createReverseCounter
 36|     return ReverseCounter.new
 37|   end
 38| end
 39| 
 40| if __FILE__ == $0
 41|   DRb.start_service(nil, Factory.new)
 42|   puts DRb.uri
 43|   DRb.thread.join
 44| end

ほかのリモートオブジェクトを生成するための,Factoryクラスを設ける。このクラスのインスタンスをDRb.start_serviceメソッドで登録する。

createCounterメソッドでCounterインスタンス,createReverseCounterメソッドでReverseCounterインスタンスを生成するが,このままだとリモートオブジェクトではなく,これらのインスタンスがそのままクライアントに渡されてしまう。

Counterクラス,ReverseCounterクラスでDRbUndumpedモジュールをincludeすると,これらのクラスのインスタンスはリモートオブジェクトとして扱われる。

次に,クライアント。

sample-4.1-cli.rb
  1| 
  2| # -*- encoding:euc-jp -*-
  3| 
  4| require "drb/drb"
  5| 
  6| if __FILE__ == $0
  7|   remote = ARGV.shift
  8|   raise "no remote-server URI" if !remote
  9| 
 10|   DRb.start_service()
 11|   factory = DRbObject.new(nil, remote)
 12|   nc = factory.createCounter()
 13|   rc = factory.createReverseCounter()
 14|   
 15|   nc.incr(); nc.incr(); nc.incr()
 16|   p nc.value
 17| 
 18|   rc.incr(); rc.incr(); rc.incr()
 19|   p rc.value
 20| end
実行結果;
3
997

FactoryクラスのcreateCounterメソッドなどを呼び出し,リモートオブジェクトへのスタブを取得する。あとは,それぞれのオブジェクトを普通に扱える。

この例では,毎回リモートオブジェクトを生成しているが,プールしておいて,クライアントから呼び出されたときにそれを返せば,永続的なリモートオブジェクトも簡単に作れる。

オブジェクトサーバーを分散させる

今度は,複数のサーバーを用意して,クライアントからはどのサーバーがオブジェクトを提供しているか意識せずにリモートオブジェクトを扱えるようにしてみる。

リモートオブジェクトを提供するサーバーを分散することで,一台当たりの負荷を低くしたり,サービスを停止することなくサーバーを入れ替えることができるかもしれない。

目的のオブジェクトを提供するサーバーとは別に,どのリモートオブジェクトがどのサーバーで動いているかを管理する,ネームサービスオブジェクトを用意する。

sample-5.1-name.rb
  4| require "drb/drb"
  5| 
  6| class NameService
  7|   def initialize()
  8|     @hash = Hash.new
  9|   end
 10| 
 11|   def bind(name, obj)
 12|     raise TypeError if !name.kind_of?(String)
 13|     @hash[name] = obj
 14|   end
 15| 
 16|   def resolve(name)
 17|     raise TypeError if !name.kind_of?(String)
 18|     @hash[name]
 19|   end
 20| end
 21| 
 22| if __FILE__ == $0
 23|   DRb.start_service(nil, NameService.new)
 24|   puts "start name-service."
 25|   puts DRb.uri
 26|   DRb.thread.join
 27| end

NameServiceクラスは,オブジェクトの名前とそのオブジェクトへの参照とを対応付ける。

次は,オブジェクトを提供するサーバー。先ほどのCounter, ReverseCounterを流用する。

sample-5.1-serv.rb
  4| require "drb/drb"
  5| require "./sample-4.1.rb" # Counter, ReverseCounter
  6| 
  7| if __FILE__ == $0
  8|   remote = ARGV.shift
  9|   raise "no name-server URI" if !remote
 10| 
 11|   DRb.start_service()
 12|   naming = DRbObject.new(nil, remote)
 13|   naming.bind("jp.ne.nifty.vzw00011/Counter.1", Counter.new)
 14|   naming.bind("jp.ne.nifty.vzw00011/ReverseCounter.1", ReverseCounter.new)
 15|   puts "ready."
 16|   DRb.thread.join
 17| end

NameServiceオブジェクトを取得し,オブジェクトを登録する。

次はクライアント。

sample-5.1-cli.rb
  4| require "drb/drb"
  5| 
  6| def test_counter(obj)
  7|   obj.incr(); obj.incr(); obj.incr()
  8|   p obj.value
  9| end
 10| 
 11| if __FILE__ == $0
 12|   remote = ARGV.shift
 13|   raise "no name-server URI" if !remote
 14| 
 15|   DRb.start_service()
 16|   naming = DRbObject.new(nil, remote)
 17|   nc = naming.resolve("jp.ne.nifty.vzw00011/Counter.1")
 18|   rc = naming.resolve("jp.ne.nifty.vzw00011/ReverseCounter.1")
 19| 
 20|   test_counter(nc); test_counter(rc)
 21| end

クライアントでは,いったんNameServiceオブジェクトを取得し,オブジェクトの名前からリモートオブジェクト(のスタブ)を取得する。

Counter, ReverseCounterリモートオブジェクトを呼び出すとき,これらのオブジェクトを提供するサーバーと直接交信するので,NameServiceサーバーとの交信は,大きな負荷とはならない。

実用化するには,成りすましを避ける工夫が必要。

サイト内関連文書

ORBit2
CORBA実装であるORBit2の解説