RPC与Apache Thift

来源:互联网 发布:手机网络时间同步软件 编辑:程序博客网 时间:2024/06/06 03:56

特殊的RPC服务——JAVA RMI、阿里的RPC框架Dubbo(服务治理也会一起讲解)、Apache Thift。
MQ
消息队列又是另外一种系统间通信方式的实现。消息队列的规范目前有N多种,针对的场景和实现的性能各不相同。这个系列的博文我们将花一定的篇幅介绍JMS、AMQP两种消息队列协议和实现(特别是AMQP协议),然后介绍Kafka消息队列和使用场景,最后前瞻一下目前号称最快的消息队列ZMQ。
ESB
ESB(企业服务总线)是SOA的典型实现,各种ESB软件它们的共同特点是:将各个(有访问权限的)系统所提供服务集中在一起(进行管理、控制、协调),请求方只需要访问ESB软件,然后再由ESB软件代其访问指定的服务,最后返回处理结果。ESB的功能特点是代理。
这个系列的博文,我们将花比较大的篇幅,讲解Apache Camel的原理和使用,从而间接讲解ESB中的若干重要概念(服务顺序编排/定义,服务实现隔离、多协议支撑、协议翻译、转发代理、事务控制等)。如果有多出来的富裕时间,我们将讲解一个基于Apache Camel的真实工程。
目前市面上的共享/商用的ESB软件还有很多,例如JBossESB、普元EOS、还有IBM和Oracle的ESB产品。不过还是那句话,理解原理和技术特点才是最关键的。
服务注册中心
服务注册中心,是SOA的另一种实现方式,和ESB最大的不同点是:“服务注册中心”主要提供各原子系统的服务注册、服务治理、服务隔离、权限控制。当客户端进行请求时,“服务治理”将告诉客户端到哪里去访问真实的服务,自己并不提供服务的转发。Dubbo就是一个典型的服务治理框架。
什么是RPC
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。那么我们至少从这样的描述中挖掘出几个要点:RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。这里要说明一下,目前技术的发展趋势来看,实现了RPC协议的应用工具往往都会附加其他重要功能,例如Dubbo还包括了服务管理、访问权限管理等功能。
网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。既然网络协议对其透明,那么调用过程中,使用的是哪一种网络IO模型调用者也不需要关心。信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。
这里写图片描述

Client:RPC协议的调用方。就像上文所描述的那样,最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。但实际情况来说Client或多或少的都需要指定RPC框架的一些细节。
Server:在RPC规范中,这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现(在Java中就是RPC服务接口的具体实现)。其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。
Stub/Proxy:RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议,也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的。
Message Protocol:在上文我们已经说到,一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编号和解码操作。目前流行的技术趋势是不同的RPC实现,为了加强自身框架的效率都有一套(或者几套)私有的消息格式。例如前文所讲到的RMI框架使用的消息协议为JRMP;后文我们将详细讲解的RPC框架Thrift也有私有的消息协议,“- Transfer/Network Protocol”(当然它还支持一些通用的消息格式,如JSON)。
Transfer/Network Protocol:传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。例如Hessian的传输协议基于HTTP(应用层协议);而Thrift的传输协议基于TCP(传输层协议)。传输层还需要统一RPC客户端和RPC服务端所使用的IO模型;

Apache Thrift

一款典型的RPC规范的实现Apache Thrift。Apache Thrift的介绍一共分为三篇文章,上篇讲解Apache Thrift的基本使用;中篇讲解Apache Thrift的工作原理(主要围绕Apache Thrift使用的消息格式封装、支持的网络IO模型和它的客户端请求处理方式);下篇对Apache Thrift的不足进行分析,并基于Apache Thrift实现一个自己设计的RPC服务治理的管理方案。这样对我们后续理解Dubbo的服务治理方式会有很好的帮助作用。

基本知识
Thrift最初由facebook开发用做系统内各语言之间的RPC框架 。2007年由facebook贡献到apache基金, 08年5月进入apache孵化器 ,称为Apache Thrift。和其他RPC实现相比,Apache Thrift主要的有点是:支持的语言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多种语言)、并发性能高(还记得上篇文章中,我们提到的影响RPC性能的几个关键点吗?)。
为了支持多种语言,Apache Thrift有一套自己的接口定义语言,并且通过Apache Thrift的代码生成程序,能够生成各种编程语言的代码。这样是保证各种语言进行通讯的前提条件。为了能够实现简单的Apache Thrift实例,首先我们就需要讲解一下Apache Thrift的IDL。
Thrift代码生成程序安装
如果您是在windows环境下运行进行Apache Thrift的试验,那么您无需安装任何工具,直接下载Apache Thrift在windows下的代码生成程序http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在这篇文章写作时,使用的是Apache Thrift的0.9.3版本);如果您运行在Linux系统下,那么下载http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,并进行编译、安装(过程很简单,这里就不再赘述了)。安装后记得添加运行位置到环境变量中。
IDL格式概要
以下是一个简单的IDL文件定义:

命名空间的定义 注意‘java’的关键字namespace java testThrift.iface

结构体定义struct Request {

1:required string paramJSON;2:required string serviceName;

}

另一个结构体定义struct Reponse {

1:required  RESCODE responeCode;2:required  string responseJSON;

}

异常描述定义

exception ServiceException {
1:required EXCCODE exceptionCode;
2:required string exceptionMess;
}

枚举定义enum RESCODE {

_200=200;_500=500;_400=400;

}

