内容简介:浅析“远程对象调用”
远程对象调用的概念
要说“远程对象”,必先说“远程调用”,也就是 RPC 。比较著名的 RPC 框架有,最近很火的 gRPC ,也就是 Google 开源的 RPC 。另外还有 Facebook 开源的 Thrift 等等……我厂内部也有很多 RPC 框架,琳琅满目不暇接。 Java 在 JDK 里面也支持 RMI ( Remote Method Invoke: 远程方法请求)功能,也可以视为一种 RPC ,但实际上这个更像我们现在要讨论的“远程对象调用”。
在诸多的 RPC 中,我们都基本认为是通过网络,对运行在另外一个进程(或者电脑)里的某个函数,发起一次调用请求。既然是一次函数调用,那么我们自然要传入参数,然后期望获得返回值。在这个过程中,我们往往只需要输入:函数名 + 参数, RPC 就能找到一个远程的进程,去执行对应的函数,然后传入目标参数。在这个过程里,执行这个函数的进程,会被认为是无状态的,所有的输出,都仅与输入的参数有关,除非有一部分状态是记录在数据库(持久化设备)上的。因此,计算的过程(算法),和计算的数据,实际上分离的,这些计算所需的数据,要么来源于参数,要么是数据库设备。而被请求的函数,以及装载这个函数的容器——进程,是不保证任何的状态维护能力的。
而“远程对象调用”,正是在“状态”这个环节上,和 RPC 不同——它是由框架去保证某种状态的。当我们发起一个远程对象调用的时候,是需要首先“找到”一个远程对象,然后再发起“方法”(成员函数)调用。这和 RPC 就产生了两个明显的区别:
一、 我们需要用某种手段定位到对象,而不是仅仅用一个函数名。对象是一个更复杂的远程概念,因为有可能同属于一个类( class ),而存在多个状态一致或不一致的对象,在远程的机器上存在。我们就不能仅仅通过一个固定的路由标志(比如类名)去找一个这样的对象。远程对象的路由方式成为不同“远程对象调用”框架之间的一个显著区别。
二、 我们并不需要把所有的数据,在每次请求时都通过参数发给远程对象,因为对于同一个远程对象来说,它是可以包含大量过程状态的。我们只要找到正确的远程对象,就能获得之前操作所造成的结果状态。有远程对象往往是生存在进程的内存中,所以对于访问自己的状态数据,会非常快速,这对于有延迟压力的程序来说,是非常有用的。
所以,远程对象调用,最大的特点,就是数据和计算是合并在一起的——这很好的提高了使用面向对象编程的便利性,也大大降低了远程调用中因为数据拉取产生的延迟。
远程对象的优点: DB 压力、易用性
在传统的“请求 - 响应”为基础的分布式服务器中,最常见的数据系统是:接入 - 逻辑 - 缓存 - 数据库 这样一个四层结构。为了让承担计算压力的“逻辑”模块能分布到不同的进程上,我们往往会把“逻辑”模块做成“无状态”的,这样我们就可以随意的启动、停止任何一个逻辑模块的进程,而不需要担心因此丢失用户数据。但是这样做,逻辑模块是轻松了,承担状态存储的“缓存 - 数据库”哥俩压力就大了。因为每一个数据操作,都需要去从他们这里读取数据,然后再回写结果(如果有数据修改操作的话)。
由于“缓存 - 数据库”模块是有状态的,一般来说还很难简单的做分布式部署,因为如果随机分布数据的话,逻辑模块可能就会找不到状态所在的缓存进程。从 CAP 理论可以知道,我们要让状态能分布,就一定要牺牲一些一致性或可用性。因此我们更倾向以 NoSQL 的存储系统去充当“缓存 - 数据库”模块。但是,即便是 NoSQL ,还是会有两个缺点:一个是跨进程访问的延迟;一个是编程上的复杂性。
跨进程访问的延迟来源于两方面,一方面是本身跨进程通过 socket 之类的手段通讯,就会有比进程内存访问高的多的延迟,而且我们常常会把一个业务流程按数据的类型划分到不同的“逻辑模块”里,这样一个业务请求可能会需要多次的跨进程访问才能访问完所需的数据,这就大大加重了因为网络带来的延迟;另外一方面来源于路由查找,虽然我们可以用一致性哈希这类算法取代路由查找,但是基于数据的业务特性,我们却不太喜欢把所有数据都拆的七零八落,所以常常还是有一个查询、或探索数据所在地的过程。
编程的复杂性也是很严重的问题。不管是 SQL 还是 NoSQL ,这些数据都是以序列化的方式描述的,并且也按照数据的组织(存放)形式,要求使用者去准备好输入或者解析读出这些数据。这些数据和我们在编程中常用的结构体、对象往往完全是不一样的形式。这就造成了我们很多额外的编码和调试的工作。这些数据往往还是“结构敏感”的:如果我们修改了数据结构,往往需要重新配置数据表结构,修改访问代码等等。这让我们在快速开发业务逻辑的时候,背上沉重的开发效率包袱。——因此业界才有很多所谓 ORM (对象关系映射)的框架出现。
但是如果我们使用“远程对象调用”,就可以有效的缓解以上两个问题:
一、 缓解跨进程延迟。由于远程对象本身已经包含了数据,所以对于所需的数据,都是从内存中直接读写,这方面的延迟是绝对最快的。另外,由于远程对象调用发起之前,已经需要先查找到目地对象,这样就把查找方法和查找数据的两个过程合二为一了,在路由层面也能有效降低延迟。
二、 极好的易用性。由于面向对象编程的概念已经深入人心,所以对于“先找到一个对象”,然后“调用其方法”的过程,是非常自然的。复杂的负载均衡、容灾、扩容等问题,实际上都隐藏在“查找对象”这个环节底下,开发者几乎无需关心,所以用起来会非常方便。而编写一个远程对象,也非常简单,就是写一个类,实例化一个对象,然后登记到服务器里而已。这都是面向对象编程的传统做法。由于对象本身都是带数据的,所以编写这些远程方法也会比较简单,大部分的数据都直接在本地内存读写,比如从对象成员属性里。节省了大量编写 SQL 或者定义和使用特别的存储设备协议的时间。
业界远程对象方案: EJB/MS-WCF/IBM-ORB
远程对象调用的框架,在业界也是常见的东西,这里大概说一下三家的 : EJB, MS WCF, IBM ORB 。这三家的框架大概的说明现在远程对象调用的主流用法。
一, EJB
EJB 全称 Enterprise Java Bean ,是 Java 的企业分布式集群方案的核心( J2EE 规范)。能部署在多个服务器上提供远程对象调用服务的 JAVA 对象,就称为 EJB 对象。底层的网络是通过 JDK 自带的 RMI 功能实现。 EJB 本身只是 J2EE 规范中的一部分,仅仅是一套接口。具体的实现由类似 Weblogic 这样的“ EJB 容器”软件提供。 EJB 之所以不及 SSH ( Spring Structs Hibernate )流行,很大原因就是因为这些容器软件都是商业软件,需要花很贵的价格购买。但这并不影响 EJB 作为一个优秀的远程对象方案的技术地位。 EJB 现在已经升级到 3.0 版本以上了,摒弃了以前配置复杂,功能晦涩的特点,大胆的使用更简单的生命周期管理、简单的注解式配置、好用的 ORM 能力,让 EJB 3.0 重新成为一流的技术。
一个客户端程序,想要访问一个 EJB 对象,一般需要使用一个叫做 JNDI 的 API ,来具体连接到 EJB 对象上。 JNDI 的全称是 Java Naming and Directory Inerface ,基本等于我们常说的名字、目录服务接口。 Java 通过一套 API 规范,来统一各种目录服务器的使用方法。所有的 J2EE 容器,都必须提供一个 JNDI 服务,而客户端程序则通过使用 J2EE 容器提供的 JNDI 来访问容器内的 EJB 对象。 JNDI 的使用方法,基本上就是输入一个字符串,然后 API 会返回给你一个对象。在 J2EE 的环境里,这个对象就是 EJB 对象的 Home 接口对象(对应远程 EJB 对象的一个映像,也叫桩对象)。代码类似:
Context ctx = new InitialContext(env);
Object ejbHome = ctx.lookup( “ java:comp/env/ejb/HelloBean ” );
HelloHome empHome = (HelloHome) PortableRemoteObject.narrow(ejbHome, HelloHome.class );
输入 lookup() 函数的字符串,是用户可以自己定义的任何内容,只要在对应的 EJB 容器里面登记了这个对应关系即可。从这个代码我们可以看到,如果 EJB 想要做容灾、负载均衡等功能,是完全可以通过 ctx.lookup() 这个接口来实现的。另外,远程对象的 Home 接口(桩代码)是需要预先部署在客户端测,在上面的例子里是 HelloHome.class 这个类。而 EJB 对象的这个 Home 接口类,是由 EJB 工具,自动通过来源的 EJB 对象类定义生成的。对比 CORBA , Thrift 等技术, EJB 可以直接用 .java 源代码代替 IDL 定义,然后自动生成桩代码,这确实是简便很多。
EJB 规范把远程对象定义为三种:无状态会话 Bean ,有状态会话 Bean ,消息驱动 Bean 。这意味着 EJB 容器对于 EJB 对象的生命周期是有管理的。其中无状态会话 Bean 和消息驱动 Bean 的声明周期是类似的,都是来一个请求(消息驱动的意思是每来一个 JMS 消息),就可能 new 一个 Bean 对象。当然也可能不是每次请求都新建对象,总之容器不保证会保持 Bean 对象的生存周期,这样容器可以根据负载压力,灵活的管理众多的 Bean 对象。而最特别的是“有状态会话 Bean ”,容器会根据客户端的会话状态(和客户端的 context 对象对应),来保持 Bean 对象,也就是说,每个客户端 context 对应一个有状态 Bean 。如果你用这个客户端 context ,发起多次 lookup() 查找,访问的那个 EJB 对象都将会是同一个。这对于需要保持登录状态的服务,就非常方便了。客户无需自己去维持一个远程对象的生命周期,而能得到状态保存的功能。
最后说说 EJB 的部署配置,以前的 EJB 容器部署异常复杂。除了需要写一个继承于特定基类的业务 JAVA 类外,还要配置很多细节。而 EJB3.0 之后,通过 JAVA 注解功能( Annotation ),这些配置都可以和源代码写到一起,而业务 JAVA 类也无需集成特定的接口和类型,可以是任何一个普通的类( POJO ),只是需要加上一些特定的注释即可。 EJB 容器提供 工具 对这些加了 EJB 注释的 JAVA 类进行处理,一方面把这个 JAVA 类自动部署到容器中,另一方面生成客户端的 Home 接口类文件,供用户发布(拷贝)到需要使用的客户方服务器上去。而一些 EJB 容器(如 Weblogic )还提供了 Eclipse ( IDE )的图形界面工具,让整个过程几乎都不在需要编写额外的配置和命令行操作。
二, MS WCF
WCF 全称 Windows Communication Foundation ,是微软发布的用于构建面向服务的应用程序框架。这套框架的底层是 Windows 的 COM+ 技术,而编程接口则更多的使用 C# 语言 /VB 语言和 .Net 平台。这和 EJB 有一定的类似,差别就是 WCF 中的远程对象,不需要一个像 JVM 那样的虚拟机,而是结合在 WINDOWS 操作系统里。
无独有偶, WCF 的远程接口定义,也是直接使用 C#/VB 代码,加上类似注解的“特性”( Attribute )功能注释,标注在一个定义好的接口( Interface )上来组成的。具体的业务实现类,只要“实现”定义的这个接口就可以了,和一个普通的类没有任何差别。和 EJB 的差别是,我们还是需要写一段 XML 配置,把这个远程对象的接口和查找字符串,注册到万能的 IIS 服务器里面。一旦注册完成,就可以通过 URL : http://xx.xx.xx.xx/servicesname/service.svc 这样的字符串去访问了。同时,如果客户端想要访问这个远程对象,则需要使用 svcuitl.exe 这个工具,输入刚刚注册的那个 URL ,就可以生成对应的客户端桩代码库。客户端可以直接 new 这个新建立的桩类型对象,然后直接调用其方法,就和调用本地对象的方法一样。
// Create a client.
CalculatorClient client = new CalculatorClient();
// Call the Add service operation.
double value1 = 100.00D;
double value2 = 15.99D;
double result = client.Add(value1, value2);
Console.WriteLine( "Add({0},{1}) = {2}" , value1, value2, result);
当然,如果你想连接不同的服务器,还是有机会的,一位内生成的客户端代码,会使用一个配置文件。在里面可以修改远程服务器的地址(还是那个注册的 URL )。
< client >
< endpoint
address = " http://localhost/servicemodelsamples/service.svc "
binding = " wsHttpBinding "
contract = " Microsoft.ServiceModel.Samples.ICalculator " />
</ client >
你除了可以通过 IIS 来提供 WCF 的远程对象服务外,还可以自己写一个单独的程序,通过定义 main() 来完全的控制这些远程对象,从而提供服务。另外, WCF 除了通过 URL 直接对应一个远程对象外,还可以通过编写“路由服务”,来对同一个 URL 的远程对象调用进行灵活的路由。虽然 WCF 没有提供类似 EJB 的远程对象生命周期管理功能,但是你完全可以通过 WCF 的服务 API 和路由服务,来自己编码实现任何形式的远程对象生命周期管理。
三, IBM RMI-IIOP
IBM 公司的 RMI-IIOP 服务,是以 JAVA 技术为基础的,但是又不同于 EJB 的另外一套远程对象技术。这套技术更接近于以 JAVA 为基础实现的 CORBA 体系。这个技术的使用标准的 JAVA RMI 接口( RMIInterface )作为远程对象的接口,使用 JAVA 的序列化、反序列化能力作为编码能力。然后自己写一个 main() 函数,建立一个 org.omg.CORBA.ORB 对象来构造一个远程服务器。而客户端则是通过一个字符串来定位想要访问的远程对象。这个字符串类似: corbaloc:iiop:1.2@localhost:8080/OurLittleClient 。我们可以看到这里面有 IP 和端口,还有一个编写服务器远程对象时注册的字符串 OurlLittleClient 。我们通过 rmic – iiop Server 这样的命令行部署远程对象,然后用 start java Server 启动服务器,用 start java Client 启动客户机。这些命名,都是包含在 IBM Developer Kit for Java technology v1.3.1 里面的。我们可以发现, RMI-IIOP 是一个更加原始的远程对象方案,基本上就是一个 CORBA 的 API 实现的组合。使用起来有点繁琐,但是好处是不需要学习和部署复杂的容器服务,可以完全自己编码去实现一套远程对象服务。这里没有限定你使用什么方法去定位查找远程对象,也没有限定你怎么管理远程对象的生命周期,一切都由开发者自己去编写实现。
总结
规范 |
远程对象定位 |
远程对象生命周期管理 |
服务器部署 |
EJB |
JNDI 路径字符串查找 |
自动管理,带会话状态对象 |
使用容器服务 |
WCF |
URL 、路由服务 |
无 |
部署到 IIS 或自写 main() |
RMI-IIOP |
COBRA URL 定位 |
无 |
自写 main() |
在对象定位的选择上,通过字符串查找已经是标准,而复杂的自定义路由也可以隐藏在这个查找操作下面。远程对象的生命周期管理,实际上是对服务器资源的管理,除了 EJB 有容器支持以外,其他的方案都比较少提供这样的能力,说明这一块是比较困难的。服务器部署方面,可以让用户以 API 自己写 main() 去构建服务器,提供了极大的灵活性。
远程对象的挑战:生命周期管理、数据一致性
通过上面的分析,我们可以发现,远程对象的生命周期管理,是一个比较重大且复杂的课题。我们要保证这样的生命周期管理程序,能有一个通用的策略,来保持各种业务情况下的服务器资源稳定,是比较困难的。而且在分布式系统的情况下,为了负载均衡,还要把同样类型的远程对象,部署到不同的进程上,这就引入了一个新的问题:数据一致性。
远程对象的生命周期,除了占用服务器的内存资源外,还会占用记录其地址的路由空间,检查维护生命周期的 CPU 运算时间。如果我们提供自动化的对象生命周期管理,势必就需要在客户使用的时候,提供这方面的教育,以及防止客户使用错误、过载等情况下对象管理失效的防御性策略。所以即便是 EJB 容器,也仅仅提供了非常简单的生命周期管理策略:会话状态、无状态这两种。
对于一般的互联网应用,只有 EJB 这两种生命周期管理的远程对象,基本上是够用的。因为一般的互联网应用,大部分数据都是持久化数据,需要读写数据库。临时状态数据一般来说不多,主要是用户登录后的产生的一些过程数据,有一个“会话( Session )”类型的生命周期就足够了。但是,如果我们的业务是网络游戏,那么这么简单的生命周期就是完全不够的,因为游戏中有大量的临时状态,比如组队的状态,玩家所在房间的状态,关卡副本的状态等等。这些临时状态,都是需要我们通过业务逻辑代码,来控制和管理所对应的对象生命周期的。所以一个适合游戏的远程对象系统,需要提供让客户端程序来选择,“新建 / 初始化”和“销毁”远程对象的能力。
在对远程对象进行管理的时候,我们常常会用到一种叫“对象池”的技术,使用这种技术避免频繁的新建和销毁对象。但是如果这些对象的是带状态的,那么我们的“池”就必须带索引,并且对象也必须有一个 key 。同时我们的对象还需要有一个“ reset ”的重置方法,用来让对象回归到初始化状态。
在分布式的系统下,我们的对象池因为是分别存放在不同的机器上,所以其一致性的维护往往是比较困难的。但是,我们可以把这个问题,转换成构建一个“分布式对象池”的问题。假如每个对象池,都按 KEY 的某个规律,如一致性哈希,存放不同的对象。那么只要在远程调用发起的时候,也就是通过 lookup() 查找远程对象的时候,把请求导向到对象所在进程,那么就能很方便的从本地进程对象池中获得对象。远程对象的“定位”和“一致性”在查找对象这个环节结合起来,是一个非常好的想法。这样能让远程状态对象的使用进一步简化,用户完全无需关心远程对象在什么地方,又能快速的访问到正确的对象。
[ 扩容下的远程对象迁移 ]
当分布式的对象容器出现部分进程故障,或者需要动态扩容的时候,只要我们针对对象查找的数据做某种程度的数据搬迁,或者缓存清理,就能很容易的实现对象的重新分布。如果对象同时能够支持持久化,那么这种数据搬迁,只需要简单的让对象写入持久化。然后在新的机器上,通过缓存建立的策略,从持久化设备读取出对象即可。
总结
远程对象调用,是一种业界成熟的分布式服务器系统模型。这套模型提供了强大的分布式程序架构能力,并且能方便的置入统一的运维特性能力:容灾、扩容、负载均衡。
它比远程方法调用,增加了对数据位置的指向,能有效的提高系统的响应速度。同时面向对象的形态,也能显著降低复杂逻辑的开发成本。
远程对象的生命周期管理,实际上一种分布式缓存系统的管理。良好的远程对象系统,能提高丰富的生命周期管理功能,以适合网络游戏,这种需要处理丰富临时状态的行业需求。
如果我们把远程对象的寻址和数据一致性维护结合起来,并且提供对象的持久化支持,那么远程对象调用将是一个高度自动化,且具有自我维护能力的强大分布式计算系统。
本公众号精彩文章:
以上所述就是小编给大家介绍的《浅析“远程对象调用”》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python编程初学者指南
[美]Michael Dawson / 王金兰 / 人民邮电出版社 / 2014-10-1
Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python可以用于很多的领域,从科学计算到游戏开发。 《Python编程初学者指南》尝试以轻松有趣的方式来帮助初学者掌握Python语言和编程技能。《Python编程初学者指南》共12章,每一章都会用一个完整的游戏来演示其中的关键知识点,并通过编写好玩的小软件这种方式来学习编程,引发读者的兴趣,降低学习的难度。每章最后都会......一起来看看 《Python编程初学者指南》 这本书的介绍吧!