远程过程调用RPC 3:高性能的RPC框架

前文介绍了RPC思想和框架,可以移步:
远程过程调用RPC 1:举例理解
远程过程调用RPC 2:RPC思想与RPC框架

RPC框架的需求与性能

在前一篇文章中介绍了RPC框架的一些内容。一个高性能的RPC框架应该拥有在一些模块实现一些基本功能并提升自己的性能,可以是:

  • 服务发现与注册
  • 线程调度模型
  • I/O调度模型
  • 序列化框架
  • 负载均衡策略
  • 网络传输

其中最核心的是I/O调度模型、线程调度模型、序列化框架

序列化框架

在这里插入图片描述

在RPC调用的时候,需要把各种参数和调用信息,序列化成字节流传递给服务提供方,再把二进制字节流反序列化得到各种信息;在调用结束后,也需要将返回结果序列化和反序列化,最终调用方得到结果。
因此,序列化框架的选择对于一个RPC框架的性能影响是非常显著的。
影响序列化性能的关键因素:

  • 序列化后的码流大小(网络带宽的占用)
  • 序列化的性能(CPU资源占用)
  • 是否支持跨语言(异构系统的对接和开发语言切换)
序列化框架优点缺点
jdk原生无法跨语言、序列化后的码流太大、序列化的性能差
Fastjson接口简单易用、目前java语言中最快的json库过于注重快,而偏离了“标准”及功能性
Protobuf序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python
Hessian采用二进制协议的轻量级二进制web service协议,支持动态类型、跨语言无法跨语言、序列化后的码流太大、序列化的性能差
Kryo基于Protobuf协议,只支持java语言,需要注册(Registration),然后序列化(Output),反序列化(Input)无法跨语言

但是,在进行选型的时候,还是要根据业务实际情况进行选择,可以考虑几个问题:

  • 是否需要跨平台、跨语言
  • 序列化速度
  • 生成的体积

I/O模型与线程模型

服务方

常见的I/O模型有:同步阻塞I/O(BIO)非阻塞I/O(NIO),这里的I/O主要理解为网络I/O,例如事件轮询、编解码、数据传输等。采用多线程非阻塞模型可以提升这方面的性能。
在这里插入图片描述
图源:Dubbo官网-线程模型
线程模型主要可以理解为业务线程和I/O线程。业务线程主要执行的是业务逻辑,如果业务非常的复杂,例如需要频繁的操作数据库、进行复杂业务计算的话,就需要我们将业务线程分配到业务线程池中去,防止业务操作一直阻塞I/O线程。
上图是Dubbo的线程模型,可以看出,此时我们需要一个dispatcher将业务线程分配到业务线程池中。当然,也并不是必须分配到业务线程池中,这个是可选的。当业务逻辑简单时,可以直降将业务逻辑交由I/O线程去处理。这里Dubbo就提供了五种可选方案,具体Dubbo的内容后面文章再详细介绍。

客户端

成熟的RPC框架需要能够提供多种的调用方式:

  • 同步Sync
  • 异步Future
  • 回调Callback
  • 单向Oneway

RPC框架的性能和吞吐量与合理使用调用方式是息息相关的。

同步Sync

客户端线程发起RPC调用后,当前线程会一直阻塞,直至服务端返回结果或者处理超时异常。一般RPC框架默认的调用方式就是同步调用。所以客户端需要设置超时时间。
在这里插入图片描述
图源:23 架构设计:如何实现一个高性能分布式 RPC 框架

异步Future

同步调用发起后,客户需要一直等待有返回之后才能够继续客户逻辑,在此之间会被阻塞。因此,考虑异步的方式,客户端发起调用后不会再阻塞等待,而是拿到RPC框架返回的Future对象,调用结果会被服务端缓存,客户端自行决定后续何时获取返回结果。
在这里插入图片描述
图源:23 架构设计:如何实现一个高性能分布式 RPC 框架

回调Callback

其实设想现实中的场景,一个人(客户端)要求另外一个人(服务端)去做一件事情,一个非常正常的想法就是“你做完告诉我一声”。那么在进行设计的时候也可以通过回调的方式进行交流。回调的实现是接口或抽象类来实现的,我们把实现这种接口的类称为回调类,回调类的对象称为回调对象。
在这里插入图片描述
客户端发起调用时,将Callback对象传递给RPC框架,无须同步等待返回结果,直接返回。当获取到服务端响应结果或者超时异常后,再执行用户注册的Callback回调。所以Callback接口一般包含onResponse和onException两个方法,分别对应成功返回和异常返回两种情况。

