一文搞懂RPC框架/RPC框架解密

内容分享15小时前发布 蔡曌
0 1 0

前言

本文介绍了什么是RPC协议,及为什么用RPC框架,RPC框架的原理和使用到的技术及技术的解密,然后手写一个简单的RPC框架进行本质上的理解,最后是对RPC框架的选型参考提议。

一、RPC 是什么?

RPC(Remote Procedure Call Protocol)远程过程调用协议

里边有两个关键字:远程 和 过程。

  • 远程:指的是需要经过网络的,而不是应用内部、机器内部进行的。
  • 过程:也就是方法。

一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

那么我们至少从这样的描述中挖掘出几个要点:

  • RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo(阿里)、Thrift(脸书)、GRPC(谷歌)等。
  • 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。
  • 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。
  • 应该有跨语言能力:为什么这样说呢?由于调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

二、为什么会有RPC框架

所有框架的产生都是由需求驱动的。当我们开发简单的单一应用,逻辑简单、用户不多、流量不大,那我们用不着。当我们的系统访问量增大、业务增多时,我们会发现一台单机运行此系统已经无法承受。此时,我们可以将业务拆分成几个互不关联的应用,分别部署在各自机器上,以划清逻辑并减小压力。此时,我们也可以不需要RPC,由于应用之间是互不关联的。

当我们的业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。此时,可以将公共业务逻辑抽离出来,将之组成独立的服务Service应用 。而原有的、新增的应用都可以与那些独立的Service应用 交互,以此来完成完整的业务功能。

所以此时,我们急需一种高效的应用程序之间的通讯手段来完成这种需求。

1、RPC框架是单体服务向分布式服务转化的产物,也是遵循“高内聚、低耦合”的一个体现,通过RPC来提供服务级别的能力,本质上是复用。

2、RPC框架屏蔽了底层调用的细节(虚线部分),开发者应用更简单。

一文搞懂RPC框架/RPC框架解密

三、RPC框架原理

要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:

一文搞懂RPC框架/RPC框架解密

  1. 服务消费方(client)调用以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  3. client stub找到服务地址,并将消息发送到服务端;
  4. server stub收到消息后进行解码;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给server stub;
  7. server stub将返回结果打包成消息并发送至消费方;
  8. client stub接收到消息,并进行解码;
  9. 服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

下面是网上的另外一幅图,感觉一目了然:

一文搞懂RPC框架/RPC框架解密

四、RPC用到的技术

1、动态代理

怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强劲和高效,但代码维护不易,大部分公司实现RPC框架时还是选择JDK 提供的原生动态代理,也可以使用开源的:Cglib 代理,Javassist 字节码,byteBuddy字节码生成技术。

下面简单介绍下动态代理怎么实现我们的需求。我们需要实现RPCProxyClient代理类,代理类的invoke方法中封装了与远端服务通信的细节,消费方第一从RPCProxyClient获得服务提供方的接口,当执行
helloWorldService.sayHello(“test”)方法时就会调用invoke方法

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;
    }
}

public class Test {
     public static void main(String[] args) {         
     HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);
     helloWorldService.sayHello("hello world!");
    }
}

2、序列化

在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。

序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。

为什么需要序列化?转换为二进制串后才好进行网络传输

反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。

为什么需要反序列化?将二进制转换为对象才好进行后续处理

现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看三点:

  • 通用性:列如是否能支持Map等复杂的数据结构;
  • 性能:包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将超级可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
  • 可扩展性:对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。

目前比较高效的开源序列化框架:如 Protobuf、Kryo、fastjson 等。

自定义二进制协议来实现序列化

一文搞懂RPC框架/RPC框架解密

3、NIO 通信

NIO(Non-blocking I/O)。

出于并发性能的思考,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。

Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。

可以选择 Netty 或者 mina 来解决 NIO 数据传输的问题。

4、注册中心(服务注册和发现)

可选:Redis、Zookeeper、Consul、Etcd、Nacos

一般使用 Zookeeper 提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。

(1)服务注册与发现流程

服务注册:服务提供方将对外暴露的接口发布到注册中心内,注册中心为了检测服务的有效状态,一般会建立双向心跳机制。

服务订阅:服务调用方去注册中心查找并订阅服务提供方的 IP,并缓存到本地用于后续调用。

(2)如何实现:基于ZK

