RPC调用框架比较分析

概述

前段时间项目要做服务化,所以我比较了现在流行的几大RPC框架的优缺点以及使用场景,最终结合本身项目的实际情况选择了使用dubbox作为rpc基础服务框架。下面就简单介绍一下RPC框架技术选型的过程。

RPC简述

该系列文章将讲述以下RPC框架的helloword实例以及其实现原理简述,由于每一种RPC框架的原理实现不同且都比较复杂,如果想深入研究还请自行到官网或者其他技术博客学习。 
RPC框架职责 
RPC框架要向调用方屏蔽各种复杂性,要向服务提供方也屏蔽各类复杂性:

  • 调用方感觉就像调用本地函数一样
  • 服务提供方感觉就像实现一个本地函数一样来实现服务

RPC框架是架构(微)服务化的首要基础组件,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节 
RPC框架的职责是:让调用方感觉就像调用本地函数一样调用远端函数、让服务提供方感觉就像实现一个本地函数一样来实现服务

对于不是很理解服务化的同学可以看看这两篇文章: 
服务化架构的演进与实践 
浅谈服务化架构

服务化的好处 
互联网架构为什么要做服务化?

服务架构的拆分原则

来源:李林峰的文章:华为内部如何实施微服务架构?基本就靠这5大原则

服务拆分原则:围绕业务功能进行垂直和水平拆分。大小粒度是难点,也是团队争论的焦点。

不好的实践

  • 以代码量作为衡量标准,例如500行以内。
  • 拆分的粒度越小越好,例如以单个资源的操作粒度为划分原则。

建议的原则

  • 功能完整性、职责单一性。
  • 粒度适中,团队可接受。
  • 迭代演进,非一蹴而就。
  • API的版本兼容性优先考虑。

代码量多少不能作为衡量微服务划分是否合理的原则,因为我们知道同样一个服务,功能本身的复杂性不同,代码量也不同。还有一点需要重点强调,在项目刚开始的时候,不要期望微服务的划分一蹴而就。 
微服务架构的演进,应该是一个循序渐进的过程。在一个公司、一个项目组,它也需要一个循序渐进的演进过程。一开始划不好,没有关系。当演进到一个阶段时,微服务的部署、测试和运维等成本都非常低的时候,这对于你的团队来说就是一个好的微服务。

服务架构的开发原则

微服务的开发还会面临依赖滞后的问题。例如:A要做一个身份证号码校验,依赖服务提供者B。由于B把身份证号码校验服务的开发优先级排的比较低,无法满足A的交付时间点。A会面临要么等待,要么自己实现一个身份证号码校验功能。

以前单体架构的时候,大家需要什么,往往喜欢自己写什么,这其实是没有太严重的依赖问题。但是到了微服务时代,微服务是一个团队或者一个小组提供的,这个时候一定没有办法在某一个时刻同时把所有的服务都提供出来,“需求实现滞后”是必然存在的。

一个好的实践策略就是接口先行,语言中立,服务提供者和消费者解耦,并行开发,提升产能。无论有多少个服务,首先需要把接口识别和定义出来,然后双方基于接口进行契约驱动开发,利用Mock服务提供者和消费者,互相解耦,并行开发,实现依赖解耦。

采用契约驱动开发,如果需求不稳定或者经常变化,就会面临一个接口契约频繁变更的问题。对于服务提供者,不能因为担心接口变更而迟迟不对外提供接口,对于消费者要拥抱变更,而不是抱怨和抵触。要解决这个问题,一种比较好的实践就是管理 + 技术双管齐下:

  • 允许接口变更,但是对变更的频度要做严格管控。
  • 提供全在线的API文档服务(例如Swagger UI),将离线的API文档转成全在线、互动式的API文档服务。
  • API变更的主动通知机制,要让所有消费该API的消费者能够及时感知到API的变更。
  • 契约驱动测试,用于对兼容性做回归测试。

服务架构的测试原则

微服务开发完成之后需要对其进行测试。微服务的测试包括单元测试、接口测试、集成测试和行为测试等,其中最重要的就是契约测试:

RPC调用框架比较分析

利用微服务框架提供的Mock机制,可以分别生成模拟消费者的客户端测试桩和提供者的服务端测试桩,双方可以基于Mock测试桩对微服务的接口契约进行测试,双方都不需要等待对方功能代码开发完成,实现了并行开发和测试,提高了微服务的构建效率。基于接口的契约测试还能快速的发现不兼容的接口变更,例如修改字段类型、删除字段等。

服务架构的部署原则

测试完成之后,需要对微服务进行自动化部署。微服务的部署原则:独立部署和生命周期管理、基础设施自动化。需要有一套类似于CI/CD的流水线来做基础设施自动化,具体可以参考Netflix开源的微服务持续交付流水线Spinnaker: 
RPC调用框架比较分析

