版权声明:本文由韩伟原创文章,转载请注明出处:
文章原文连接:https://www.qcloud.com/community/article/242java
来源:腾云阁 https://www.qcloud.com/community程序员
做者介绍:韩伟,1999年大学实习期加入初创期的网易,成为第30号员工,8年间从程序员开始,历任项目经理、产品总监。2007年后创业4年,开发过视频直播社区,及多款页游产品。2011年后就任于腾讯游戏研发部公共技术中心架构规划组,专一于通用游戏技术底层的研发。算法
要说“远程对象”,必先说“远程调用”,也就是RPC。比较著名的RPC框架有,最近很火的gRPC,也就是Google开源的RPC。另外还有Facebook开源的Thrift等等……我厂内部也有不少RPC框架,琳琅满应接不暇。Java在JDK里面也支持RMI(Remote Method Invoke: 远程方法请求)功能,也能够视为一种RPC,但实际上这个更像咱们如今要讨论的“远程对象调用”。数据库
在诸多的RPC中,咱们都基本认为是经过网络,对运行在另一个进程(或者电脑)里的某个函数,发起一次调用请求。既然是一次函数调用,那么咱们天然要传入参数,而后指望得到返回值。在这个过程当中,咱们每每只须要输入:函数名+参数,RPC就能找到一个远程的进程,去执行对应的函数,而后传入目标参数。在这个过程里,执行这个函数的进程,会被认为是无状态的,全部的输出,都仅与输入的参数有关,除非有一部分状态是记录在数据库(持久化设备)上的。所以,计算的过程(算法),和计算的数据,实际上分离的,这些计算所需的数据,要么来源于参数,要么是数据库设备。而被请求的函数,以及装载这个函数的容器——进程,是不保证任何的状态维护能力的。编程
而“远程对象调用”,正是在“状态”这个环节上,和RPC不一样——它是由框架去保证某种状态的。当咱们发起一个远程对象调用的时候,是须要首先“找到”一个远程对象,而后再发起“方法”(成员函数)调用。这和RPC就产生了两个明显的区别:缓存
咱们须要用某种手段定位到对象,而不是仅仅用一个函数名。对象是一个更复杂的远程概念,由于有可能同属于一个类(class),而存在多个状态一致或不一致的对象,在远程的机器上存在。咱们就不能仅仅经过一个固定的路由标志(好比类名)去找一个这样的对象。远程对象的路由方式成为不一样“远程对象调用”框架之间的一个显著区别。服务器
咱们并不须要把全部的数据,在每次请求时都经过参数发给远程对象,由于对于同一个远程对象来讲,它是能够包含大量过程状态的。咱们只要找到正确的远程对象,就能得到以前操做所形成的结果状态。有远程对象每每是生存在进程的内存中,因此对于访问本身的状态数据,会很是快速,这对于有延迟压力的程序来讲,是很是有用的。网络
因此,远程对象调用,最大的特色,就是数据和计算是合并在一块儿的——这很好的提升了使用面向对象编程的便利性,也大大下降了远程调用中由于数据拉取产生的延迟。数据结构
在传统的“请求-响应”为基础的分布式服务器中,最多见的数据系统是:接入-逻辑-缓存-数据库 这样一个四层结构。为了让承担计算压力的“逻辑”模块能分布到不一样的进程上,咱们每每会把“逻辑”模块作成“无状态”的,这样咱们就能够随意的启动、中止任何一个逻辑模块的进程,而不须要担忧所以丢失用户数据。可是这样作,逻辑模块是轻松了,承担状态存储的“缓存-数据库”哥俩压力就大了。由于每个数据操做,都须要去从他们这里读取数据,而后再回写结果(若是有数据修改操做的话)。
架构
因为“缓存-数据库”模块是有状态的,通常来讲还很难简单的作分布式部署,由于若是随机分布数据的话,逻辑模块可能就会找不到状态所在的缓存进程。从CAP理论能够知道,咱们要让状态能分布,就必定要牺牲一些一致性或可用性。所以咱们更倾向以NoSQL的存储系统去充当“缓存-数据库”模块。可是,即使是NoSQL,仍是会有两个缺点:一个是跨进程访问的延迟;一个是编程上的复杂性。
跨进程访问的延迟来源于两方面,一方面是自己跨进程经过socket之类的手段通信,就会有比进程内存访问高的多的延迟,并且咱们经常会把一个业务流程按数据的类型划分到不一样的“逻辑模块”里,这样一个业务请求可能会须要屡次的跨进程访问才能访问完所需的数据,这就大大加剧了由于网络带来的延迟;另一方面来源于路由查找,虽然咱们能够用一致性哈希这类算法取代路由查找,可是基于数据的业务特性,咱们却不太喜欢把全部数据都拆的七零八落,因此经常仍是有一个查询、或探索数据所在地的过程。
编程的复杂性也是很严重的问题。无论是SQL仍是NoSQL,这些数据都是以序列化的方式描述的,而且也按照数据的组织(存放)形式,要求使用者去准备好输入或者解析读出这些数据。这些数据和咱们在编程中经常使用的结构体、对象每每彻底是不同的形式。这就形成了咱们不少额外的编码和调试的工做。这些数据每每仍是“结构敏感”的:若是咱们修改了数据结构,每每须要从新配置数据表结构,修改访问代码等等。这让咱们在快速开发业务逻辑的时候,背上沉重的开发效率包袱。——所以业界才有不少所谓ORM(对象关系映射)的框架出现。
可是若是咱们使用“远程对象调用”,就能够有效的缓解以上两个问题:
缓解跨进程延迟。因为远程对象自己已经包含了数据,因此对于所需的数据,都是从内存中直接读写,这方面的延迟是绝对最快的。另外,因为远程对象调用发起以前,已经须要先查找到目地对象,这样就把查找方法和查找数据的两个过程合二为一了,在路由层面也能有效下降延迟。
极好的易用性。因为面向对象编程的概念已经深刻人心,因此对于“先找到一个对象”,而后“调用其方法”的过程,是很是天然的。复杂的负载均衡、容灾、扩容等问题,实际上都隐藏在“查找对象”这个环节底下,开发者几乎无需关心,因此用起来会很是方便。而编写一个远程对象,也很是简单,就是写一个类,实例化一个对象,而后登记到服务器里而已。这都是面向对象编程的传统作法。因为对象自己都是带数据的,因此编写这些远程方法也会比较简单,大部分的数据都直接在本地内存读写,好比从对象成员属性里。节省了大量编写SQL或者定义和使用特别的存储设备协议的时间。
远程对象调用的框架,在业界也是常见的东西,这里大概说一下三家的: EJB, MS WCF, IBM ORB。这三家的框架大概的说明如今远程对象调用的主流用法。
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)的图形界面工具,让整个过程几乎都不在须要编写额外的配置和命令行操做。
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服务,是以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()查找远程对象的时候,把请求导向到对象所在进程,那么就能很方便的从本地进程对象池中得到对象。远程对象的“定位”和“一致性”在查找对象这个环节结合起来,是一个很是好的想法。这样能让远程状态对象的使用进一步简化,用户彻底无需关心远程对象在什么地方,又能快速的访问到正确的对象。
[扩容下的远程对象迁移]
当分布式的对象容器出现部分进程故障,或者须要动态扩容的时候,只要咱们针对对象查找的数据作某种程度的数据搬迁,或者缓存清理,就能很容易的实现对象的从新分布。若是对象同时可以支持持久化,那么这种数据搬迁,只须要简单的让对象写入持久化。而后在新的机器上,经过缓存创建的策略,从持久化设备读取出对象便可。
远程对象调用,是一种业界成熟的分布式服务器系统模型。这套模型提供了强大的分布式程序架构能力,而且能方便的置入统一的运维特性能力:容灾、扩容、负载均衡。
它比远程方法调用,增长了对数据位置的指向,能有效的提升系统的响应速度。同时面向对象的形态,也能显著下降复杂逻辑的开发成本。
远程对象的生命周期管理,实际上一种分布式缓存系统的管理。良好的远程对象系统,能提升丰富的生命周期管理功能,以适合网络游戏,这种须要处理丰富临时状态的行业需求。
若是咱们把远程对象的寻址和数据一致性维护结合起来,而且提供对象的持久化支持,那么远程对象调用将是一个高度自动化,且具备自我维护能力的强大分布式计算系统。