另一个枚举enum EXCCODE {

PARAMNOTFOUND = 2001;SERVICENOTFOUND = 2002;

}

服务定义

service HelloWorldService {
Reponse send(1:Request request) throws (1:ServiceException e);
}
以上IDL文件是可以直接用来生成各种语言的代码的。下面给出常用的各种不同语言的代码生成命令:

生成java

thrift-0.9.3 -gen java ./demoHello.thrift

生成c++

thrift-0.9.3 -gen cpp ./demoHello.thrift

生成php

thrift-0.9.3 -gen php ./demoHello.thrift

生成node.js

thrift-0.9.3 -gen js:node ./demoHello.thrift

生成c

thrift-0.9.3 -gen csharp ./demoHello.thrift

您可以通过以下命令查看生成命令的格式

thrift-0.9.3 -help
Aapche Thrift与消息格式
Apache Thrift支持多种消息格式封装。这些消息格式是如果进行编码和解码的是不需要使用者关心的,只需要根据自己的需要制定不同的消息封装格式即可。Apache Thrift所有消息格式封装的实现,都继承与TProtocol这个抽象类。
TBinaryProtocol
二进制流的编码格式。由于需要支持跨语言,所以Apache Thrift支持有限的几种通用类型,包括基本类型(Float、Double、Integer、Long、String、Short)、集合类型(Map、Set、List)还有Pojo类型(实际上就是前两者若干类型的组合形式)。
那么这个类所生成的二进制流和传统的Java序列化后生成的二进制流有什么样的区别(或者是优势)呢?我们可以通过阅读TBinaryProtocol的源代码进行研究。
我们以TBinaryProtocol中,对Integer的序列化过程进行详细的解释,来对比java提供的其他几种序列化的方式找到不同。首先java中,如果要将一个Integer对象通过网络发送出去,要做的第一件事情就是序列化,那么我们常用的序列化方式有两种,如下所示:
• java中序列化Integer对象的第一种方法:
Integer integerObject = 10066329;
integerObject.toString().getBytes();
• java中序列化Integer对象的第二种方法:
ByteArrayOutputStream aStream = new ByteArrayOutputStream();
ObjectOutputStream oStream = new ObjectOutputStream(aStream);
oStream.writeObject(integerObject);
aStream.toByteArray();
第一种方式是将Integer对象中的值序列化;第二种方式,是将Integer整个对象序列化。这两种方式虽然都产生byte[],实际上性质是完全不一样的。我们来看一下这两种方式产生的byte[]的内容:
• 序列化Integer的值:
[49, 48, 48, 54, 54, 51, 50, 57]
• 序列化整个Integer对象:
[-84, -19, 0, 5, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1, 73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, -103, -103, -103]
第一种方式序列化后,byte数组有8个byte元素(因为是首先转换成字符串的,所以实际上这个大小会随着Integer值的大小增加而增加);第二中方式序列化后,byte数组一共有 > 20 个byte元素,其中除了记录Integer的值以外,还包括描述这个类型的其他属性。
那么我们再来看看TBinaryProtocol中,是如何序列化Integer类型的。首先我们来看一下TBinaryProtocol进行Integer序列化的这部分源代码,如下图所示:
private byte[] i32out = new byte[4];public void writeI32(int i32) throws TException {
i32out[0] = (byte)(0xff & (i32 >> 24));
i32out[1] = (byte)(0xff & (i32 >> 16));
i32out[2] = (byte)(0xff & (i32 >> 8));
i32out[3] = (byte)(0xff & (i32));
trans_.write(i32out, 0, 4);
}
服务治理

Apache Thrift的性能要比大多数RPC框架优秀。但如果您使用过Apache thrift,那么相信您会发现它的一些不足(或者说是所有单纯的RPC框架的不足):由于Apache Thrift使用IDL定义RCP 调用接口,实现跨语言性。那么一旦当业务发生变化后,是否要重新编写IDL,重新生成接口代码呢?
• 如果以上的事实成立,那如果在生成环境使用了多种语言,且服务节点又很多的情况下。岂不是重新部署的工作量会很大?
• 另外,生产环境的服务是不能停机的?那么就会出现一部分接口是新部署的,另外一部分接口是还未更新的。服务者怎么保证接口的稳定呢?
再说,我的生产环境下一共有20个相对独立运行的系统:计费系统、客户系统、订单系统、库存系统、物流系统、税务联动系统,等等。负责他们的开发团队都是不一样的。如何在某个系统的接口发生变动后,通知到其它系统“我的接口变动了”?即便是不能通知到所有系统“我的接口变动了”,又如何做到之前的接口也一样可以使用呢?显然以上这些问题,单纯使用Apache Thrift(或者单纯的某一款RPC框架)是无法解决的;使用人工的方式就更不要想解决了。如果您的相关系统只有2-3个,又或者每个系统的服务节点数量也不多(例如5、6个),那么以上这些问题还不太明显。但是随着您的系统越来越大,系统间协作越来越复杂,那么这些问题就会凸现出来,甚至成为影响您架构扩容的显著问题。
解决这个问题的方式,阿里的做法是在众多系统的RPC通信的上层再架一层专门进行RPC通信的协调管理,称之为服务治理框架(DUBBO框架,目前这个框架已经开源,在后面的文章中,我会花比较大的篇幅进行介绍。和DUBBO框架类似的还有Taobao的HSF)。事实上现在的软件架构中,都是使用相似的“服务治理”思想,来解决这个问题的。如下图所示:

原创粉丝点击