最后一起看下微服务的运行容器:微部署可以部署在Dorker容器、PaaS平台(VM)或者物理机上。使用Docker部署微服务会带来很多优先:

  • 一致的环境,线上线下环境一致。
  • 避免对特定云基础设施提供商的依赖。
  • 降低运维团队负担。
  • 高性能接近裸机性能。
  • 多租户。

相比于传统的物理机部署,微服务可以由PaaS平台实现微服务自动化部署和生命周期管理。除了部署和运维自动化,微服务云化之后还可以充分享受到更灵活的资源调度:

  • 云的弹性和敏捷。
  • 云的动态性和资源隔离。

服务架构的治理原则

服务部署上线之后,最重要的工作就是服务治理。微服务治理原则:线上治理、实时动态生效。 
微服务常用的治理策略:

  • 流量控制:动态、静态流控制。
  • 服务降级。
  • 超时控制。
  • 优先级调度。
  • 流量迁移。
  • 调用链跟踪和分析。
  • 服务路由。
  • 服务上线审批、下线通知。
  • SLA策略控制。
  • 微服务治理模型如下所示:

RPC调用框架比较分析

最上层是为服务治理的UI界面,提供在线、配置化的治理界面供运维人员使用。SDK层是提供了微服务治理的各种接口,供服务治理Portal调用。最下面的就是被治理的微服务集群,集群各节点会监听服务治理的操作去做实时刷新。例如:修改了流控阈值之后,服务治理服务会把新的流控的阈值刷到服务注册中心,服务提供者和消费者监听到阈值变更之后,获取新的阈值并刷新到内存中,实现实时生效。由于目前服务治理策略数据量不是特别大,所以可以将服务治理的数据放到服务注册中心(例如etcd/ZooKeeper),没有必要再单独做一套。

服务最佳实践

介绍完微服务实施之后,下面我们一起学习下微服务的最佳实践。 
服务路由:本地短路策略。关键技术点:优先调用本JVM内部服务提供者,其次是相同主机或者VM的,最后是跨网络调用。通过本地短路,可以避免远程调用的网络开销,降低服务调用时延、提升成功率。原理如下所示:

RPC调用框架比较分析
服务调用方式:同步调用、异步调用、并行调用。一次服务调用,通常就意味着会挂一个服务调用线程。采用异步调用,可以避免线程阻塞,提升系统的吞吐量和可靠性。但是在实际项目中异步调用也有一些缺点,导致使用不是特别广泛: 
需要写异步回调逻辑,与传统的接口调用使用方式不一致,开发难度大一些。 
一些场景下需要缓存上下文信息,引入可靠性问题。 
并行调用适用于多个服务调用没有上下文依赖,逻辑上可以并行处理,类似JDK的Fork/Join, 并行服务调用涉及到同步转异步、异步转同步、结果汇聚等,技术实现难度较大,目前很多服务框架并不支持。采用并行服务调用,可以把传统串行的服务调用优化成并行处理,能够极大的缩短服务调用时延。

微服务故障隔离:线程级、进程级、容器级、VM级、物理机级等。关键技术点:

  • 支持服务部署到不同线程/线程池中。
  • 核心服务和非核心服务隔离部署。
  • 为了防止线程膨胀,支持共享和独占两种线程池策略

RPC调用框架比较分析

谈到分布式,就绕不开事务一致性问题:大部分业务可以通过最终一致性来解决,极少部分需要采用强一致性。 
RPC调用框架比较分析
具体的策略如下:

  • 最终一致性,可以基于消息中间件实现。
  • 强一致性,使用TCC框架。服务框架本身不会直接提供“分布式事务”,往往根据实际需要迁入分布式事务框架来支持分布式事务。

微服务的性能三要素:

  • I/O模型,这个通常会选用非堵塞的,Java里面可能用java原生的。
  • 线程调度模型。
  • 序列化方式。

公司内部服务化,对性能要求较高的场景,建议使用异步非阻塞I/O(Netty) + 二进制序列化(Thrift压缩二进制等) + Reactor线程调度模型。 
RPC调用框架比较分析
最后我们一起看下微服务的接口兼容性原则:技术保障、管理协同。

  • 制定并严格执行《微服务前向兼容性规范》,避免发生不兼容修改或者私自修改不通知周边的情况。
  • 接口兼容性技术保障:例如Thrift的IDL,支持新增、修改和删除字段、字段定义位置无关性,码流支持乱序等。
  • 持续交付流水线的每日构建和契约化驱动测试,能够快速识别和发现不兼容。

现在流行的RPC框架:

服务治理型

  • dubbo
  • dubbox
  • motan

多语言型

  • grpc
  • thrift
  • avro
  • Protocol Buffers (google) 
    RPC调用框架比较分析

