Ruby 分布式 Ruby 解决之道 - DRb

lyfi2003 · 2012年09月15日 · 最后由 Mark24 回复于 2022年01月04日 · 16093 次阅读
本帖已被管理员设置为精华贴

分布式 Ruby 解决之道

其实用 Druby 很久了,今天需要完成一个进程数据同步的机制,我需要的不是运行速度快,不是用 linux / mac 下的扩展,而是独立,快速开发效率,方便最简单的 Ruby 环境可运行,可以吗?DRb(即分布式 Ruby,下面都这样说它) 是内置于 Ruby 标准库中的对象代理的实现。什么是对象代理,现在不明白不要紧,一会就知道了。

解决什么样的问题?

有的时候,我们需要提供远程的服务,比如提供远程 API 调用(如果你听过 RPC,或 WDSL),这样,我们可以很大程度上解耦各大模块,对外提供服务。

还有的时候,我们需要在两个进程中通信,以获得互相的同步或资源。

更有,我想实现实现某种透明的对象,让对象可以在不同的进程或主机上传递。

这些,都可以通过 DRb 来实现。DRb 的相关文档非常少,但在想快速实现一个轻量级分布应用,依赖最少化时,使用它是非常方便的。我对分布式的研究不多,欢迎各位看了本文后能提出更多解决方案。

使用方法

依官方的例子为主,各位看官建议看的时候复制下试试。因为是分布式解决方案,肯定是 服务端客户端 双方的代码。

  1. 简单的例子

* 服务端

# ==== 服务端代码,保存为 timer_server.rb
#
require 'drb/drb'

# 监听的地址,你可以改为 0.0.0.0 来支持远程连接
URI="druby://localhost:8787"

class TimeServer

  def get_current_time
    return Time.now
  end

end

# 被代理的对象,客户端获取的到的对象就是它
FRONT_OBJECT=TimeServer.new

DRb.start_service(URI, FRONT_OBJECT)
# 
DRb.thread.join

* 客户端

# ==== timer_client.rb

require 'drb/drb'

SERVER_URI="druby://localhost:8787"

# 这句是必要的,因为我们很快会用到回调与引用,一会说。
# 所以纯粹的客户端是不存在的。
DRb.start_service

timeserver = DRbObject.new_with_uri(SERVER_URI)
puts timeserver.get_current_time

我必须要说的是,很符合我们的 C/S 模型,但是你有没有想过如果 get_current_time 返回一个远程对象,会发 生什么呢?接下来,就是我要讲的。

  1. 远程对象代理

* 服务端

require 'drb/drb'

URI="druby://localhost:8787"

class Logger

  # Logger 是被远程代理,客户端不会存在,所以用这句
  include DRb::DRbUndumped

  def initialize(n, fname)
    @name = n
    @filename = fname
  end

  def log(message)
    File.open(@filename, "a") do |f|
      f.puts("#{Time.now}: #{@name}: #{message}")
    end
  end

end

class LoggerFactory

  def initialize(bdir)
    @basedir = bdir
    @loggers = {}
  end

  def get_logger(name)
    if !@loggers.has_key? name
      # 保证文件名是合法的
      fname = name.gsub(/[.\/]/, "_").untaint
      @loggers[name] = Logger.new(name, @basedir + "/" + fname)
    end
    return @loggers[name]
  end

end
# 在执行之前你要手动创建一下dlog
FRONT_OBJECT=LoggerFactory.new("dlog")

DRb.start_service(URI, FRONT_OBJECT)
DRb.thread.join
  • 客户端

    require 'drb/drb'
    
    SERVER_URI="druby://localhost:8787"
    
    DRb.start_service
    
    log_service=DRbObject.new_with_uri(SERVER_URI)
    
    ["loga", "logb", "logc"].each do |logname|
    
      logger=log_service.get_logger(logname)
    
      logger.log("Hello, world!")
      logger.log("Goodbye, world!")
      logger.log("=== EOT ===")
    
    end
    

    吐嘈,执行完,你会发现日志被写在了服务端的 dlog/ 目录里,注意 DRb::DRbUndumpedLogger 对象的加载,这样的对象是无须传递给客户端的,这样,客户端代码里拿到的 loggger 对象是远程代理对象,所有该对象调用的方法实际上是在远程服务端执行的。我们称这种方法是按引用传递。

    那当然有一种传递叫,按值传递,什么情况是呢?显然,上面第一种方法即是,我们调用 get_current_time 是本地对象,再调用该对象的方法时,方法在本地执行。

    如此,便是 DRb 的基本使用方法了,应该说不难理解。你可以这样理解,都是对象,只不是有些对象是远程的,有些是本地的,远程的对象方法的执行是在远端,本地的方法是在本地。远程的对象是包含了 DRb::DRbUpdumped 的对象。不包含的都会转换为本地对象。

    那么,何为分布式的 Ruby,这明显是忽悠我们群众嘛?别急,我正要说,还记得一开始代码里注释的 start_service 了吧。所谓 服务端 可以随时获取 客户端 的远程对象,对吧?所以用 DRb 实现一个通信是非常简单的。为了有深入理解,我想需要将它的实现原理分析一下。

