了解RPC框架
既然要设计RPC框架,就需要先知道什么是RPC ?
RPC(Remote Procedure Call Protocol)全称远程过程调用 ,像调用本地服务(方法)一样调用服务器的服务(方法)。
RPC又是如何进行调用的呢?
很早的时候,国外的工程师设计了一种能够通过A计算机调用B计算机上边应用程序的技术,这种技术不需要开发人员对于网络通讯了解过多,并且调用其他机器上边程序的时候和调用本地的程序一样方便好用。A机器发起请求去调用B机器程序的时候会被挂起,B机器接收到A机器发起的请求参数之后会做一定的参数转换,最后将对应的程序结果返回给A,这个就是最原始的RPC服务调用了。
通常的RPC架构可分为了以下几个核心组件:
- Client
- Client Stub
- Server
- Server Stub
Client和Server两个名词的概念一般都比较好理解,前一个是客户端(请求发起方),后一个是服务端(服务提供方)。下面来说一下Server Stub和Client Stub是做什么的。
Client Stub: 就是将客户端请求的参数、服务名称、服务地址进行打包,统一发送给server方。
Server Stub: 简单来说就是,服务端接收到Client发送的数据之后进行消息解包,调用本地方法。
具体的调用流程和关系如下图所示:
RPC框架整体分析和设计
上面我们已经知道了RPC框架的一些基本概念,接下来对我们的RPC框架进行进一步的分析和设计。
RPC框架调用流程
本地客户端以本地调用方式调用服务,首先需要通知到本地的存根(Client Stub)
,接着本地存根会进行一些数据格式的包装,网络请求的封装等,最终组装成能够进行网络传输的消息体,按照一定的规则将这个消息体通过Socket
发送到指定的目标机器上。
服务端的存根(Server Stub)
在接收到相关的数据信息之后,需要将其按照事先约定好的规则进行解码,从而识别到消息体内部的信息,然后将对应的请求转发到本地服务对应的函数中进行处理。处理完的数据需要再通过Socket
返回给调用方。
调用方存根在接收到服务方数据的时候,需要进行数据解码,最后得到这次请求的最终结果。
调用的流程如下图所示:
这幅图只是一个最简单的调用流程,接下来我们尝试在这张调用图的基础上 不断地进行功能扩展,从而最终设计出一个比较完善的RPC框架。
代理层设计
RPC最大的特点就是像调用本地服务(方法)一样调用服务器的服务(方法),实现这一点就需要我们在远程调用的时候,将其中内部的细节进行封装屏蔽,让调用者感知不到远程调用的逻辑。
看了上面的描述会不会联想到代理模式
? 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这个时候,访问对象(Client)
不适合或者不能直接引用目标对象(Service)
,代理对象作为访问对象和目标对象之间的中介。
代理模式的主要优点有:
- 代理模式能将代理对象与真实被调用的目标对象分离;
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理模式一定程度上降低了系统的耦合度,增加了程序的可扩展性。
所以面对客户端的请求,我们可以增加一个代理层,统一将内部的细节都屏蔽起来,让调用者使用起来无感知。这个时候我们可以用下图表示这个请求调用的流程:
路由层的设计
当服务提供者有多个的时候,我们就会面临以下问题:当目标服务众多的时候,客户端需要如何确定最终请求的服务提供者是谁呢?
这里就需要引入一个叫做路由的角色
,负责遴选出符合条件的服务提供者,此时客户端的调用整体流程大致如下:
客户端的请求会经过一个叫做路由层的部分,通过路由层内部的规则去选择对应的Server
服务。
协议层的设计
客户端在使用RPC框架进行远程调用的时候,还需要对数据信息进行统一的包装和组织,最后才能将其发送到目标机器并且被目标机器接收解析,因此对于数据的各种序列化、反序列化,协议的组装我们统一可以封装在协议层中进行实现,此时客户端的调用整体流程如下图所示:
router层
会负责计算好最终需要调用的服务提供者具体信息,然后将对应的地址信息、请求参数传输给到protocol层,最终由protocol层
对数据封装为对应的协议体,然后进行序列化处理,最终通过网络发送给到目标机器。
可插拔式组件设计
从客户端本地请求,到protocol层
发送数据,整个链路中可能还需要考虑后续的一些二次扩展设计。例如某些自定义条件的过滤,服务分组等等,所以在设计的时候可以考虑在代理层(proxy)和路由层(router)之间加入一些链路模块。这类设计有点类似于责任链模式
,整体的设计结构大致如下:
可插拔式的组件主要是为了以后方便进行二次扩展。
注册中心层的设计
当服务提供者呈现集群的时候,客户端需要去动态获取服务提供者的诸多信息,那么在这个过程中就需要引入一个叫做注册中心的角色。
服务提供者将自己的地址、接口等详细信息都上报到注册中心模块,并且当服务上线、下线都会通知到注册中心。然后服务调用方只需要订阅注册中心即可。
所以对于注册中心层我们也可以统一抽取一个层面出来,现在我们再来调整下整体的设计结构图:
市面上有很多注册中心技术,常见的组件有ZooKeeper
,Nacos
,Redis
等等。
容错层的设计
在进行远程调用的过程中,难免会出现一些异常的情况。市面上常见的RPC框架在处理调用异常的时候通常都会提供一些容错方面的处理手段,常见手段如下:
- 超时重试:当调用某个provider失败的时候,会重试其他provider,可以设置重试次数;
- 快速失败:当调用某个provider失败的时候,不会重试其他provider,快速返回异常结果;
- 无限重试:请求失败后会自动会自动记录在失败队列中,并由一个定时线程重试;
- 异常回调:出现异常后,回调指定方法;
- 无视失败:出现异常后,不做任何处理。
- ……
面对这种异常的场景,我们可以尝试将这些处理手段统一抽象出来,交给容错层
去处理,所以此时我们需要再对这款RPC框架进行修改:
服务提供者的线程池设计
当请求发送到了服务提供者的时候,服务提供方需要对其进行相应的解码,然后在本地进行核心处理。我们如果想提升服务器的并发访问,这部分的工作需要交给专门的线程去计算处理。此时我们再对RPC框架的调用图进行修改,大致如下:
好了到这里,RPC框架已经设计的差不多了,这个就是我们最终设计的RPC框架。
总结
最后我们再做一下总结,在上面一步一步设计完RPC的框架之后,我们的RPC框架的整体结构基本分层为:
- 代理层:负责对底层调用细节的封装;
- 链路层:负责执行一些自定义的过滤链路,可以供后期二次扩展;
- 路由层:负责在集群目标服务中的调用筛选策略;
- 协议层:负责请求数据的转码封装等作用;
- 注册中心:关注服务的上下线,以及一些权重,配置动态调整等功能;
- 容错层:当服务调用出现失败之后需要有容错层的兜底辅助;
另外再附一张业界知名RPC框架Dubbo
中各个层依赖关系图作对比:(来自Dubbo官方文档)
🚀🚀🚀