上图来自于dubbo。服务治理型RPC框架结构大多如此,大致分为服务提供者,服务消费者,注册中心,监控报警中心几大模块。

服务性能

在服务化,或者微服务化过程中,首先考虑的问题就是性能问题,因为在服务化之后,会增加以下额外的性能开销:

  1. 客户端需要对消息进行序列化,主要占用CPU计算资源。
  2. 序列化时需要创建二进制数组,耗费JVM堆内存或者堆外内存。
  3. 客户端需要将序列化之后的二进制数组发送给服务端,占用网络带宽资源。
  4. 服务端读取到码流之后,需要将请求数据报反序列化成请求对象,占用CPU计算资源。
  5. 服务端通过反射的方式调用服务提供者实现类,反射本身对性能影响就比较大。
  6. 服务端将响应结果序列化,占用CPU计算资源。
  7. 服务端将应答码流发送给客户端,占用网络带宽资源。
  8. 客户端读取应答码流,反序列化成响应消息,占用CPU资源。

RPC框架高性能设计

要想提高效率,除了硬件的提升,主要考虑以下三个方面:

  1. I/O调度模型:同步阻塞I/O(BIO)还是非阻塞I/O(NIO)。
  2. 序列化框架的选择:文本协议、二进制协议或压缩二进制协议。
  3. 线程调度模型:串行调度还是并行调度,锁竞争还是无锁化算法。

IO调度现在主流的就是netty。 
高性能序列化目前性能最好的是ice,google 的 pb协议,FB的thrift协议等 
线程没啥好说的,肯定多线程了。当然也可以是AKKA(java)

总结

综上所述,服务化是现在大型互联网公司主流的架构模式,现在还有更流行的微服务,docker部署等等。

个人建议采用dubbox,集成其他各种协议,在该系列文章最后有各个协议的性能对比。

之所以建议采用dubbox是因为,dubbox有比价完善的服务治理模型,其包含ZK注册中心,服务监控等,可以很方便的为我们服务。 
虽然dubbo本身不支持多语言,但是我们可以集成其他的序列化协议,比如thrift、avro,使其可以支持多种入门语言,让部门间的协作沟通更加灵活方便

当然,在实际使用过程中,尤其是集成其他协议的过程中,肯定需要对协议本身有比较深入的了解,才能正确的使用。

motan

新浪微博开源的RPC框架

helloword示例直接去官网下载运行即可

github地址:https://github.com/weibocom/motan 
文档地址:https://github.com/weibocom/motan/wiki/zh_quickstart 
用户指南;https://github.com/weibocom/motan/wiki/zh_userguide

# grpc

中文版官方文档:gRPC 官方文档中文版

helloWord示例,我就是根据这个文章做的,写得挺详细的:rpc框架之gRPC 学习 - hello world

grpc原理: grpc原理分析

dubbo

dubbo 已经与12年年底停止维护升级,忽略

thrift

请参考我写的另一篇文章:thrift学习笔记(一) thrift简介及第一个helloword程序

dubbox

dubbox 是当当团队基于dubbo升级的一个版本。是一个分布式的服务架构,可直接用于生产环境作为SOA服务框架。 
dubbo官网首页:http://dubbo.io/ 上面有详细的用户指南和官方文档,介绍的比较详细,这里不再赘述。

当当官方的github地址:https://github.com/dangdangdotcom/dubbox

升级为spring4.X(及其他依赖组件)版本dubbox的github的地 
址:https://github.com/yjmyzz/dubbox

参考资料【博客:菩提树下的杨过 的文章写得非常全面,介绍的已经非常详细了】: 
dubbox升级spring到4.x及添加log4j2支持 
分布式服务框架 dubbo/dubbox 入门示例 
dubbox 的各种管理和监管 
dubbo/dubbox 增加原生thrift及avro支持

# 各个RPC框架性能比较

测试环境

jdk7 
win7 64位 
idea 
个人笔记本配置:

person对象:

  1. private int age;
  2. private String name;
  3. private boolean sex;
  4. private int childrenCount;

测试数据,入参:

  1. private int ageStart;
  2. private int ageEnd;

返回值:

  1. Person.PersonBuilder builder = Person.builder();
  2. List<Person> list = new ArrayList<Person>();
  3. for (short i = 0; i < 10; i++) {
  4. list.add(builder.age(i).childrenCount(i).name("test" + i).sex(true).build());
  5. }
  6. return list;

各协议测试使用的配置

  • grpc


rpc getPersonList (yjmyzz.grpc.study.dto.QueryParameter) returns (yjmyzz.grpc.study.dto.PersonList) {}

  • motan

