分布式Ruby解决之道
其实用Druby很久了,今天需要完成一个进程数据同步的机制,我需要的不是运行速度快,不是用 linux / mac 下的扩展,而是独立,快速开发效率,方便最简单的Ruby环境可运行,可以吗? DRb(即分布式Ruby,下面都这样说它)是内置于Ruby标准库中的对象代理的实现。什么是对象代理,现在不明白不要紧,一会就知道了。
解决什么样的问题?
有的时候,我们需要提供远程的服务,比如提供远程API调用(如果你听过RPC,或WDSL),这样,我们可以很大程度上解耦各大模块,对外提供服务。
还有的时候,我们需要在两个进程中通信,以获得互相的同步或资源。
更有,我想实现实现某种透明的对象,让对象可以在不同的进程或主机上传递。
这些,都可以通过 DRb 来实现。DRb 的相关文档非常少,但在想快速实现一个轻量级分布应用,依赖最少化时,使用它是非常方便的。我对分布式的研究不多,欢迎各位看了本文后能提出更多解决方案。
使用方法
依官方的例子为主,各位看官建议看的时候复制下试试。因为是分布式解决方案,肯定是 服务端 与 客户端 双方的代码。
- 简单的例子
* 服务端 # ==== 服务端代码,保存为 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` 返回一个远程对象,会发 生什么呢? 接下来,就是我要讲的。
- 远程对象代理
* 服务端 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?你不用看都能知道是吧?因为我也会这样实现的。
代理器
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_message
是method_missing
做的最重要的事,它引出来了下面的事情。通信底层
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 中,这一切显得如此简单。
序列化方法(与对象引用转换)
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解决之道
分布式Ruby解决之道的更多相关文章
- 《分布式事务解决之道》沙龙ppt共享
大型分布式系统往往由很多“微服务”组成,而不同的微服务往往又连接着不同的数据库,在看似常用的功能背后,可能又需要横跨不同的“微服务”和“数据库”才能实现.那么如何才能保证系统事务的一致性呢?这也同时是 ...
- atitit. web 在线文件管理器最佳实践(1)--- elFinder 的使用流程解决之道 。打开浏览服务器文件夹java .net php
atitit. web 在线文件管理器最佳实践(1)--- elFinder 的使用流程解决之道 .打开浏览服务器文件夹java .net php 1. 环境:::项目java web,需要打开浏览服 ...
- 淘宝杨志丰:OceanBase--淘宝结构化大数据解决之道
时至今日,“Big data”(大数据)时代的来临已经毋庸置疑,尤其是在电信.金融等行业,几乎已经到了“数据就是业务本身”的地步.这种趋势已经让很多相信数据之力量的企业做出改变.恰逢此时,为了让更多的 ...
- java中文乱码解决之道(九)-----总结
乱码,我们前台展示的杀手,可能有些朋友和我的经历一样:遇到乱码先按照自己的经验来解决,如果没有解决就google,运气好一搜就可以解决,运气不好可能够你折腾一番了.LZ之所以写这个系列博客就是因为遇到 ...
- java中文乱码解决之道(二)-----字符编码详解:基础知识 + ASCII + GB**
在上篇博文(java中文乱码解决之道(一)-----认识字符集)中,LZ简单介绍了主流的字符编码,对各种编码都是点到为止,以下LZ将详细阐述字符集.字符编码等基础知识和ASCII.GB的详情. 一.基 ...
- java中文乱码解决之道(七)-----JSP页面编码过程
我们知道JSP页面是需要转换为servlet的,在转换过程中肯定是要进行编码的.在JSP转换为servlet过程中下面一段代码起到至关重要的作用. <%@ page language=" ...
- 详解收发不畅原因及U-Mail邮件中继解决之道
邮件在商务往来中扮演着信息交流的重要角色,假如传输受阻,必将造成沟通不畅:可能三五封邮件的投递你意识不到其重要性,但假如长期需和客户保持沟 通,则需要保证其一贯的稳定性,这就很考验相关软件平台的性能是 ...
- Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java
Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java 1. 内存区域的划分 1 2. PermGen内存溢出深入分析 1 3. PermGen OOM原因总结 ...
- atitit.html5 拼图游戏的解决之道.
atitit.html5 拼图游戏的解决之道. 1. 拼图游戏的操作(点击法and 拖动法) 1 1. 支持键盘上.下.左.右键移动: 1 2. 支持点击空白模块中的上下左右箭头移动: 1 3. 支持 ...
随机推荐
- [supervisor] 使用小记(入门教程)
之前到现在很久没有用了,还是从安装说下,做个简单的实验,系统为Ubuntu14.04 快速安装配置 sudo pip_python install supervisor sudo echo_super ...
- EBS中的采购单据状态及其控制
李 颖 (济南钢铁股份有限公司 装备部,山东 济南 250101) 摘 要:介绍了Oracle Purchasing模块中采购单据的管理与控制,结合实例,分析了各状态下可采取的控制活动及控制活 ...
- Android初级教程进程间的通信AIDL
在介绍跨程序进程间通信AIDL前,先看一下本程序activity与某个服务是怎么绑定在一起进行交互的. 需求:服务有两个方法.分别是播放音乐与停止播放音乐.该程序的活动要访问这两个方法,在activi ...
- Linux内核2.6的进程调度
Linux是多任务抢占操作系统,多任务就是指多个进程间通过分时切换来并发执行.非抢占的系统是对每个进程而言,除非时间片用完或主动放弃否则不会被剥夺CPU,主动放弃包括调用一些调度的系统调用( ...
- Hessian源码分析--HessianProxyFactory
HessianProxyFactory是HessianProxy的工厂类,其通过HessianProxy来生成代理类. 如下面代码: HessianProxyFactory factory = new ...
- Uva - 210 - Concurrency Simulator
自己写个双端队列,或者直接用deque,这个也比较好用 AC代码: #include <iostream> #include <cstdio> #include <cst ...
- C语言中的内存分配
对于一个C语言程序而言,内存空间主要由以下几个部分组成: 1)程序代码区:用来存储程序的二进制代码 2)全局区/静态存储区 3)BSS段:用来存储未初始化的全局变量和静态变量. 4)栈区:存储局部变量 ...
- EBS DBA指南笔记(三)
第五章 patching patch的作用:解决应用代码的问题:安装新的特征:更新technology stack组件.打patch不是一个简单的过程,但我们也没必要深究里面每个细节. EBS的p ...
- 【Unity技巧】调整画质(贴图)质量
写在前面 当我们在Unity中,使用图片进行2D显示时,会发现显示出来的画面有明显的模糊或者锯齿,但是美术给的原图却十分清晰. 要改善这一状况实际上很简单. 造成这样的原因,是Unity在导入图片(或 ...
- Web Service进阶(六)SOAPBinding绑定方式异常 is not found. Have you run APT to generate them
当在类中填充相应方法时,提示如下错误: 出现以上错误的原因就是在注解中没有添加@SOAPBinding(style=SOAPBinding.Style.RPC)这句话.估计也与JDK的版本相关,这方面 ...