您的位置:首页 > 其它

【转】RPC技术简介

2016-04-11 18:24 399 查看
原文地址:http://flychao88.iteye.com/blog/2192009原文如下:思考一下分布式系统中的 RPC (Remote Procedure Call) 问题,一个完整的 RPC 模块需要可以分为三个层次服务层(service):RPC 接口定义与实现
协议层(protocol):RPC 报文格式和数据编码格式
传输层(transport):实现底层的通信(如 socket)以及系统相关的功能(如事件循环、多线程)在实际的大型分布式系统中,不同的服务往往会使用不同的语言来实现,所以一般的 RPC 系统会提供一种跨语言的过程调用功能,比如一段用C++实现的客户端代码可以远程调用一个用 Java 实现的服务。实现跨语言 RPC 有两种方法:
静态代码生成:开发者用一种中间语言(IDL,接口定义语言)来定义 RPC 的接口和数据类型,然后通过一个编译器来生成不同语言的代码(如C++, Java, Python),并由生成的代码来负责 RPC 协议层和传输层的实现。例如,服务的实现用C++,则服务端需要生成实现RPC协议和传输层的C++代码,服务层使用生成的代码来实现与客户端的通信;而如果客户端用 Python,则客户端需要生成Python代码。
基于“自省”的动态类型系统来实现:协议和传输层可以只用一种语言实现成一个库,但是这种语言需要关联一个具备“自省”或者反射机制的动态类型系统,对外提供其他语言的绑定,客户端和服务端通过语言绑定来使用 RPC。比如,可以考虑用 C 和 GObject 实现一个 RPC 库,然后通过 GObject 实现其他语言的绑定。
第一种方法的优点是RPC的协议层和传输层的实现不需要和某种动态类型系统(如GObject)绑定在一起,同时避免了动态类型检查和转换,程序效率比较高,但是它的缺点是要为不同语言提供不同的 RPC 协议层和传输层实现。第二种方法的主要难度在于语言绑定和通用的对象串行化机制的实现,同时也需要考虑效率的问题。*******************原文结束,学习笔记开始****************************只看上面还是不是很清晰,自己又在网上找了两篇文章,说的比较透彻。http://blog.csdn.net/mindfloating/article/details/39474123http://blog.jobbole.com/92290/下面的学习笔记就是结合这两篇文章展开:RPC 的全称是 Remote Procedure Call 是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。我对此理解就是怎么简洁调用其它团队的服务,就像调用本地服务一样。RPC是在Socket的基础上实现的,它比socket需要更多的网络和系统资源,但是 主要目的是提供透明简洁的方式去调用远程服务,屏蔽掉通讯细节,可以让用户更专注于业务实现。下面引自上面文章中图:

RPC 服务方通过
RpcServer
去导出(export)远程接口方法,而客户方通过
RpcClient
去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理
RpcProxy
。代理封装调用信息并将调用转交给
RpcInvoker
去实际执行。在客户端的
RpcInvoker
通过连接器
RpcConnector
去维持与服务端的通道
RpcChannel
,并使用
RpcProtocol
执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。RPC 服务端接收器
RpcAcceptor
接收客户端的调用请求,同样使用
RpcProtocol
执行协议解码(decode)。解码后的调用信息传递给
RpcProcessor
去控制处理调用过程,最后再委托调用给
RpcInvoker
去实际执行并返回调用结果。下面针对上面的步骤进行逐一说明:一 导出服务(发布服务)简单来说就是告知调用者服务列表,少量的服务可以IP+端口方式+服务名称,要想自动管理就参见dubbo的方式,使用zookeeper.简单来讲,zookeeper可以充当一个
服务注册表
(Service Registry),让多个
服务提供者
形成一个集群,让
服务消费者
通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。如下图所示:

zookeeper提供了“心跳检测”功能,定期检测服务端可用性,如有变化,告知consumer.更为重要的是zookeeper 与生俱来的容错容灾能力(比如leader选举),可以确保服务注册表的高可用性。二:导入远程接口与客户端代理导入相对于导出远程接口,客户端代码为了能够发起调用必须要获得远程接口的方法或过程定义。目前,大部分跨语言平台 RPC 框架采用根据 IDL 定义通过 code generator 去生成 stub 代码,这种方式下实际导入的过程就是通过代码生成器在编译期完成的。如webservice.而对于同一语言平台的 RPC 则可以通过共享接口定义来实现.对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码不易维护,大部分公司实现RPC框架时还是选择动态代理方式。也可以理解为代码生成技术,只不过是在运行时生成。
public
class
RPCProxyClient
implements
java.lang.reflect.InvocationHandler{
private
Object obj;
public
RPCProxyClient(Object obj){
this
.obj=obj;
}
/**
 
* 得到被代理对象;
 
*/
public
static
Object getProxy(Object obj){
return
java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new
RPCProxyClient(obj));
}
/**
 
* 调用此方法执行
 
*/
public
Object invoke(Object proxy, Method method, Object[] args)
throws
Throwable {
//结果参数;
Object result =
new
Object();
// ...执行通信相关逻辑
// ...
return
result;
}
}
123456
public
class
Test {
public
static
void
main(String[] args) {
HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.
class
);
helloWorldService.sayHello(
"test"
);
}
}

三协议编解码