<motan:basicService export="demoMotan:8002" 
group="motan-demo-rpc" accessLog="false" shareChannel="true" module="motan-demo-rpc" 
application="myMotanDemo" registry="registry" id="serviceBasicConfig"/>

  • dubbox
  1. <dubbo:protocol name="dubbo" serialization="kryo" optimizer="com.alibaba.dubbo.demo.SerializationOptimizerImpl"/>
  2.  
  3. <dubbo:service interface="com.alibaba.dubbo.demo.person.PersonService" ref="personService" protocol="dubbo"/>
  • thrift

TNonblockingServer + TFramedTransport

测试结果

  1.  
  2. rgpc 100000 次NettyServer调用,耗时:53102毫秒,平均1883次/秒 【简单grpc】
  3. rgpc 100000 次NettyServer调用,耗时:52138毫秒,平均1917次/秒 【简单grpc】
  4. rgpc 100000 次NettyServer调用,耗时:51800毫秒,平均1930次/秒 【简单grpc】
  5. rgpc 100000 次NettyServer调用,耗时:51313毫秒,平均1948次/秒 【简单grpc】
  6.  
  7. rgpc 100000 次NettyServer调用,耗时:56812毫秒,平均1760次/秒[2016-10-08 19:17:31] Dubbo service server started! 【dubbox.kryo】
  8. rgpc 100000 次NettyServer调用,耗时:55133毫秒,平均1813次/秒[2016-10-08 19:18:42] Dubbo service server started!【dubbox.kryo】
  9. rgpc 100000 次NettyServer调用,耗时:52280毫秒,平均1912次/秒[2016-10-08 19:20:01] Dubbo service server started! 【dubbox.kryo】
  10.  
  11. rgpc 100000 次NettyServer调用,耗时:44414毫秒,平均2251次/秒[2016-10-08 19:13:34] Dubbo service server started! 【dubbox.fst】
  12. rgpc 100000 次NettyServer调用,耗时:44805毫秒,平均2231次/秒[2016-10-08 19:12:25] Dubbo service server started! 【dubbox.fst】
  13. rgpc 100000 次NettyServer调用,耗时:46245毫秒,平均2162次/秒[2016-10-08 19:14:43] Dubbo service server started! 【dubbox.fst】
  14.  
  15. rgpc 100000 次NettyServer调用,耗时:12203毫秒,平均8194次/秒[2016-10-09 19:52:34] Dubbo service server started!【dubbox.thrift】
  16. rgpc 100000 次NettyServer调用,耗时:14142毫秒,平均7071次/秒[2016-10-09 19:30:17] Dubbo service server started!【dubbox.thrift】
  17. rgpc 100000 次NettyServer调用,耗时:13762毫秒,平均7266次/秒[2016-10-09 19:30:43] Dubbo service server started!【dubbox.thrift】
  18.  
  19.  
  20. rgpc 100000 次NettyServer调用,耗时:44334毫秒,平均2255次/秒 【motan】
  21. rgpc 100000 次NettyServer调用,耗时:37844毫秒,平均2642次/秒 【motan】
  22. rgpc 100000 次NettyServer调用,耗时:39007毫秒,平均2563次/秒 【motan】
  23. rgpc 100000 次NettyServer调用,耗时:38610毫秒,平均2590次/秒 【motan】

测试结果说明

  1. 使用的自己的笔记本电脑测试的,测试的方式可能不太专业,但能够说明问题。
  2. 通过上面结果可以看到,thrift的性能最好,而且是相当的好

网上其他人做的测试

ice-dubbo-thrift-grpc性能测试对比 
RPC框架的性能比较

总结

影响RPC性能的因素主要有:

  • 序列化性能
  • IO性能
  • 线程模式

序列化的话,肯定是Google的PB协议和thrift最好,IO和线程的话,先流行的性能比较好的都是采用多线程非阻塞IO。

grpc是Google出品,使用了PB协议,但是由于它出现的比较晚,还不怎么成熟,而且采用http协议,非常适合现在的微服务,不过性能上差了许多,而且像服务治理与监控都需要额外的开发工作,所以放弃grpc。 
thrift和grpc一样,性能优越,但是开发难度相比较于dubbox和motan也是高了一点点,需要编写proto文件(其实对于程序员来说这算不上难度)。像服务治理与监控也是需要额外的开发工作。 
dubbo比较老了,直接弃用。 
dubbox和后来的motan都比较适合我们的团队。dubbox后来经过当当的开发,引入了rest风格的http协议,并且还支持kryo/fst/dubbo/thrift等其他协议,而且其他团队也在使用dubbo,集成方便,服务治理监控功能齐全,所以最终采用dubbox。

其实我个人而言还是喜欢thrift,毕竟性嫩优越,在大型分布式系统中,哪怕一点点性能提升累计在一起也是很可观的。不过再具体选型的过程中还要结合团队目前的状况和团队其他开发人员的接受程度进行取舍。

转自:https://blog.csdn.net/liubenlong007/article/details/54692241

上一篇:【原创】自己动手实现RPC服务调用框架


下一篇:Extending JavaScript Natives