单向调用Oneway

客户端发起请求之后直接返回,忽略返回结果。
在这里插入图片描述
图源:23 架构设计:如何实现一个高性能分布式 RPC 框架

网络传输

作为RPC思想两大核心内容,网络传输自然是对RPC框架的性能产生很重大的影响。
大部分主流RPC框架会选择TCP、HTTP协议,出名的gRPC框架使用的则是HTTP2。TCP、HTTP、HTTP2都是稳定可靠的,但其实使用UDP协议也是可以的,具体看业务使用的场景。

代理

就像在第一篇里面讲到的,需要依赖动态代理的方式,以实现屏蔽掉框架调用细节。代理类是在运行时生成的,所以代理类的生成速度、生成的字节码大小都会影响 RPC 框架整体的性能和资源消耗。
常见的动态代理方式有几种:

  • JDK动态代理
    通过反射调用的形式代理类中的方法,性能肯定低于直接调用。在运行时可以动态创建代理类,但是JDK动态代理的功能比较局限,代理对象必须实现一个接口,否则抛出异常。
  • Cglib动态代理
    Cglib 是基于 ASM 字节码生成框架实现的,通过字节码技术生成的代理类。所以性能要优于反射,且代理类型是不受限制的。代理方法方面,Cglib是有优势的,它采用了FastClass机制,可以效率更该的定位要调用的方法
  • Javassist和ASM
    二者都是Java字节码操作框架,使用起来难度较大。比反射的性能要高。

负载均衡

在分布式系统中,服务提供者和服务消费者都会有多台节点,采用合适的负载均衡策略是至关重要的,会影响PRC框架的吞吐量。受制于服务器性能等各方面的不同,合理的负载均衡策略才能发挥整体服务集群的性能。
几个常见的负载均衡策略:

  • Round-Robin 轮询
    依次轮询
  • Weighted Round-Robin 权重轮询
    按照不同服务节点的服务能力进行控制,增加权重系数,根据能力调整负载均衡状态
  • Least Connections 最少连接数
    客户端根据服务端节点当前的连接数进行负载均衡,客户端会选择连接数最少的一台服务器进行调用。类似的,也可以采用最少请求数、CPU利用率最低等其他维度进行分配
  • Consistent Hash 一致性Hash
    目前主流的负载均衡策略。是一种特殊的哈希算法,在服务端节点扩容或者下线时,尽可能保证客户端请求还是固定分配到同一台服务器节点。Consistent Hash算法是采用哈希环来实现的,通过Hash函数将对象和服务器节点放置在哈希环上,一般来说服务器可以选择IP + Port进行Hash,然后为对象选择对应的服务器节点,在哈希环中顺时针查找距离对象 Hash 值最近的服务器节点

在进行选择的时候,可以根据业务灵活选择负载均衡策略,根据CPU、连接数、内存、健康状态等都是可以的。

服务发现与注册

分布式系统中,存在多个服务实例,因此对实例的感知是非常重要的。
这里就使用注册中心来实现服务注册和发现的功能。服务端节点上线后自行向注册中心注册服务列表,节点下线时需要从注册中心将节点元数据信息移除。客户端向服务端发起调用时,自己负责从注册中心获取服务端的服务列表,然后在通过负载均衡算法选择其中一个服务节点进行调用。以上是最简单直接的服务端和客户端的发布和订阅模式,不需要再借助任何中间服务器,性能损耗也是最小的。
由于存在实例非正常下线的情况,需要对下线进行感知。可以采用主动通知+心跳检测的方案。对服务实例进行感知。
采用注册中心可以解耦客户端和服务端之间的关系,并且能够实现对服务的动态管理。服务配置可以支持动态修改,然后将更新后的配置推送到客户端和服务端,而无须重启任何服务。
目前常见的可以作为服务发现与注册模块的有:Zookeeper、Eureka、Etcd、Nacos等。

参考

23 架构设计:如何实现一个高性能分布式 RPC 框架
Dubbo 文档
回调(callback)
面试官:详细说说你对序列化的理解
JavaGuide

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值