如何实现的呢

DRb 的本质是,一个通信底层,一个序列化方式,一个代理器,OK?你不用看都能知道是吧?因为我也会这样实现的。

  1. 代理器

    method_missing 将一个对象的方法传递给另一个对象的神器,谓之代理,多像有关部门,不做事情,只是将事情移交给另一个有关部门。看看核心代码:

    # drb/drb.rb: 1078 (ruby-1.9.3)
    def method_missing(msg_id, *a, &b)
      if DRb.here?(@uri)
        obj = DRb.to_obj(@ref)
        DRb.current_server.check_insecure_method(obj, msg_id)
        return obj.__send__(msg_id, *a, &b)
      end
    
      succ, result = self.class.with_friend(@uri) do
      DRbConn.open(@uri) do |conn|
        conn.send_message(self, msg_id, a, b)
      end
      #。。。处理异常
    end
    

    obj 显然是被代理的对象,上面除了缓存机制外,send_messagemethod_missing做的最重要的事,它引出来了下面的事情。

  2. 通信底层

    DRb 的底层是一层透明的传输协议,通过它的接口,可以将数据(或命令)无压力收取,且看它的关键接口:

    # drb/drb.rb:728 打开一个连接
    def open(uri, config, first=true)
      @protocol.each do |prot|
        begin
          return prot.open(uri, config)
        rescue DRbBadScheme
        rescue DRbConnError
          raise($!)
        rescue
          raise(DRbConnError, "#{uri} - #{$!.inspect}")
        end
      end
      if first && (config[:auto_load] != false)
        auto_load(uri, config)
        return open(uri, config, false)
      end
      raise DRbBadURI, 'can\'t parse uri:' + uri
    end
    
    # drb/drb.rb:901 发送一个请求,通俗的说,调用一个方法
    def send_request(ref, msg_id, arg, b)
      @msg.send_request(stream, ref, msg_id, arg, b)
    end
    
    # 在服务端,接受一个方法
    def recv_request
      @msg.recv_request(stream)
    end
    
    # 服务端,发送一个结果
    def send_reply(succ, result)
      @msg.send_reply(stream, succ, result)
    end
    
    # 客户端,接受一个结果
    def recv_reply
     @msg.recv_reply(stream)
    end
    

    继续吐嘈,默认 DRb 使用 DRbTCPSocket 来通信,你可以随时调整为 UnixSocket 或者 Http,甚至 SSL。这个视你的需求而定,比如你要从公司用基于 Ruby 的方法,遥控你的家用电脑,建议你使用 SSL。

    抽象你的接口,是实现易于维护系统的关键,是吧。如何序列化是整个 DRb 的关键,而在 Ruby 中,这一切显得如此简单。

  3. 序列化方法(与对象引用转换)

    Marshal 神器用来序列化对象,默认直接使用即可。例如:

    class A
      def initialize(a)
        @a = a
      end
    end
    a = A.new(1)
    b = Marshal.dump(a)
    c = Marshal.load(b)
    puts c.a  # ok, 输出 1
    

    它被引用在 DRb 中,做为 DRbMessage 的关键,传递对象使用。

于是,组合以上思路,DRb 就产生了,不过,我们还缺点什么没讲,作为安全的程序员,一定要看看。

代理对象如果被发送了 instance_eval("rm -rf /") Ok,我们系统没了。。。

所以,$SAFE = 1 是可以保障基本安全的,然而,这还不够,更细的控制,应该由 Ruby 1.9.1 以后(应该是说我没深入研究过)开始的,我就不细说了,你如果有需求可以仔细看看。

另一个问题是,分布式要求远程对象长期生效,那么你可以去研究下 DRb::TimerIdConv 进行生存期保存。

最后一个问题,远程对象支持 block 调用吗?答案是,YES。如何实现的呢?

# drb/invokemethod.rb 
def perform_with_block
  @obj.__send__(@msg_id, *@argv) do |*x|
    jump_error = nil
    begin
      block_value = block_yield(x) #本质是 block.call(*x),只是特殊处理了 Array
    rescue LocalJumpError
      jump_error = $!
    end
    if jump_error
     case jump_error.reason
     when :break
       break(jump_error.exit_value)
     else
       raise jump_error
     end
  end
  block_value