A. 在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例

如:
/micro/service/com.orderService),在这个路径再创建服务提供方与调用方目录(server、

client),分别用来存储服务提供方和调用方的节点信息。

B. 服务端发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储注册信息。

C. 客户端发起订阅时,会在服务调用方目录中创建一个临时节点,节点中存储调用方的信息,同时watch 服务提供方的目录(
/micro/service/com.orderService/server)中所有的服务节点数据。当服务端产生变化时ZK就会通知给订阅的客户端。

ZooKeeper方案的特点:

强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。

5、服务治理

熔断、限流、监控(健康监测)

1、限流

在Dubbo框架中, 可以通过Sentinel来实现更为完善的熔断限流功能,服务端是具体如何实现限流逻辑的?

方法有许多种, 最简单的是计数器,还有平滑限流的滑动窗口、漏斗算法以及令牌桶算法等等。Sentinel采用是滑动窗口来实现的限流。

2、熔断

熔断器有三种状态:

CLOSE:关闭

OPEN:打开

HALF_OPEN:半开

其中,熔断器处于OPEN状态时,链路处于非健康状态,命令执行时,直接调用返回逻辑,跳过正常逻辑。熔断器默认处于CLOSE状态,CLOSE状态时,链路处于健康状态。

熔断器状态变迁图:

一文搞懂RPC框架/RPC框架解密

  • 红线:初始时,熔断器处于CLOSE状态,链路处于健康状态。当满足如下条件,断路器从 CLOSED 变成 OPEN 状态:

周期: 统计健康状态的时间窗口(可配,
circuitBreakerRollingTimeInMilliseconds)内,总请求数超过必定量(可配,
circuitBreakerRequestVolumeThreshold:单位时间内访问数达到的基值)。

错误率:错误请求数占总数请求数超过必定比例,失败率达到多少百分比后熔断(可配,
circuitBreakerErrorThresholdPercentage)

重试时间:触发熔断后会设置一个延时时间,熔断多少秒后去尝试请求(可配,
circuitBreakerSleepWindowInMilliseconds)

备注:满足条件的统计数据是由record进行记录并触发熔断。

  • 绿线:熔断器处于OPEN状态,命令执行时,若当前时间超过熔断器开启时间必定时间,熔断器变成HALF_OPEN状态,尝试调用正常逻辑,根据执行是否成功,打开或关闭熔断器[蓝线]

如果执行成功:清空record的计数统计,更新熔断器状态为 关闭 如果执行失败:继续设置一个延时时间,更新熔断器状态为 打开

3、健康监测

为什么需要做健康监测?

列如网络中的波动,硬件设施的老化等等。可能造成集群当中的某个节点存在问题,无法正常调用。

健康监测实现分析

心跳检测的过程总共包含以下状态:健康状态、波动状态、失败状态。

完善的解决方案

(1)阈值: 健康监测增加失败阈值记录。

(2)成功率: 可以再追加调用成功率的记录(成功次数/总次数)。

(3)探针: 对服务节点有一个主动的存活检测机制。

6、服务路由、策略选择

1)、负载均衡算法

a、静态负载均衡算法

轮询(Round Robin):服务器按照顺序循环接受请求。

随机(Random):随机选择一台服务器接受请求。

权重(Weight):给每个服务器分配一个权重值,根据权重来分发请求到不同的机器中。

IP哈希(IP Hash):根据客户端IP计算Hash值取模访问对应服务器。

URL哈希(URL Hash):根据请求的URL地址计算Hash值取模访问对应服务器。

一致性哈希(Consistent Hash ):采用一致性Hash算法,一样IP或URL请求总是发送到同一服务器。

b、动态负载均衡算法

最少连接数(Least Connection):将请求分配给最少连接处理的服务器。

最快响应(Fastest Response):将请求分配给响应时间最快的服务器。

观察(Observed):以连接数和响应时间的平衡为依据请求服务器。

预测(Predictive):收集分析当前服务器性能指标,预测下个时间段内性能最佳服务器。

动态性能分配(Dynamic Ratio-APM):收集服务器各项性能参数,动态调整流量分配。

服务质量(QoS):根据服务质量选择服务器。

服务类型(ToS): 根据服务类型选择服务器。

c、自定义负载均衡算法

