分布式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::DRbUndumped
    Logger 对象的加载,这样的对象是无须传递给客户端的,这样,客户端代码里拿到的 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

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

原文链接:分布式Ruby解决之道

时间: 2024-11-03 11:13:23

分布式Ruby解决之道的相关文章

Velocity China 2016:阿里巴巴Aliware EDAS微服务解决之道

原文:http://mp.weixin.qq.com/s/F6_E8RxwLrWIe5nslOfuSQ 如今的阿里巴巴平台上,业务生态百花齐放,新的创新业务不断涌现,而这都得益于阿里底层的微服务架构高可扩展.而谁能想到,早在10年以前,偌大的淘宝网站点都是运行在单一的部署包内,往往对其中一个模块的改动都会牵一发而动全身. 自从2007年以来,在这近10年时间里,阿里巴巴技术团队一直在微服务的道路上摸索前进着,其间伴随着互联网和移动互联网的盛行,海量的用户一次又一次的洗礼了各个机构的IT系统,而在

鹰眼跟踪、限流降级,EDAS的微服务解决之道

本文主要从服务化的起源开始讲起,围绕EDAS介绍这些年来,随着阿里庞大的电商技术平台流量和并发不断攀升过程中,中间件的微服务技术面临的一系列挑战及解决方法.同时,也会向读者介绍历次双十一背后,EDAS服务化技术的演进历程. 服务化的起源 微服务的解决之道 海量微服务的挑战 关于演讲者 以下为精彩内容整理:   服务化的起源 阿里巴巴前期技术现状 当时阿里巴巴技术团队规模有500人左右,整个技术网站使用单一War应用,基于传统应用开发架构,业务每年翻倍增长. 我们面临着非常多的问题: 上百人维护一

华为存储系统 大数据解决之道

随着近年来互联网.云计算.移动终端和物联网的迅猛发展,全球在2010 年正式进入ZB 时代.随着大数据时代到来,海量,多类型数据的存储.分析.容灾.备份以及统一管理都对传统架构产生了很大的冲击. 大数据时代,华为推出了大数据解决之道:华为OceanStor MVX存储系统(以下简称MVX).MVX系统是融合Scale-out NAS.Scale-out Database和Scale-out Backup,实现多位一体,面向大数据存储的集群存储系统.MVX在一个系统内实现了分布式存储.分布式备份以

浪潮张东:大数据时代,挑战与解决之道

第五届中国云计算大会于2013年6月5-7日在北京国家会议中心拉开帷幕.本次大会以国际视野,洞悉全球云计算发展趋势,并从应用出发,探讨云计算与大数据.云计算与移动互联网.云安全及云计算行业应用等焦点话题.大会还特别设立了云计算服务展示区域,交流国际云计算最新研究成果,展示国内云计算试点城市发展成就,分享云计算发展经验,促进全球云计算创新合作. 浪潮集团系统软件总监.云计算产品部总经理 张东 在第五届云计算大会第二天的演讲上,浪潮集团系统软件总监.云计算产品部总经理张东给我们带来了名为<大数据时代

Java/J2EE中文问题终极解决之道

j2ee|解决|问题|中文 Java中文问题一直困扰着很多初学者,如果了解了Java系统的中文问题原理,我们就可以对中文问题能够采取根本的解决之道.    最古老的解决方案是使用String的字节码转换,这种方案问题是不方便,我们需要破坏对象封装性,进行字节码转换.     还有一种方式是对J2EE容器进行编码设置,如果J2EE应用系统脱离该容器,则会发生乱码,而且指定容器配置不符合J2EE应用和容器分离的原则.     在Java内部运算中,涉及到的所有字符串都会被转化为UTF-8编码来进行运

网站过度优化的原因及解决之道

每个站长都很重视SEO,无论是新站老站都在SEO方面卯足了劲,但我们也常发现并不是一份耕耘一份收获,有时候我们做了大量的SEO工作到头来却发现网站被搜索引擎惩罚了!降低排名.降权的情况很多网站都出现过,这也就是所谓的优化过度,那么什么样的情况会导致优化过度呢?优化过度的网站又应该如何起死回生?以下是本站长给您的一些建议. 以下是网站过度优化的4点原因以及解决之道: 1.关键词堆砌过度:网站的关键词优化很重要,这是网民找到网站的线索,但如果网站关键过度堆砌,搜索引擎会认为网站有作弊的嫌疑,这时候网

软件开发的那些事儿:解决之道

前面提出了软件开发的轮回:期望--破灭--崩溃--新的轮回,我们的解决之道在哪里呢? 我的反思--不在沉默中爆发,就在沉默中灭亡 反思,我在反思-- 对于来自客户的变更,我永远忘不了的是大学时老师的谆谆教导.上软件工程课的时候,老师总是一再地反复强调,一定要将需求变更消灭在需求分析阶段.按照过去的瀑布式开发理论的描述,总是要求我们在需求分析阶段了解清楚客户的所有需求,并编写成<软件需求说明书>,交给客户签字.客户一旦在<软件需求说明书>上签字,那么需求就不能再更改了,软件就照这个开

存储虚拟化:避免常规陷阱 五大解决之道

存储虚拟化已经发展成为当前主流技术,如何在你部署虚拟化项目之前避免常规陷阱,找到解决之道.本文就列出了5个你需要考虑的关键问题. Gartner最近的调查显示,如果你是一名IT经理,机会是你已经计划部署虚拟化存储.近乎1/4不少于500名员工的厂商已经配置了存储虚拟化产品,其余55%则计划在未来的2年内实现它. 存储虚拟化是一种不同与实际物理存储的抽象化表述服务器和存储应用的概念,典型的集合多重储存装置,并将他们通过一个管理控制台进行管理. 这项技术快速在企业中风靡起来有如下理由:在许多情况下,

电脑硬盘出问题有哪些解决之道?

  硬盘出问题的解决之道有哪些? 1.进行硬盘整理,在整理硬盘碎片的时候,记得要关闭其他所有的应用程序,包括屏幕保护程序,最好将虚拟内存的大小设置为固定值.不要对硬盘进行读写操作,一旦整理程序发现硬盘的文件有改变,它将重新开始整理. 2.整理硬盘碎片的频率要控制合适,过于频繁的整理也会缩短磁盘的寿命.一般经常读写的磁盘分区最多一周整理一次. 3.整理完硬盘碎片之后,你还可以通过"硬盘清理"程序来对硬盘当中的一些多余的文件进行清理,这样可以节省一部分硬盘空间. 4.进行硬盘扫描,虽然它的