end

看的出来(再吐嘈),block 是通过本地的调用后,将结果再传递给远程对象。详细可以继续看 drb/drb 里的 perform 实现。

值得注意的是,如果一个对象没有 include DRb::DRbUndumped 被返回到客户端,则会抛出 DRbUnknownError 异常。这个很容易理解。另一个注意点是,一个类无法使用 Marshal.dump 时(例如打开了一个文件句柄),则需要想办法自己实现它,或者。。。或者你应该实现为远程代理类,对吧。

好了,基本上都讲完了。代码里还有许多精华,例如 self.allcate 可以跳过 initialize 来创建一个类。

看完后,你再想想开篇的需求是否可以轻松解决掉?实际上只需要几步:

  • 创建一个类,按一般方法编写它的方法。如果方法有返回自定义对象,根据是否远程代理加载 DRbUpdumped

  • 加载 DRb,启动服务。

  • 客户端连接,获取代理对象,调用方法。

与其他语言的解决方案的对比与区别

  • JAVA 的 RMI

RMI 是 JAVA 的远程调用实现方法,这里有一篇不错的介绍:http://damies.iteye.com/blog/51778

DRb 是分布式的,RMI 是单向的 C/S。DRb 不需要声明接口,直接使用。熟练后,可以极快速度完成一个通信和同步的应用。

  • CORBA

看这个:http://zh.wikipedia.org/wiki/CORBA ,基本原理相同,不过 DRb 足够轻,足够快。

  • WDSL

利用 xml 的标准 RPC 调用。适合于静态语言。

由于对其他的了解不深入,欢迎熟悉的看客们提出你的看法。

其他需求

  • 在公司之前的工作时,需要将 JRuby 的对象代理到 Ruby 中,这样可以复用 gems。

  • 需要远程 API 的方法调用另一个进程的所有方法。

因为要代理所有本地不存在的对象,只使用 DRb 还不够。但基本思路很简单,利用一个模块的 const_missing 动态加载远程的对象,而远程对象在创建时均自动加载 DRbUpdumped 被远程代理。根据以上,我们可以写一个看似本地代码却可以轻易转到远程执行。

例如:

# 本地代码
require 'watir'
ie = Watir::IE.new
ie.goto("www.baidu.com") # 本地打开一个浏览器

# 加载为远程进程执行
ATU.require 'watir'
ie = ATU::Watir::IE.new
ie.goto("www.baidu.com") # 远程的进程打开一个浏览器

有了它,几乎同一份代码可以同用两个用途。可以非常方便的以代码级的控制远程主机和对象,并且重用性很高。

如何实现,可以自己想想,同时可以查看这里:ruby_proxy 的实现

还有一篇 slide: http://windy.github.com/ruby_proxy.html

推荐续读

一个让 DRb 真正分布式的 rinda(Dave Thomas)

一些介绍的例子

来自 windy

本文采用 署名 - 非商业 - 复制保留本授权 的方式进行发布。

犀利!谢谢楼主分享!

哇~

为什么好贴总是少有人回复 ....

DRb 小众一点,一般也就是使用的工具中有开启这个功能的,自己编写的不多

补:后来又想了想,可能还是因为我们论坛话题太集中了吧,基本上 rails 上手 + 各种工具 PK

lz 好幽默。。

学习了,收藏啊

很好很强大,这个可以有👍

收藏了,来学习学习

请教一个问题 Rails 如何集成 DRB 我们项目中,在集成 DRB 过程中(将 DRB 的定义卸载 config/initializer/xxx.rb 中) 但是在访问某些 action 的时候,会出现 mysql connect 的异常 移除 DRB 的定义文件后,恢复正常。 你遇到过这种情况么?

#8 楼 @chucai 没有遇到过类似问题,能否将相关的错误贴出来,比如 initializer 里的关键代码。包括你的集成思路

#9 楼 @lyfi2003 好的,先谢谢了。 我详细的说明一下我遇到的问题,如下:

  • 项目需求 我自己写了一个 xmpp_server Gem,功能主要是:连接 xmpp 服务器(openfire),并负责发送消息 到 xmpp 服务器; 使用 DRB 开放一个叫 push 的方法,主要功能是让 Rails 项目能向队列 (Queue) 存放需要 xmpp push 出去的数据 具体的代码如下 https://github.com/chucai/xmpp_server

  • 与 Rails 的整合 新建config/initializers/xmppserver.rb文件

    require 'drb/drb'
    SERVER_URI="druby://localhost:8787"  
    DRb.start_service  
    XMPPSERVER = DRbObject.new_with_uri(SERVER_URI)  
    

    然后在 Rails 项目中可以

    XMPPSERVER.push :username => "zhangsan", :content => "yes, i work."
    
  • 引起的问题 具体问题已经解决,在贴出来,是 Mysql 的异常

  • 现在解决的办法 将出现异常的 Mysql 操作放到 thread 中

    Thread.new {
    @some_model.destroy
    }
    

#10 楼 @chucai 我与我之前的同事有用过 DRb 与 Rails 的整合,但不建议是直接使用。你这里可以简单处理,因为看你的需求应当是类似于 RPC 的方式,所以客户端代码不需要 DRb.start_service ( 这个会打开一个线程监听,与现在的 Rails 单线程处理不对应 ), 你可以去掉试下。

如果还有问题,你再回复我。

#11 楼 @lyfi2003 恩。谢谢 我觉得你说到关键点上了,:-) 我先测试一下 有问题在 call 你

#11 楼 @lyfi2003 移除 start_service 后,无法调用方法了

#13 楼 @chucai 看了你的 xmpp_server 感觉用法存在一些问题导致,在 push.rbXmppServer::QUEUE.push m 后加上 return nil 试下。这样防止返回一个服务端对象。

#14 楼 @lyfi2003 OK, 刚才我又测试了一下,现阶段已经没有问题了。 谢谢了。 呵呵

#15 楼 @chucai Good~ 动手能力很强,用 DRb 最重要的就是理解对象是 DRbObject 还是本地对象,如果不需要客户端知道这个对象,两个事情可以解决:

  • 加入 DRbUndumped
  • 返回一个客户端也存在的对象,比如 nil

这是它的关键理解点。否则,可能会出现 DRbUnknownError, 这就是 DRb 向你报怨无法序列化对象的证词。

#11 楼 @lyfi2003 除了简单的这么处理,还有没有更好的解决方案?

#17 楼 @chucai 这没有所谓好不好的解决方案,在这里这就是你的正确解决方案。

如果想使用分布式代理,才会用到 start_service.

#18 楼 @lyfi2003 哦。恩。:-)。谢谢。 我是不是可以使用 纯的 http 将数据提交到 服务器端,存入 queue 中 而不需要 drb 你觉得如何?

#19 楼 @chucai 嗯,这也完全可以。DRb 的目标是让你最简单方便使用对象代理,如果你用 http 就需要设计方法调用,http 容器,相比而言,DRb 更简单些。

#14 楼 @lyfi2003 这里不明白,返回值本来就是 nil 啊,所以 client 不会拿到服务端对象吧

#22 楼 @fsword push 返回是原始对象数组,所以会回传给 client 序列化作为客户端对象,这样就报错了。

#23 楼 @lyfi2003 我以为是自己记错了 api,不过试了一下没错啊

$ irb
1.9.3p194 :001 > require 'thread'
 => true 
1.9.3p194 :002 > aa = Queue.new
 => #<Queue:0x000000022db1b0 @que=[], @waiting=[], @mutex=#<Mutex:0x000000022db138>> 
1.9.3p194 :003 > xx = aa.push 'sss'
 => nil 
1.9.3p194 :004 > xx.class
 => NilClass 
1.9.3p194 :005 > 

#24 楼 @fsword 刚才看了一下 @chucai 的 xmpp_server, push 默认返回的是 #<Thread:0x00000102a06ce0 所以会报错。这里跟 threadqueue 是不一致的。

#25 楼 @lyfi2003 找到原因了

def push(obj)
  @mutex.synchronize{
    @que.push obj
    begin
      t = @waiting.shift
      t.wakeup if t
    rescue ThreadError
      retry
    end
  }
end

关键是这个 synchronize

def synchronize
  self.lock
  begin
    yield
  ensure
    self.unlock rescue nil
  end
end

所以返回值是 t.wakeup,也就是一个 Thread 实例

wakeup → thr: Marks thr as eligible for scheduling (it may still remain blocked on I/O, however). Does not invoke the scheduler (see Thread#run)

恩,距离文盲又远了一些

#26 楼 @fsword 缺省的返回值不那么靠谱~所以在 DRb 中要小心对待。

ps: 照你的钻研法,可不是一般人能比的,呵呵。

话说为什么监听的这个地址只能有 domain 和 port,不能有 path 啊(否则显示 can't parse uri:druby://0.0.0.0:8787/file (DRb::DRbBadURI))。。这样如果要代理十个类不是要监听十个端口了嘛?

好文,张姿势了

涨姿势了

lyfi2003 Ruby 有类似于 dubbo 这样的分布式框架吗 提及了此话题。 09月28日 21:46
需要 登录 后方可回复, 如果你还没有账号请 注册新账号