灰度发布:平滑过渡的发布方式,可以降低发布失败风险,减少影响范围,发布出现故障时可以快速回滚,不影响用户。

版本隔离:为了兼容或者过度,某些应用会有多个版本,保证1.0版本不会调到1.1版本服务。

故障隔离:生产出故障后将出问题的实例隔离,不影响其他用户,同时也保留故障信息便于分析。

定制策略:根据业务情况定制跟业务场景最匹配的策略。

2)、优雅启动

(1)什么是启动预热?

启动预热就是让刚启动的服务,不直接承担全部的流量,而是让它随着时间的移动慢慢增加调用次数,最终让流量缓和运行一段时间后达到正常水平。

(2)如何实现?

第一要知道服务提供方的启动时间,有两种获取方法:

一种是服务提供方在启动的时候,主动将启动的时间发送给注册中心;

另一种就是注册中心来检测, 将服务提供方的请求注册时间作为启动时间。

调用方通过服务发现获取服务提供方的启动时间, 然后进行降权,减少被负载均衡选择的概率,从而实现预热过程。

3)、优雅关闭

(1)为什么需要优雅关闭?

调用方会存在以下情况:目标服务已经下线;目标服务正在关闭中。

(2)如何实现优雅关闭?

当服务提供方正在关闭,可以直接返回一个特定的异常给调用方。然后调用方把这个节点从健康列表挪出,并把其

他请求自动重试到其他节点。如需更为完善, 可以再加上主动通知机制。

在Dubbo框架中, 在以下场景中会触发优雅关闭:

JVM主动关闭( System.exit(int) ; JVM由于资源问题退出( OOM ); 应用程序接受到进程正常结束信号:SIGTERM 或 SIGINT 信号。

优雅停机是默认开启的,停机等待时间为10秒。可以通过配置
dubbo.service.shutdown.wait 来修改等待时间。Dubbo 推出了多段关闭的方式来保证服务完全无损。

具体步骤:

  1. 开启关闭挡板,拒绝新的请求
  2. 利用引用计数器确保正在执行的请求处理完
  3. 设置超时时间,保证服务可以正常关闭
  4. 执行关闭时,服务提供方通知服务调用方下线相关节点

服务优雅关闭的示意图如下。

一文搞懂RPC框架/RPC框架解密

7、可观察性

trace、log、metric

五、RPC简单实现示例

为了让大家更好地理解RPC的实现,我们直接编写了一个极简(可能是最简单的了)的RPC示例项目。该实例只用少量的几个类便实现了RPC功能,并且配有服务调用方和服务提供方的展示示例。成熟的框架要比我们的项目复杂的多,但越是简单的项目,越能让大家理解其基本原理。项目地址为:

https://github.com/feigaoalibaba/EasyRPCDemo

下面是客户端的代码结构,看着类有点多,实则代码不长。其中的RPC代码完成完成动态代理、远程调用参数序列化、远程调用发起、远程调用结果反序列化的工作。

一文搞懂RPC框架/RPC框架解密

下面是服务端的代码结构,代码更少,完成远程调用接收、调用参数反序列化、调用实际触发、调用结果序列化的工作。

一文搞懂RPC框架/RPC框架解密

六、常见框架及比较

大厂常用框架:dubbo、grpc、Thirft、Spring Cloud,还有些基予dubbo封装改造的,如京东的JSF、五八的SCF。

框架

开发语言

服务治理

多种序列化

多种注册中心

管理中心

跨语言通信

整体性能

dubbo

Java

支持

不支持

grpc

跨语言

只支持pb

支持

Thirft

跨语言

只支持thrift

支持

说明:

1、dubbo包含服务治理能力,而Grpc的定位为通信层协议,并不提供连接池、服务框架、服务治理,谷歌给出的解决方案是Istio(服务网格mesh)

一文搞懂RPC框架/RPC框架解密

七、小结

1、RPC是一种远程方法调用协议,常见的RPC框架有 Dubbo、Thirft、Grpc。RPC框架是单体服务向分布式服务转化的产物,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简化了开发。

2、RPC原理和相关技术:包括动态代理、对象序列化、NIO通信、服务注册和发现、服务治理、服务路由和策略选择等。

3、通过撸一个简单的RPC框架代码理解其本质。

© 版权声明

相关文章

1 条评论

  • 头像
    尼尼 读者

    这个厉害了👏

    无记录
    回复