第11章 分布式Ruby
分布式技术主要是为了满足单台计算机的计算能力不足、而又无法使用超级计算机(多CPU和TB级别内存)的情况,它通过多台计算机的累加,把计算量和数据分布到每一台机器上,以达到超级计算机的效果。
比如在计算PageRank时,100亿个网页链接的ID号组成的稀疏矩阵,将会消耗超过TB的内存,普通计算机根本无法完成它的计算量。这时,通过几千甚至上万台机器一起运算,把运算量分散到多台机器上,可以完成超级计算机才能够完成的计算任务。本章主要介绍Ruby的分布式技术和它的相关技术。
11.1 概述
分布式应用是在由通信网络上互联的多台服务器上,共同完成一个任务的应用,分布式Ruby(或者说DRb,druby)是一个完全由Ruby编写的分布式应用开发框架,它诞生于1999年,支持druby和drbunix内部协议。
通过把运算任务分散到多台计算机,可以实现多台计算机一起计算,使计算速度得以线性提高。而分布式Ruby技术,正是可以搭建这种分布式计算的平台。
分布式Ruby和RPC、CORBA、Java RMI、SOAP以及DCOM等远端调用和分布式技术类似,通过DRb技术,开发者可以非常迅速地给客户提供一个分布式计算的解决方案。DRb技术以对象的深度序列化技术(Marshal)为基础,不但支持简单对象的网络传递,更可以做到面向目的编程。DRb拥有以下优势:
l DRb是简洁的。搭建DRb分布式解决方案,一般只是搭建一个CORBA或SOAP平台应用的几分之一甚至十几分之一。
l DRb支持动态调整。通过修改Ruby的部分源文件,可以在运行时改变部分服务的实现细节。
l DRb隐藏了调度和简单对象持久化的技术细节。通过DRb编写应用解决方案可以将对象传递的顾虑降至最小。
l DRb不畏惧未知的潜在需求。通过eval功能,使任何应用的开发和部署都变得简洁快速。
l DRb采用了Ruby语言,能充分发挥Ruby的强大特性。
Ruby强大的文本处理能力和DRb强大敏捷的分布式开发能力,使Ruby可以被快速地应用在各个领域。可以说,分布式Ruby是Ruby语言最强大的杀手级应用(Kill Application)之一。
本章通过介绍循序渐进的方式介绍分布式Ruby和它相关的极其重要的技术――Marshal(持久化)。
11.2 简单的DRb例程
下面的例子演示了一个简单的分布式Ruby的应用。在这个简单的例程中,可以通过打开多个DRbClient程序连接到DRb服务器上,并且能够调用服务器提供者ServObj的execute方法并取得它的返回值。
1.DRb服务器
下面的代码通过DRb模块的start_service方法,启动了一个分布式系统服务器处理系统。该系统通过ServObj提供服务。
代码11.1 简单的DRb例程:
#!/usr/bin/ruby
#DRb例程
require 'drb/drb'
class HelloDRbServer
def execute
"Hello, Distributed Ruby"
end
end
#创建为DRb服务器提供服务的对象
ServObj = HelloDRbServer.new
#使用创建的ServObj对象作为DRb服务提供者
DRb.start_service('druby://localhost:8888', ServObj)
DRb.thread.join
2.DRb客户端
下面的代码通过DRbObject对象连接到DRb服务器,生成DRbObject对象实例。DRbObject的对象实例可以执行任意ServObj的方法。
代码11.2 DRb客户端:
require 'drb'
#连接DRb服务器,采用的是druby协议
obj = DRbObject.new(nil, 'druby://localhost:8888')
p obj.execute
客户端连接到DRb服务器,得到分布式Ruby服务实体对象DRbObject实例obj,通过obj调用类HelloDRbServer的execute方法,得到执行结果。
"Hello, Distributed Ruby"
在上面的例子中,DRb服务器将HelloDRbServer对象的实例注册为分布式Ruby服务器的服务实体。DRbObject的第一参数nil表示要注册一个新的DRbObject,也可以注册以前生成的DRbObject。
分布式Ruby通过启动本地的分布式服务器,隐藏了服务对象实体和外部的交互,方法的参数和返回值都是用marshalled和unmarshalled方法传递的,但是这些细节都由druby协议隐藏了起来,对程序员完全透明。DRb库提供了以下几个部分:
l drb DRb的主程序。
l acl Access control list。
l Rinda tuple-space 分布式工作管理系统,Linda的Ruby版本。
11.3 对象的持久化(Marshal)
持久化又称序列化或对象的序列化(Serialization)。所谓对象的持久化,是保存和恢复对象状态的一种方式。一般,程序在运行时产生的对象,会随着程序的关闭而被释放,但如果想把运行期对象的特性保存下来,在程序终止运行后,尽管这些对象实例被释放了,但是仍然可以通过在程序中重新加载这些特性,从而将这些对象实例还原成原来的状态,以进行场景重现。
11.3.1 什么是持久化
实际上,持久化技术的历史远远比Java早。从最古老的游戏保存记录,到目前网络游戏中的保存人物状态,都是持久化技术的一种体现。
持久化技术在众多领域都有广泛的应用,其中最常见的就是图形图像软件。图形软件的一个很重要功能就是场景重现,Photoshop和AutoCAD等软件将用户的操作保存到硬盘中,在用户下一次加载这些工程文件时,工程会还原每一个图形元件所在的位置以及用户的操作顺序。一个支持序列化的对象必须支持以下特征:
l 持久化接口必须是虚接口。
l 没有成员变量的对象都支持持久化。
l 持久化对象必须支持数据的序列化导入和导出。
l 如果一个对象支持持久化,那么这个对象的每一个祖先必须支持持久化。
l 如果持久化类的子类不支持持久化,那么该子类的所有子孙都不支持持久化。
11.3.2 Marshal技术
Ruby通过Marshal层和对象层的序列化方法接口实现数据持久化。
对象层通过实例方法_dump(转储)和类方法_load(加载)实现对象的还原和场景重现。
l _dump obj._dump(depth)返回一个字符串,这个字符串包含了能够还原这个对象的所有信息,这里的depth是序列化深度。
l _load ClassName._load(string)读取_dump输出的字符串,返回一个重新生成的对象,前提是解释器能够找到这个对象的定义,ClassName是它的类名。
Marshal层提供了两个类方法:类方法dump(转储)和类方法load(加载),用于序列化一个对象和所有它的派生子类。
l dump Marshal.dump(obj, obj, depth=-1 )返回一个io对象或数据,这些数据包括obj的所有亲属的信息,可以将对象还原。
l load Marshal.load( data) -> obj通过dump出的数据还原对象。
可以认为是Marshal层的持久化方法支配对象层的持久化接口实现了Ruby对象的持久化操作,Marsha1技术的简单实例如下所示。
代码11.3 Marshal技术:
#!/usr/bin/ruby
#Marshal技术演示
class Foo
def initialize(str)
@hello = str
end
def Hello
@hello
end
end
#下面代码对对象实例进行序列化
obj = Foo.new("Hello World/n")
data = Marshal.dump(obj)
obj = Marshal.load(data)
obj.Hello >>Hello World
这段代码通过Marshal的dump方法导出对象实例obj的持久化数据,并且通过Marshal的load方法,根据持久化的数据,重新还原出了一个Foo的对象实例。它和obj实例在任何性质上都是相同的。
Marshal还提供了一个名为restore的类方法,它和类方法load的使用方法和功能都一样。不过Marshal序列化技术是有局限性的,如果一个对象需要被还原,那么解释器必须能够找到这个对象的定义。在下面的例子中便演示了如果没有找到一个对象的定义,Marshal技术的执行结果。
例子中有两个文件,其中,文件marshalsave.rb用于保存对象序列化的数据,文件marshalload.rb用于将对象还原。
代码11.4 Marshal技术-保存对象序列化数据marshalsave.rb:
#!/usr/bin/ruby
#marshalsave.rb
#Marshal技术例程,保存对象
class Foo
def initialize(str)
@hello = str
end
def Hello
@hello
end
end
#下面对对象实例进行序列化
obj = Foo.new("Hello World/n")
data = Marshal.dump(obj)
File.new('testfile','w+') do |f|
f.write(data)
end
代码11.5 Marshal技术-场景还原marshalload.rb:
#!/usr/bin/ruby
#marshalload.rb
#Marshal技术例程,还原对象
$data = ''
File.new('testfile') do |f|
$data = f.read
end
obj = Marshal.load($data) >> undefined method `Foo' for class `Object' (NameError)
p obj.Hello
代码11.5的实例中,用于还原数据的Marshal操作在解释器无法发现类名“Foo”之后,报告了undefined method `Foo' for class `Object'(NameError)错误。说明解释器无法从“类常量表”中找到“Foo”这个类的定义,因此无法对$data进行场景重现。
同时,解释器找到的这个对象的类的定义必须和原来的定义相同,否则还原出的对象将不是原来保存的对象。下面的示例通过重新定义“Foo”类,将一个来自Lucy的问候,变成了来自Lily的问候。
代码11.6 Marshal技术―伪重现:
#!/usr/bin/ruby
#错误的还原对象的Marshal技术例程
class Foo
def initialize(str)
@hello = str
end
def Hello
'I am '+@hello
end
end
obj = Foo.new("Lucy")
data = Marshal.dump(obj)
obj = Marshal.load(data)
p obj.Hello >>I am Lucy
#这里,我们重新定义了Foo
class Foo
undef Hello
def Hello
@hello = 'Lily'
'I am '+@hello
end
end
#下面发现,这个名叫Lucy的女孩子变成了Lily
obj2 = Marshal.load(data)
p obj2.Hello >>I am Lily
11.3.3 实现对象的序列化
本节的例子展示了如何通过类方法_dump和_load实现对象的序列化。在下面的例子中可以看到,类方法_dump和_load被定义后并没有显式调用,它们只负责将对象自己的属性加载或导出。而外围操作都由Marshal模块的dump和load方法负责实现。
代码11.7 _dump和_load方法示例:
#!/usr/bin/ruby
#定义对象的序列化方法
class Foo
def initialize(valueouter)
@valueouter = valueouter
@valueinner = " Great "
end
def _dump(depth)
@valueouter.to_str
end
def Foo._load(str)
result = Foo.new(str);
end
def to_s
@valueouter + ", "+@valueinner + " ! "
end
end
#dump和load只由Marshal类操作
a = Foo.new("Hello, World")
data = Marshal.dump(a)
obj = Marshal.load(data)
puts obj
这段程序的输出结果如下所示。
Hello, World, Great !
11.4 DRb对象
11.4.1 DRb对象
DRuby对象分为DRb::DRbObject和DRb::DRbServer。其中:
DRb::DRbServer在服务器端用于建立服务进程,处理客户端的请求。它把客户端出来的网络包转化称为对提供服务的ServerObject的方法调用,再把返回值传回给客户端。下面的代码就是用来建立一个利用ServObj作为前端对象(front object)提供服务的分布式Ruby服务器,'druby://localhost:8888'是这个服务器在网络上的URI标识。
DRb.start_service('druby://localhost:8888', ServObj)
DRb::DRbObject用于从远端服务器获取对象的引用。DRbObject通过服务器的URI连接到druby服务器,得到面向DRbServer的前端对象,进而得到针对ServObj的引用,DRbObject对象的方法如下:
obj = DRbObject.new(nil, 'druby://localhost:8888')
11.4.2 参数的传递
客户端调用ServObj的方法大都和在本地端调用一样,只需要注意ServObj的方法名称尽量不要和DRb::DRbObject的方法重名(重名时它会先调用本地的方法)。任何的基本类型对象都可以被当做参数或者返回值,它们在客户端被序列化,然后在服务器端被还原。
基于这样的原则,11.2节的例子可以被更改成如下代码的形式,其中,方法execute可以传递方法参数。
代码11.8 简单参数传递-服务器端:
#!/usr/bin/ruby
#参数传递例程的服务器端
require 'drb/drb'
class HelloDRbServer
def execute(t)
"Hello, Distributed Ruby at :" + t.to_s
end
end
ServObj = HelloDRbServer.new
DRb.start_service('druby://localhost:8888', ServObj)
DRb.thread.join
代码11.9 简单参数传递-客户端:
#!/usr/bin/ruby
#参数传递例程的客户端
require 'drb'
obj = DRbObject.new(nil, 'druby://localhost:8888')
p obj.execute(Time.now)
这段程序的输出结果如下所示,客户端程序可以打印出服务器端程序执行之后的返回值。
"Hello, Distributed Ruby at :Sun Jun 09 22:36:14 China Standard Time 2006"
对于那些不能被自动序列化的复杂对象,被传递的参数是它的引用。例如,一个新建的复杂类,客户端的DRb::DRbObject不知道如何对它进行序列化,那么在调用远端方法处理这个对象时,就会产生错误或异常。
代码11.10 复杂参数传递-服务器端:
#!/usr/bin/ruby
#未定义的复杂对象传递演示
#服务器端
require 'drb'
$SAFE = 1
class DRbEx
def initialize
@hello = 'hello'
end
def hello
@hello + ' '+Time.now.to_s
end
def returnobj(obj)
obj.time
end
end
if __FILE__ == $0
DRb.start_service('druby://localhost:8886', DRbEx.new)
puts DRb.uri
DRb.thread.join
end
代码11.11 复杂参数传递-客户端:
#!/usr/bin/ruby
#未定义的复杂对象传递
#客户端
require 'drb'
class NewObject
attr :starttime ,:time
def initailize
@starttime=Time.now
end
def get_localtime
@time = Time.now
@time
end
end
#新建一个客户端
ro = DRbObject::new_with_uri("druby://localhost:8886")
p ro.hello
p ro.returnobj(NewObject.new)
这段程序的输出结果如下所示。
"hello Sun Jun 09 21:43:40 China Standard Time 2006"
(druby://localhost:8886) drbs-acl.rb:15:in `returnobj': undefined method `time' for #<DRb::DRbUnknown:0x2d0dba8> (NoMethodError)
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1552:in `perform_without_block'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1512:in `perform'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1586:in `main_loop'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1582:in `main_loop'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1578:in `main_loop'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1427:in `run'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1424:in `run'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1344:in `initialize'
from (druby://localhost:8886) c:/ruby/lib/ruby/1.8/drb/drb.rb:1624:in `start_service'
from (druby://localhost:8886) drbs-acl.rb:21
from testcl.rb:14
在服务器端不知道客户端传递来的引用是哪一种类型时,上面这种情况的出现是无法解决的,因此应当尽量避免传递客户端的对象引用给服务器端。
如果必须需要传递这种未定义的对象给服务器,并且让服务器能够识别这种对象,可以让服务器在服务器端打开eval权限,并且将这种新对象的代码执行一遍,使服务器可以识别这种对象。
11.4.3 复杂对象的传递
DRb服务器对于传递过来的参数使用Marshal技术进行序列化场景还原时,必须能够在自己的类常量表中找到传递过来的对象的类定义。因此必须在服务器和客户端都对该对象进行定义。
例如,在下面的例子中,在服务器端拷贝了NewObject的定义,使服务器端的解释器可以找到NewObject的类常量定义,从而可以对NewObject的实例进行还原。
代码11.12 复杂对象传递-服务器端:
#!/usr/bin/ruby
#复杂对象传递,服务器端
require 'drb'
class NewObject
def initialize(valueouter)
@valueouter = valueouter
@valueinner = "Greet"
end
def _dump(depth)
@valueouter.to_str
end
def self._load(str)
result = new(str);
end
def to_s
@valueouter + ' and ' +@valueinner
end
end
#$SAFE=1
def outeval(codes)
eval(codes)
end
class DRbEx
def initialize
@hello = 'hello'
end
def hello
@hello + ' '+Time.now.to_s
end
def returnobj(obj)
Marshal.load(obj).to_s
end
def evalcode(codes)
outeval(codes)
end
end
if __FILE__ == $0
DRb.start_service('druby://localhost:8886', DRbEx.new)
puts DRb.uri
DRb.thread.join
end
代码11.13 复杂对象传递-客户端:
#!/usr/bin/ruby
#复杂对象传递,客户端
require 'drb'
class NewObject
def initialize(valueouter)
@valueouter = valueouter
@valueinner = "Greet"
end
def _dump(depth)
@valueouter.to_str
end
def self._load(str)
result = new(str);
end
def to_s
@valueouter + ' and ' +@valueinner
end
end
ro = DRbObject::new_with_uri("druby://localhost:8886")
p ro.hello
ro.evalcode(codes)
p ro.returnobj(Marshal.dump(NewObject.new('localhost')))
11.4.4 未知对象的传递
在一般语言中,复杂对象的传递可以通过定义标准的序列化方式、以及建立分布式节点之间相关约定的机制来实现。但是未知的复杂对象的传递,对于其他所有语言都是一个很难解决的问题,而Ruby的解释型动态语言特性,使这个问题变得极为简单。
未知对象的传递必须遵循几个原则:
l 对象必须支持持久化。
l 必须能够找到机制让服务器能够识别该对象。
l 尽量不要定义和服务器现存对象同名对象。
在纯解释型动态语言中,让服务器能够识别该对象比较简单,Ruby中的eval方法和load加载机制都可以实现。
l Eval机制 通过HERE文档或文件IO将对象定义文件以字符串的形式传过去,通过调用Kernel::eval执行这段字符串在服务器端生成对象的定义。
l Load机制 将传送过去的对象定义文件存放到特定目录,使用Kernel::load方法加载该文件以生成对象的定义。
相比而言,load机制更加安全,而eval机制则更加灵活。在使用eval机制时候有一个限制:执行eval的代码必须在服务器类的定义以外,而且是以全局函数的形式出现,否则Marshal模块将无法对该对象进行序列化。因为,在服务器类中执行eval,会导致对象的名字空间会限定于服务器类之内。如下面的代码中,NewObject类将被定义成为DRbEx::NewObject。
下面的示例展示了如何通过eval方法在服务器和客户端之间传递未知对象。它采用HERE文档的方式传递对象定义,在服务器端通过全局方法outeval执行,并生成该对象的定义。
代码11.14 未知对象传递-服务器端:
#!/usr/bin/ruby
require 'drb'
#$SAFE=1
#在服务器端实现未知对象功能的方法
def outeval(codes)
eval(codes)
end
class DRbEx
def initialize
@hello = 'hello'
end
def hello
@hello + ' '+Time.now.to_s
end
def returnobj(obj)
Marshal.load(obj).to_s
end
def evalcode(codes)
outeval(codes)
end
end
if __FILE__ == $0
DRb.start_service('druby://localhost:8886', DRbEx.new)
puts DRb.uri
DRb.thread.join
end
代码11.15 未知对象传递-客户端:
#!/usr/bin/ruby
require 'drb'
class NewObject
def initialize(valueouter)
@valueouter = valueouter
@valueinner = "Greet"
end
def _dump(depth)
@valueouter.to_str
end
def self._load(str)
result = new(str);
end
def to_s
@valueouter + ' and ' +@valueinner
end
end
#通过here文档将类的定义代码保存
codes = <<HERE
class NewObject
def initialize(valueouter)
@valueouter = valueouter
@valueinner = "Greet"
end
def _dump(depth)
@valueouter.to_str
end
def self._load(str)
result = new(str);
end
def to_s
@valueouter + ' and ' +@valueinner
end
end
HERE
ro = DRbObject::new_with_uri("druby://localhost:8886")
p ro.hello
#将NewObject的定义传送到服务器端
#调用服务器端的evalcode方法
ro.evalcode(codes)
p ro.returnobj(Marshal.dump(NewObject.new('localhost'))) >>localhost and Greet
11.5 DRb安全
分布式Ruby通过设定$SAFE安全级别和ACL(访问控制列表)来实现远程访问的安全。
l $SAFE Ruby的默认保留全局变量,用于控制被污染线程或类的访问权限。
l ACL(访问控制列表) DRb的开发者专门为DRb设计的访问控制组件,它位于drb的安装目录。
11.5.1 屏蔽eval操作
默认情况下,面向外部的DRb服务允许使用服务对象的所有方法,这就意味着eval之类的方法被执行是可能的,这就产生了相当大的隐患。例如,以下代码是不安全的。为了更有效地理解DRb安全,下面先演示客户端的代码。
代码11.16 eval危害演示-客户端:
#!/usr/bin/ruby
require 'drb/drb'
ro = DRbObject.new(nil, "druby://localhost:8887")
class << ro
undef :instance_eval # force call to be passed to remote object
end
ro.instance_eval("`notepad.exe`")
代码11.17 eval危害演示-服务器端:
#!/usr/bin/ruby
require 'drb/drb'
#如果需要屏蔽eval,则设置$SAFE=1
#$SAFE=1
# The URI for the server to connect to
URI="druby://localhost:8887"
class TimeServer
def get_current_time
return Time.now
end
end
# The object that handles requests on the server
FRONT_OBJECT=TimeServer.new
DRb.start_service(URI, FRONT_OBJECT)
# Wait for the drb server thread to finish before exiting.
DRb.thread.join
这段代码可以让服务器启动NotePad.exe,可以使DRb的远程对象打开写字板程序。
在客户端中,客户机通过undef方法取消本地的instance_eval方法,强制使DRbObject对象调用远程服务器的instance_eval。代码如下所示:
class << ro
undef :instance_eval #强制调用被传送到ro中
end
如果远程对象是Unix,那么便可以很轻易地使用unlink等命令来删除Unix主机的文件。
通过设置$SAFE=1可以避免这种情况,但$SAFE=1会取消所有的eval相关操作。$SAFE操作主要可以禁止被污染(taint)的类或方法,共分5个级别,其高级应用不属于本书讲解范围,有兴趣的读者可以查阅相关资料。
11.5.2 访问控制列表
ACL(访问控制列表)类是用于DRb访问控制的模块,主要用于设置DRb的访问控制列表和一些权限。下面的例子对对acl.rb的使用进行简要的介绍。
1.服务器端
l 拒绝所有网段的连接请求。
l 允许本地主机,192.168.0.*和192.168.1.*网段连接druby服务器。
l 设置安全标志$SAFE=1,以禁止eval及其相关功能。
l 提供hello、sample和test?方法以响应客户端请求。
下面的代码是一个通过带有访问列表的DRb服务器应用示例,通过构筑和安装访问控制列表。
#配置访问控制列表
acl = ACL.new(%w(deny all
allow 192.168.1.*
allow 192.168.0.*
allow localhost))
#安装访问控制列表
DRb.install_acl(acl)
通过安装访问控制列表,DRb服务器拥有了对ACL中所注册的IP的访问控制权限,它可以允许和拒绝某些IP的连接请求,访问控制列表程序示例如下。
代码11.18 访问控制列表-服务器端:
#!/usr/bin/ruby
#服务器端
require 'drb'
require 'drb/acl'
#设置安全级别
$SAFE = 1
class AclDRb
def initialize
@hello = 'hello'
end
def hello
@hello + ' '+Time.now.to_s
end
def sample(a, b, c)
a.to_i + b.to_i + c.to_i
end
def test?
info = Thread.current['DRb']
p info['client'].peeraddr if info
info['client'].peeraddr
end
end
if __FILE__ == $0
#配置访问控制列表
acl = ACL.new(%w(deny all
allow 192.168.1.*
allow 192.168.0.*
allow localhost))
#安装访问控制列表
DRb.install_acl(acl)
DRb.start_service('druby://localhost:8886', AclDRb.new)
puts DRb.uri
DRb.thread.join
end
2.客户端
l 能够连接上服务器。
l 能够调用服务器端的hello方法返回一个“hello”字符串以测试服务器端是否能运行。
l 通过test?方法希望服务器能打印出其client socket的信息。
代码11.19 访问控制列表-客户端:
#!/usr/bin/ruby
#客户端
require 'drb'
ro = DRbObject::new_with_uri("druby://localhost:8886")
p ro.hello
p ro.test?
由于本例是在本机运行,因此不会受到访问控制列表的影响,如果需要测试,需要把上例中的localhost改为实际IP。
11.6 分布式应用示例
11.6.1 分布式开发背景
本节通过实例介绍DRb的实际分布式应用,分布式系统就原型来说差异并不大,使用CORBA标准可以搭建分布式系统,使用SOAP也可以,而通过DRb则是最简单的一种方法。
它不同于CORBA和SOAP之类的标准协议,DRb只能用于Ruby。Ruby也提供了CORBA和SOAP支持,但如果没有特殊需要,这些协议在用于DRb技术的平台上并无实际意义,因为使用Ruby进行跨平台的开发已经很简单了。
Google是世界上分布式计算最出名的公司,它通过搭建数十万台如图11.1所示的廉价个人PC,组成了世界上最大的分布式计算机集群,这些计算机组成的是一个完整的分布式文件系统,使它的业务处理速度非常快。这种分布式计算机集群的一个重要优势在于,计算和服务的分布化使单个PC的损毁对于整体的服务不会造成太大影响。
11.6.2 分布式集群原理
图11.1展示了一个小型的分布式应用的原型。
图11.1 分布式应用框架
这个应用框架实现了以下功能:
1.服务器端所提供的服务
l 启动ServObj提供分布式Ruby服务
l 读取需要处理的节点
l 返回给客户端未处理的节点信息
l 通过recalc提供异常处理恢复
l 打印日志
2.客户端执行任务的流程
(1)连接DRb服务器。
(2)得到一个任务节点号,如果节点号小于0则退出。
(3)通过这个任务节点号,从存储器得到需要处理的数据并且进行处理。
(4)处理完成并且将事务提交之后,向服务器提交确认信息,通知服务器从挂起任务节点号表中删除这个节点。
(5)通知服务器打印日志。
(6)跳至步骤2。
11.6.3 分布式应用框架
本节介绍的分布式应用框架是分布式应用的基础框架,该框架主要分为3个模块,即drbserver.rb(计算服务调度者)、drbclient.rb(计算能力提供者)和analyze.rb(分析模块)。
1.服务器计算调度者
计算服务调度者提供DRb服务和向节点计算机提供待处理的计算节点。
代码11.20 分布式应用实例-服务器:
#!/usr/bin/ruby
require 'drb'
#超时时间,如果一个节点被计算了2小时仍然没返回,则认为它已经死亡
timeout=7200
class ServObj
attr_accessor :nodes,
:usingnodes
#初始化服务器对象,读取待处理的节点
def initialize
@nodes=[]
readnode
@hashused={}
end
#从配置文件读取节点信息
def readnode
open("#{Dir.pwd}/nodes.config") do |f|
@nodes = f.read().split(' ')
end
end
#把未处理完的节点保存回配置文件
def savenode
open("#{Dir.pwd}/nodes.config",'w+') do |f|
f.write(@nodes.join(' '))
f.flush
f.close
end
end
#读取下一个空闲节点
def get_nextnode
nid = @nodes.shift
if nid
@hashused[nid] = Time.now
nid
else
-1
end
end
#某个节点的操作工作已经完成
def finish(nid)
if @hashused.has_key?(nid)
@hashused.delete(nid)
arr =[]
arr += @nodes
arr += @hashused.keys
arr.sort
else
@nodes.delete(nid)
end
end
#提交给本机监控程序
#判断是否可以关闭服务器
#如果运算完毕,则关闭服务器
def needclose?
p @nodes
p @hashused
if @nodes.empty? && @hashused.empty?
true
else
false
end
end
#由于部分节点的操作可能因为客户机以外而终结
#recalc对这些未完成的事务进行部分的保证
def recalc
@hashused.delete_if do |key,value|
if key-value > timeout
@nodes << key
end
key-value > timeout
end
end
#日志打印
def log(message)
File.open("#{Dir.pwd}/log.log", "a") do |f|
f.write("#{Time.now}: #{@name}: #{message}")
end
end
def get_time
Time.now.strftime("%Y-%m-%d %H:%M:%S")
end
end
if __FILE__ == $0
serv = DRb.start_service("druby://localhost:2222", ServObj.new)
Thread.new{
serv.thread.join
}
#定时轮循,看是否可以关闭服务
Thread.new{
while true do
if serv.front.needclose?
serv.stop_service
break
end
sleep 3
end
}.join
end
2.客户端计算能力提供者
计算能力提供者提供计算能力的客户端。下面是它的实现代码。
代码11.21 分布式应用实例-客户端:
#!/usr/bin/ruby
require 'drb'
require 'analyze'
if __FILE__ == $0
DRb.start_service
dServ= DRbObject.new(nil, "druby://localhost:8880")
p dServ.class
begin
while true
nid = dServ.get_nextnode
if nid.to_i > 0
AnalyzeData(nid)
dServ.finish(nid)
log= "task #{nid} finished at server time:#{dServ.get_time}" <<"/n"
dServ.log(log)
$stdout << log
else
break
end
end
rescue DRb::DRbError =>e
puts "a serious error accur"
puts "error string:/n#{e.message.class}"
system("pause")
ensure
end
end
3.数据处理模块
分析模块进行数据分析和处理的模块。本例中的分析只进行了简单地打印字符串,并且使主线程休眠1秒钟的操作,这样是为了能够让两个客户机的操作顺序更加明显。
代码11.22 分布式应用实例-客户端分析模块:
#!/usr/bin/ruby
def AnalyzeData(nid)
puts 'analyze...'
sleep 1
end
if __FILE__ == $0
AnalyzeData(0)
end
图11.2~图11.4列出了程序执行情况。
图11.2 drbserv.rb的运算 图11.3 drbclient.rb的第1个实例
图11.4 drbclient.rb的第2个实例
日志文件“log.log”列出了该应用所有任务的执行情况。
Tue Jun 11 23:03:59 China Standard Time 2006: : task 1 finished at server time:2006-07-11 23:03:59
Tue Jun 11 23:04:00 China Standard Time 2006: : task 2 finished at server time:2006-07-11 23:04:00
Tue Jun 11 23:04:00 China Standard Time 2006: : task 3 finished at server time:2006-07-11 23:04:00
Tue Jun 11 23:04:01 China Standard Time 2006: : task 4 finished at server time:2006-07-11 23:04:01
Tue Jun 11 23:04:01 China Standard Time 2006: : task 5 finished at server time:2006-07-11 23:04:01
………….
Tue Jun 11 23:04:08 China Standard Time 2006: : task 17 finished at server time:2006-07-11 23:04:08
Tue Jun 11 23:04:08 China Standard Time 2006: : task 18 finished at server time:2006-07-11 23:04:08
Tue Jun 11 23:04:09 China Standard Time 2006: : task 19 finished at server time:2006-07-11 23:04:09
Tue Jun 11 23:04:09 China Standard Time 2006: : task 20 finished at server time:2006-07-11 23:04:09