从零开始实现RPC框架–(1)基本原理
一.RPC概述
RPC即是远程过程调用(Remote Procedure Call),允许一台计算机调用另一台计算机上的程序得到结果,而代码中不需要做额外的编程,就像在本地调用一样。
现在互联网应用的量级越来越大,单台计算机的能力有限,需要借助可扩展的计算机集群来完成,分布式的应用可以借助rpc来完成机器之间的调用。
二.RPC框架原理
在RPC框架中主要有两个角色,client和server。一台服务器作为server向外提供服务,同时其内部又调用了其它的服务,这时它也是另外一个服务的client。
1.RPC框架原理
下面通过分析一下一次rpc调用的过程来大致了解RPC的原理
- 客户端调用了某个服务的某个方法,期望得到处理的结果
- 把本次调用的上下文,如服务名、方法签名、参数等信息序列化,构造request
- 根据被调用的服务名,方法签名等信息找到可以提供服务的server列表
- 根据负载均衡的规则,选出其中一个server作为目标来调用
- 向选出的server发送该请求,客户端线程挂起
- server接收到请求,反序列并解析得到对应的服务名,方法签名,参数信息
- server根据调用信息找到真正的业务服务实例,调用业务服务该方法
- 把方法的返回值序列化,构造返回response
- 把response传回给client
- client接收并反序列化response,得到服务处理结果,返回给1中调用的地方,唤醒对应的客户端线程
2.实现一次RPC请求要解决的问题
原理并不复杂,但实现起来每一个步骤中都有需要解决的问题,对应一种或多种解决的方案,一个个来分析:
1.客户端调用了某个服务的某个方法,期望得到处理的结果
Q:如何自然的发起RPC调用?
client调用服务时希望尽量能够自然,就像在调用本地方法一样,比如要调用的服务是UserService,那么希望调用的时候也是userService.getUser(userId)这样的形式,客户端代码编写阶段就清晰的看到服务可提供的方法的签名
A:利用代理模式,这里需要的是一个和userService有一样接口的代理,在其方法被调用的时候执行远程请求的逻辑,以java为例,可以用JDK的动态代理代理或者AOP来实现。
2.把本次调用的上下文,如服务名、方法签名、参数等信息序列化,构造request
Q:如何进行序列化?
序列化其实是协议的一部分,网络一端以什么样的规则把数据对象序列化成二进制串,网络传输到了另一端后以什么样的规则解析回来。
A:成熟的序列化实现方式已经有很多,不需要自己来实现,比如JDK自带的序列化,jackson,hession,protostuff等,我们会根据各种序列化的速度,序列化后占用空间大小等方面来选择,详见各种序列化性能比较。
3.根据被调用的该服务名,方法签名 找到可以提供服务的server列表
Q:server列表存在哪里?
A:这里所说的是一个服务注册中心,在组件启动的时候可以加载当前正在提供服务的server列表,这意味注册中心需要能够存储这个列表,并且在server服务器上/下线、心跳中断/恢复时及时更新列表,并且注册中心必须保证高可用,不能由于一两台机器挂掉就变得不可用,也就是说需要集群和冗余的支持。听起来有些复杂,其实业界已经有成熟的方案了,可以选择利用zookeeper来实现,其高可用性,支持集群和冗余,pub/sub的功能都是注册中心需要的。
另外,除了注册中心外,client上也可以在内存中维护一份列表,启动时访问注册中心初始化,在注册中心列表变化时接收通知同步修改,这样做的好处是不用每次调用都访问注册中心。
4.根据负载均衡的规则,选出其中一个server作为目标来调用
Q:负载均衡由谁实现?如何实现?负载均衡的策略如何?
A:负载均衡可以作为独立模块,也可以直接放在客户端中,两种方案各有利弊:
独立部署的好处是客户端的职责单一,只负责调用,负载均衡模块可以根据全局的服务器状态来选择合适的server,缺点是客户端每次请求都要到负载均衡中心去请求server地址,多了一次网络请求,而且如果负载均衡中心挂了,服务就会立刻不能访问。
如果放在客户端上,客户端本地也维护一套注册中心的server列表,并且接收注册中心消息进行同步变更,每次就可以直接从本地的server列表中取出使用,但是这时的负载均衡策略就只能根据当前客户端调用server的情况来进行,比如当前客户端列表中有a,b,c三个server,可以轮询访问,从所有的客户端角度来看也是均衡的。
5.向第4步中选出的server发送该请求,客户端线程挂起
Q:如何选择网络传输协议?同步/异步IO?
A:网络传输协议的选择,常用的一般有TCP和HTTP两种方式,TCP方式传输效率高,但是实现起来复杂,HTTP方式更通用,开发起来更简单,但是传输效率不高。
同步的发送方式是发出请求后线程阻塞等待服务处理返回,实现起来容易,但是性能会差很多,一次请求发送时,阻塞IO会占有线程,不能使CPU得到充分的利用。而在异步IO的情况下,请求网络传输不阻塞,CPU可以处理其它的线程,而等传输完成后再来计算,系统吞吐量会大很多。但是异步IO实现起来会比较复杂,一般我们借助netty,mina等nio通讯框架实现。
6.server接收到请求,反序列化并解析得到对应的服务名,方法签名,参数信息
Q:如果RPC支持多种序列化方式的扩展,接收的一端如何知道用哪种方式来进行反序列化?
A:得到二进制串之后,如果是可支持多种序列化协议的,需要我们自己做一部分的二进制串解析工作,比如我们约定第第一个字节代表选择序列化的方式,收到二进制串后先读出这个字节,选择对应的反序列化协议解析后面的字节。
7.server根据调用信息找到真正的业务服务实例,调用业务服务该方法
Q:如何根据服务名,函数参数找到对应的服务实例?
A:一般的应用都会用spring来管理,比如像服务中调用服务的service,让spring来管理,组装完成之后再返回使用起来非常方便,所以我们要根据服务名从spring中找到对应的bean,这里需要借助反射从服务名到class的转换,然后同样借助反射完成方法的调用。
8.把方法的返回值序列化,构造返回response
序列化时也要同6中所述,把序列化方式按照同样的规则写在二进制串中。
9.把response传回给client
对应步骤5中选择的网络协议及通讯方式实现
10.client接收并反序列化response,得到服务处理结果,返回给步骤1中调用的地方,唤醒对应的客户端线程
Q:如果是异步调用,如何让客户端在调用时同步得到结果?
A:这里涉及到一个异步转同步的问题,客户端在发出请求后,可以让当前线程等待在一个锁上,并把锁对象放在以该请求id为key的map中,而收到返回的response的回调时,根据reponse中的之前的请求Id从内存中的这个map中拿到这个锁,唤醒客户端线程,继续取走并返回的reponse。
3.启动流程
1.client在启动时需要从服务注册中心同步自己关心的服务的地址列表,并在服务列表发生变化时更新列表。 2.server在启动时需要向服务注册中心注册自己能够提供服务的地址和端口。 3.监控系统定时向server发送心跳请求来确认server的可用性,如果不可用通知注册中心踢出列表,重试可用后再添加等。
下一篇文章我们将详细分析并实现RPC框架的主流程,源码会放在git上供大家参考,如果有问题,欢迎一起讨论。
文章欢迎转载,转载时请保留作者与原文链接
作者:赵轩辰
本文原文地址:http://zxcpro.github.io/blog/2015/12/10/cong-ling-kai-shi-shi-xian-rpc-kuang-jia-1-ji-ben-yuan-li/