(2002.04.27 加筆。)
ネットワーク越しにオブジェクト(のメソッド)を呼び出せる分散オブジェクト技術。pure Rubyな実装であるdRubyで遊んでみる。
一つのプロセス内だとオブジェクトを操作するのは単にメソッドを呼び出すだけ。プロセスを跨ごうとすると,とたんにソケットだの何だのと,オブジェクトを分解して送信し,受信したら再びオブジェクトに復元しないといけなくなる。ネットワークの先にあるオブジェクトに直接アクセスできたらいいのに,と思う。これができるのが分散オブジェクトの嬉しさ。
分散オブジェクトの構図を図にするとこんな感じ。
[クライアント] [オブジェクト実装] | (オブジェクトサーバー) | | [スタブ] [スケルトン] | | [Object Request Broker (ORB)] [ORB] | [ネットワーク]-----[リクエスト]----->
ネットワーク向こうにあるオブジェクト(リモートオブジェクト)を操作したいクライアントと,オブジェクトを実装するオブジェクト・サーバーとがある。
Object Request Broker (ORB) が仲介する。ORBは,クライアントではスタブを,サーバーではスケルトンを生成する。スタブへのアクセスが発生すると,ORBはメソッド呼び出しを直列化してサーバーへ送る。
オブジェクト自体をやり取りするときも,ORBが適切に変換・復元してくれる。
まずはネットワークを介しないスクリプトを書いてみる。
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を利用して,ネットワークを介して呼び出すようにする。
まずはサーバー。
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()
次はクライアント。
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インスタンス自体をクライアントに渡したい。
まずはサーバー。
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インスタンスを生成し,それを返す。
で,クライアント。
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で済ませたい。
目的のリモートオブジェクトを生成するためだけのリモートオブジェクトを用意し,これを窓口にする。
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すると,これらのクラスのインスタンスはリモートオブジェクトとして扱われる。
次に,クライアント。
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メソッドなどを呼び出し,リモートオブジェクトへのスタブを取得する。あとは,それぞれのオブジェクトを普通に扱える。
この例では,毎回リモートオブジェクトを生成しているが,プールしておいて,クライアントから呼び出されたときにそれを返せば,永続的なリモートオブジェクトも簡単に作れる。
今度は,複数のサーバーを用意して,クライアントからはどのサーバーがオブジェクトを提供しているか意識せずにリモートオブジェクトを扱えるようにしてみる。
リモートオブジェクトを提供するサーバーを分散することで,一台当たりの負荷を低くしたり,サービスを停止することなくサーバーを入れ替えることができるかもしれない。
目的のオブジェクトを提供するサーバーとは別に,どのリモートオブジェクトがどのサーバーで動いているかを管理する,ネームサービスオブジェクトを用意する。
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を流用する。
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オブジェクトを取得し,オブジェクトを登録する。
次はクライアント。
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サーバーとの交信は,大きな負荷とはならない。
実用化するには,成りすましを避ける工夫が必要。