3.1确定消息结构 客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。参照代码就是invoke里需要封装通信细节,而通信的第一步就是要确定客户端和服务端相互通信的消息结构。客户端的请求消息结构一般需要包括以下内容:1)接口名称在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;2)方法名一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;3)参数类型&参数值参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);以及相应的参数值;4)超时时间5)requestID,标识唯一请求id,在下面一节会详细描述requestID的用处。同理服务端返回的消息结构一般包括以下内容。1)返回值2)状态code3)requestID除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。3.2序列化一旦确定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。什么是序列化?序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。什么是反序列化?将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。为什么需要序列化?转换为二进制串后才好进行网络传输嘛!为什么需要反序列化?将二进制转换为对象才好进行后续处理!从RPC的角度上看,序列化主要看三点:1)通用性,比如是否能支持Map等复杂的数据结构;2)性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;3)可扩展性,对互联网公司而言,业务变化快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,删除老的字段,而不影响老的服务,这将大大提供系统的健壮性。目前国内各大互联网公司广泛使用hessian、protobuf、thrift、avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。四 传输服务(通信)协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。目前有两种IO通信模型:1)BIO;2)NIO。一般RPC框架需要支持这两种IO模型,原理可参考:《一个故事讲清楚 NIO》如何实现RPC的IO通信框架?推荐基于netty,现在很多RPC框架都直接基于netty这一IO通信框架,比如阿里巴巴的HSF、dubbo,Twitter的finagle等。这里给自己补一下背景知识:为什么推荐netty.

同步阻塞 IO(BIO)线程模型图由上图我们可以看出,传统的同步阻塞 IO 通信存在如下几个问题: 线程模型存在致命缺陷:一连接一线程的模型导致服务端无法承受大量客户端的并发连接;性能差:频繁的线程上下文切换导致 CPU 利用效率不高;可靠性差:由于所有的 IO 操作都是同步的,所以业务线程只要进行 IO 操作,也会存在被同步阻塞的风险,这会导致系统的可靠性差,依赖外部组件的处理能力和网络的情况。采用非阻塞 IO(NIO)之后,同步阻塞 IO 的三个缺陷都将迎刃而解:就只之前文章提到的Reactor 模式。NIO成为主流的方式。那么为什么不采用 JDK 的 NIO 类库编程呢?还是复杂,难以开发跟维护。推荐netty在功能、性能经过主流商业开发验证。
五 执行调用client stub 所做的事情仅仅是编码消息并传输给服务方,而真正调用过程发生在服务方。server stub 从前文的结构拆解中我们细分了
RpcProcessor
RpcInvoker
两个组件,一个负责控制调用过程,一个负责真正调用。这里我们还是以 java 中实现这两个组件为例来分析下它们到底需要做什么?java 中实现代码的动态接口调用目前一般通过反射调用。除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用,因此
RpcInvoker
就是封装了反射调用的实现细节。调用过程的控制需要考虑哪些因素,
RpcProcessor
需要提供什么样地调用控制服务呢?下面提出几点以启发思考:1. 效率提升 每个请求应该尽快被执行,因此我们不能每请求来再创建线程去执行,需要提供线程池服务。
2. 资源隔离
当我们导出多个远程接口时,如何避免单一接口调用占据所有线程资源,而引发其他接口执行阻塞。
3. 超时控制
当某个接口执行缓慢,而 client 端已经超时放弃等待后,server 端的线程继续执行此时显得毫无意义。
六消息里为什么要带有requestID如果使用netty的话,一般会用channel.writeAndFlush()方法来发送消息二进制串,这个方法调用后对于整个远程调用(从发出请求到接收到结果)来说是一个异步的,即对于当前线程来说,将请求发送出来后,线程就可以往后执行了,至于服务端的结果,是服务端处理完成后,再以消息的形式发送给客户端的。于是这里出现以下两个问题:1)怎么让当前线程“暂停”,等结果回来后,再向后执行?2)如果有多个线程同时进行远程方法调用,这时建立在client server之间的socket连接上会有很多双方发送的消息传递,前后顺序也可能是随机的,server处理完结果后,将结果消息发送给client,client收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的?如下图所示,线程A和线程B同时向client socket发送请求requestA和requestB,socket先后将requestB和requestA发送至server,而server可能将responseA先返回,尽管requestA请求到达时间更晚。我们需要一种机制保证responseA丢给ThreadA,responseB丢给ThreadB。

怎么解决呢?1)client线程每次通过socket调用一次远程接口前,生成一个唯一的ID,即requestID(requestID必需保证在一个Socket连接里面是唯一的),一般常常使用AtomicLong从0开始累计数字生成唯一ID;2)将处理结果的回调对象callback,存放到全局ConcurrentHashMap里面put(requestID, callback);3)当线程调用channel.writeAndFlush()发送消息后,紧接着执行callback的get()方法试图获取远程返回的结果。在get()内部,则使用synchronized获取回调对象callback的锁,再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态。4)服务端接收到请求并处理后,将response结果(此结果中包含了前面的requestID)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到requestID,再从前面的ConcurrentHashMap里面get(requestID),从而找到callback对象,再用synchronized获取callback上的锁,将方法调用结果设置到callback对象里,再调用callback.notifyAll()唤醒前面处于等待状态的线程。
1234567
public
Object get() {
synchronized
(
this
) {
// 旋锁
while
(!isDone) {
// 是否有结果了
wait();
//没结果是释放锁,让当前线程处于等待状态
}
}
}
1234567
private
void
setDone(Response res) {
this
.res = res;
isDone =
true
;
synchronized
(
this
) {
//获取锁,因为前面wait()已经释放了callback的锁了
notifyAll();
// 唤醒处于等待的线程
}
}
******************总结************从rpc概念出发,到目的,到用流程怎么实现。算是把原理梳理完了。但是这不是最重要的。重要是的看看具体应用。那么dubbo是极好的学习对象。先贴个链接:http://shiyanjun.cn/archives/325.htmldubbo大图结束本次学习
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: