带你手把手实操一个RPC框架

7,696 阅读11分钟

前言:

这篇文章我们来聊一聊RPC框架,为什么要聊RPC呢 ?

首先从个人成长角度,如果一个新时代码农能清楚的了解RPC框架所具备的要素,掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异步调用、熔断降级等技术,可以全方位的提升基本素质。

其次,目前市面上也有非常多优秀的框架,GitHub上也有相关源码,但好记性不如烂笔头,只有自己真正了解并且动手去尝试写一个RPC框架,才是我们去掌握这门技术的最优路径。

一、介绍

研究一个概念或者框架,带着三个W去考虑,可能会对他有更加深刻的了解:

1)What,什么是RPC框架,RPC是 Remote Procedure Call 的简称,远程过程调用,那么什么叫远程过程调用呢,你可以理解为我们调用外部(远程)服务就像调用自己本地方法一样。

2)Where,RPC框架用在什么地方,在分布式系统和微服务盛行的今天,各业务系统会被独立拆分出来成为一个个独立的web应用,应用之间的交互和数据传输就成了必不可少的一环,RPC就是为了实现独立服务之间远程交互的框架。

3)Why,为什么需要一个RPC框架,服务之间的调用需要各种场景和因素的考虑,内部原理非常复杂和繁琐,同时在集群情况下,服务的负载均衡,熔断,限流等都是需要去考虑的,这时候就需要一个集服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异步调用、熔断降级等技术为一体的技术去完成这些公共功能,RPC框架就是在这种情况下应运而生。

目前比较主流的RPC框架包括谷歌开源的GRPC、阿里巴巴的Dubbo、Netflix 的SpringCloud等。

二、RPC框架基本组成

RPC框架需要的最基本的三个要素:

  • ServiceProvider: 服务提供方,提供相关服务接口。
  • ServiceConsumer: 服务消费方,消费服务提供方的接口。
  • Registry: 注册中心,用于进行服务的注册、发现、治理、高可用。

基于三个最基本要素,还会延伸出包括负载均衡器、熔断降级器、通信协议组件、序列化协议等等组件。

一个最简单的RPC调用模型图如下所示:

640-132.png

下面做一些名词的介绍和解释:

2.1 注册中心

注册中心是RPC框架中的管理者和协调者角色,虽然在远程过程调用中服务消费者会不经过注册中心,会直接向服务提供者发送请求,但是随着我们的服务方越来越多,每个服务的实例也不断变化的,且每个服务的地址,端口等信息是需要通知到消费方的,所以我们需要一个类似“管家”的角色,来负责管理服务注册和发现的工作,这个“管家”我们称之为注册中心。

一个合格的注册中心需要具备包括缓存和持久化服务提供方数据,动态更新服务提供者信息,动态监听服务提供方节点变化,推送节点变化到消费方,查询服务提供方数据等功能。

目前市面上比较流行的注册中心有:Zookper、 Nacos、Consul、Eurake等,针对于上面功能的实现方式也有所不同,以下是注册中心的对比:

ZookeeperNacosConsulEurake
一致性协议CPCP + APCPAP
雪崩保护
多数据中心不支持支持支持支持
自动注销实例支持支持支持支持

2.2 服务提供方(RPC服务端)

其需要对外提供服务接口,一个服务方需要包括启动连接注册中心,注册相关信息到注册中心,提供服务下线和更新机制,维护服务名和服务的映射,序列化和反序列化,启动通信等

目前服务提供方有两种服务提供维度,基于接口的服务提供和基于服务的服务提供,Dubbo3.0 之前是基于接口维度做的服务注册,Dubbo3.0之后渐渐向服务维度的服务注册发现靠拢,SpringCloud是基于服务来进行注册发现的,在云原生和容器化越来越火的今天,基于服务可以更好的适配容器化和云原生。

2.3 服务消费方(RPC消费端)

服务消费方需要具备可以从注册中心拉取服务列表,缓存服务列表,动态监听和更新服务列表的功能,还需要具备针对于服务的负载均衡策略,序列化和反序列化,根据约定的通信协议进行调用等。

目前Dubbo是基于代理和Spring的BeanDefination来实现的,在消费启动的时候会去扫描基于自定义注解或配置的信息,然后生成一个相应的代理对象,注册到Spring容器中,在调用的时候直接通过代理类进行相关一系列调用。SpringCloud是基于HTTP协议和增强版的RestTemplate来实现的,内部实现了Ribbon的负载均衡,消费方可以通过Feign或者RestTemplate实现远程调用。

2.4 通讯框架

通讯框架是服务之间进行IO交互和传输的保证,消费端需要通过通讯框架和提供方进行交互,获取数据,目前市面上主流的基于Java的NIO通讯框架就是Netty。

2.5 通讯协议

通讯协议是消费端和服务端约定好一种交互协议,当消费端拿到服务端提供的IO流之后,需要根据通讯协议获取具体的数据内容,目前TCP通讯协议比较通用的是HTTP、FTP、SMTP协议等,SpringCloud是基于HTTP的通讯协议实现的,Dubbo内部自己集成了一套通讯协议。

业界的主流协议的解决方案可以归纳如下:

  • 消息定长,例如每个报文的大小为固定长度100字节,如果不够用空格补足。
  • 在包尾特殊结束符进行分割。
  • 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。

通过对比,我们发现第三点是最为灵活和可拓展的,一般推荐都会使用第三种。

2.6 序列化

2.6.1 概念介绍

序列化(serialization)就是将对象序列化为二进制形式(字节数组),一般也将序列化称为编码(Encode),主要用于网络传输、数据持久化等。

反序列化(deserialization)则是将从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务的进行,一般也将反序列化称为解码(Decode),用于网络传输对象的解码,以便完成远程调用。

2.6.2 序列化协议

  • XML & SOAP

XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。XML历史悠久,其1.0版本早在1998年就形成标准,并被广泛使用至今,目前金融和银行行业使用较多。

  • JSON

JSON 全称 (Javascript Object Notation) 起源于弱类型语言Javascript, 它的产生来自于一种称之为”Associative array”的概念,其本质是就是采用”Attribute-value”的方式来描述对象。实际上在Javascript和PHP等弱类型语言中,类的描述方式就是Associative array。JSON的具有数据简单,可接受程度高,结构简洁,序列化包体小等特点,目前大部分的公司都是使用这种序列化协议来实现的。

  • Protobuf

由谷歌开发的一款高性能序列化框架,是一个纯粹的展示层协议,可以和各种传输层协议一起使用,目前支持Java、C++、Python 等多种语言,他具有更小的数据量,更快的解析速度,简单的调用等特点,目前得物使用的Dubbo2.7.7版本 默认使用这种序列化协议。

2.7 负载均衡

负载均衡是保证服务提供方在多实例的情况下保证负载的均衡的一种策略,目前大体有如下几种负载均衡策略:

1)轮训,采用计数器的方式,根据计数器的值和实例数量进行取余。

2)随机,采用随机请求的方式,随机一个Random的数值,根据random进行取余。

3)加权轮训,采用权重的方式,给每一个实例配置不同的权重比例,通过比例选择合适的实例。

4) 一致性Hash,采用Hash环的方式,大体的实现思路是通过寻址的方式找到就近的一个节点,具体可以自行网上搜索一下相关文档理解。

三、实现

既然是当作练手实现一个RPC框架,所以会尽量借鉴当前主流的框架,技术选型方面:

注册中心:选择Zookeeper来实现。

服务提供方: 选择基于服务维度来实现服务发现,目前主流框架都是基于这个来做的。

服务消费方: 选择Dubbo类似的基于代理Spring的BeanDefination来实现。

通讯框架: Netty。

通讯协议: 采用上述的第三种方式。

序列化协议: 支持JSON 和 Protobuf。

负载均衡: 实现轮训和随机。

当然,上述的组件都是使用SpringBoot的SPI方式来进行选择的,后续如果需要进行拓展和按不同配置加载,可以通过配置的方式来实现插件的可插拔。

废话不多说,先贴上项目整体结构:

640-133.png

整体代码结构如上,下面针对模块一一讲解:

3.1 注册中心

代码实现如下:

因为我们是以可拓展和接口方式实现的,所以我们定义了一些接口,通过不同的实现来进行区分,后期可以通过不同的配置来选择合适的注册中心。

640-134.png

3.2 服务提供方

服务提供方通过使用Spring的事件来进行监听,同时根据声明式的注解来进行解析和注册,同时通过组装元数据,将自己的服务注册到注册中心,完成服务提供方的服务注册,同时启动Netty的客户端,来进行客户端的监听,从而进行服务调用,具体流程图如下:

640-135.png

部分实现代码如下:

自动装备逻辑:

640-136.png

使用和服务注册逻辑:

640-137.png

3.3 服务消费方

服务消费方的逻辑稍微复杂一些,需要通过自动装配来创建新的BeanDefination, 然后从所有的Bean中找到相关的带有RPC注解的参数,重写BeanDefination,重写的Bean需要构建新的元数据和存入客户端缓存,然后通过动态代理的方式,创建一个代理对象,对象使用负载均衡在服务发现的时候选择一个地址,通过Netty的方式进行通信和数据交互,最后返回相关数据对象,具体的流程如下图:

640-138.png

部分实现代码如下:

启动开始类代码:

640-139.png

640-140.png

代理类实现代码:

640-141.png

负载均衡实现代码:(SPI可拓展模式)

目前采用轮训和随机的方式实现的,后期可根据接口进行拓展。

640-142.png

3.4 通讯框架

我们使用Netty4实现了通讯框架,采用了NettyClient和NettyServer来实现:

640-143.png

640-144.png

3.5 通讯协议

我们目前使用了自定义的通信协议来实现:

第一个字节是魔法数,比如我定义为0X35。

第二个字节代表协议版本号,以便对协议进行扩展,使用不同的协议解析器。

第三个字节是请求类型,如0代表请求1代表响应。

第四个字节表示消息长度,即此四个字节后面此长度的内容是消息content。

640-145.png

3.6 序列化协议

目前支持JSON和ProtoBuf,后期我们可以通过SPI进行拓展和可配置。

640-146.png

整体核心调用流程:

640-147.png

四、总结

本文从整体名词介绍、内部组件组件介绍等两个方面阐述了RPC的框架模型,从技术选型、具体代码等实现了一个RPC框架并应用到项目中。目前RPC框架整体搭建完成,可以正常通过注解接入业务方使用,但还有很多细节需要更仔细地去考虑和思索,比如系统的可拓展性和框架的整体性能等。希望通过阅读本篇文章,可以让读者对RPC有更清晰的了解,让自己的基础技能有一个更高的提升。

*文/Wade

关注得物技术,每周一三五晚18:30更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~