一、REST 与 RPC
1、什么是 REST 和 RPC 协议?
在单体应用中,各模块间的调用是通过编程语言级别的方法函数来实现,但分布式系统运行在多台机器上,一般来说,每个服务实例都是一个进程,服务间必须使用进程间通信机制来交互,而常见的通信协议主要有 RPC 和 REST 协议。
(1)REST:
REST 是基于 HTTP 实现,使用 HTTP 协议处理数据通信,更加标准化与通用,因为无论哪种语言都支持 HTTP 协议。常见的 http API 都可以称为 Rest 接口。REST 是一种架构风格,指一组架构约束条件和原则,满足 REST 原则的应用程序或设计就是 RESTful,RESTful 把一切内容都视为资源。REST 强调组件交互的扩展性、接口的通用性、组件的独立部署、以及减少交互延迟的中间件,它强化安全,也能封装遗留系统。
(2)RPC:
RPC 是一种进程间通信方式,允许像调用本地服务一样调用远程服务,通信协议大多采用二进制方式。
2、RPC 与 REST 的对比
image.png(1)传输协议与性能:RPC 的传输协议灵活,可基于 TCP 实现,由于 TCP 协<typo id="typo-542" data-origin="的" ignoretag="true">议</typo>处于协议栈的下层,能够更灵活地对协议字段进行定制,让请求报文体积更小,减少网络开销,提高传输性能并缩短传输耗时,实现更大的吞吐量和并发数。REST 的 HTTP 协议是上层协议,发送包含同等内容的信息,请求中会包含很多无用的内容,所占用的字节数比使用 TCP 协议传输更高,因此在同等网络下,HTTP 会比基于 TCP 协议的数据传输效率要低,传输耗时更长,不仅如此,REST 的 HTTP 大部分是通过 JSON 来实现的,序列化也更消耗性能,但如果是基于 HTTP2.0,那么经过封装也是可以作为一个 RPC 来使用的。
(2)灵活性、开放性与通用性:REST 通过 HTTP 实现,相对更加规范与通用,无论哪种语言都支持 HTTP 协议,所以 REST 的调用和测试都很方便,但使用 RPC 则会有很多约束,而如果 RPC 需要对外<typo id="typo-913" data-origin="使用" ignoretag="true">开放</typo>的话,需要进一步处理,灵活性不如 REST
(3)使用场景:REST 主要用于对外开放的异构环境,比如浏览器接口调用,Api 接口调用,第三方接口调用等。RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,特别是大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC
二、RPC 框架
REST 和 RPC 都常用于微服务架构中,微服务的好处之一,就是不限定服务的提供方使用什么技术选型,能够实现大公司跨团队的技术解耦。 但是,如果没有统一的通信框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等 “业务之外” 的重复技术劳动,造成整体的低效。所以,统一通信框架把上<typo id="typo-1246" data-origin="的" ignoretag="true">述</typo> “业务之外” 的技术劳动统一处理,是服务化首要解决的问题。
1、什么是 RPC 框架?
RPC 框架的目标就是让远程服务调用更简单、透明,由 RPC 框架负责屏蔽底层的序列化、传输方式和通信的细节,开发者在使用时只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。RPC 框架作为架构微服务化的基础组件,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率。
2、RPC 框架的技术架构
如下图,在典型 RPC 的使用场景中,主要包含了服务发现、负载、容错、网络传输、序列化等组件,其中 ”RPC协议”就指明了服务如何进行序列化和网络传输,这也是RPC的核心功能。
- 应用级的RPC框架:Dubbo、Google gRPC
- 通信框架:Netty
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)
3、RPC 框架的调用流程
image.png3.1、RPC 框架的核心组件:
(1)客户端(Client):服务调用方。
(2)客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
(3)服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
(4)服务端(Server):服务的真正提供者。
3.2、RPC 的调用流程:
(1)服务消费者(Client 客户端)通过本地调用的方式调用需要消费的服务
(2)客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体
(3)客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端
(4)服务端存根(Server Stub)收到消息后进行解码,反序列化操作
(5)服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
(6)服务端(Server)执行具体的业务逻辑,并将处理结果返回给服务端存根(Server Stub)
(7)服务端存根(Server Stub)将返回结果序列化,并通过网络发送给消费方
(8)客户端存根(Client Stub)接收到消息,并进行解码与反序列化
(9)服务消费方得到最终结果;
image.png而RPC框架的实现目标则是将上面的<typo id="typo-2298" data-origin="得到" ignoretag="true">第2</typo>-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务
4、如何实现一个RPC框架?
通过上面几点的介绍,如果要我们实现一个 RPC 框架,我们应该如果做呢?要实现 一个RPC 框架,我们只需要解决的以下几件最基本的事情:
4.1、如何进行网络通讯?
远程调用中,客户端和服务端的通讯是基于网络连接的,所以首先需要建立通信连接,通过这个连接把请求信息的字节流传给服务端,然后再把序列化后的响应结果传回客户端,在这个通讯过程中,它所使用的协议是没有限制的,能完成传输就行,但是在这里我们需要考虑两个问题:如何选择网络协议 和 如何建立连接。
(1)网络协议的选择:多数 RPC 框架选择 TCP 作为传输协议,但其实 UDP 也可以,也有部分选择HTTP,比如 gRPC 使用 HTTP2,但是不同的协议各有优劣势,TCP 更加高效,而 HTTP 在实际应用中更加的灵活,具体需要根据使用场景来选择,下文会介绍如何选择正确的网络传输协议
(2)通讯连接的建立:RPC 所有交换的数据都在这个连接里传输,这个连接可以是按需连接(需要调用时就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
4.2、如何那行服务寻址?
解决寻址的问题,也就是说服务端如何确定客户端要调用的函数,在本地调用中,函数是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以在远程调用中,客户端和服务端需要分别维护一个【ID -> 函数】的映射表,ID在所有进程中都是唯一确定的,客户端在做远程过程调用时,附上这个ID,服务端通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
而寻址问题的具体实现方式,则可以通过注册中心,服务提供者完成后,对外暴露相应的功能并将自己注册到注册中心上,接着服务消费者从注册中心寻找服务,然后调用该服务对应的方法完成远程调用
(1)从服务提供者的角度看:
当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;
当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。
(2)从调用者的角度看:
服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息;
当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者;
服务调用者下线的时候,则取消订阅。
4.3、如何序列化和反序列化?
在本地调用中,我们只需要把参数信息压到内存栈中,然后让函数自己去栈中读取,但是远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。所以远程过程调用中,客户端和服务端交互时,方法的参数和结果需要通过底层的网络协议如TCP传递,由于网络协议是基于二进制的(只有二进制数据才能在网络中传输),那么这些值需要序列化成二进制的形式,通过寻址和传输将序列化的二进制发送目标服务器。目标服务器接收到数据时,需要对数据进行反序列化。序列化和反序列化的速度也会影响远程调用的效率。
- 将对象转换成二进制流的过程叫做序列化
- 将二进制流转换成对象的过程叫做反序列化
5、如何选择正确的PRC网络传输协议?
在 RPC 中可选的网络传输方式有多种,比如 TCP 协议、UDP 协议、HTTP 协议。每一种协议对整体的性能和效率都有不同的影响,那如何选择一个正确的网络传输协议呢?针对这个问题,我们首先要搞明白各种传输协议在 RPC 中的工作方式:
- 基于 TCP 的协议实现的 RPC 调用,由于 TCP 协议处于协议栈的下层,能够更加灵活地对协议字段进行定制,让请求报文体积更小,减少网络开销,提高传输性能并缩短传输耗时,实现更大的吞吐量和并发数。但是需要更多关注底层复杂的细节,实现的代价更高,同时对不同平台,如安卓,iOS 等,需要重新开发出不同的工具包来进行请求发送和相应解析,工作量大,难以快速响应和满足用户需求。
- 基于 HTTP 协议实现的 RPC 则可以使用 JSON 和 XML 格式的请求或响应数据,而 JSON 和 XML 作为通用的格式标准(使用 HTTP 协议也需要序列化和反序列化,不过这不是该协议下关心的内容,成熟的 Web 程序已经做好了序列化内容),开源的解析工具已经相当成熟,在其上进行二次开发会非常便捷和简单。但是由于 HTTP 协议是上层协议,发送包含同等内容的信息,请求中会包含很多无用的内容,所占用的字节数比使用 TCP 协议传输更高,因此在同等网络下,HTTP 会比基于 TCP 协议的数据传输效率要低,传输耗时更长,当然压缩数据,能够缩小这一差距。
三、RPC框架dubbo
1、dubbo 是什么?
前面讲到,RPC 常用于微服务架构中,而 RPC 框架作为架构微服务化的基础组件,能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率。而 Dubbo 是阿里巴巴开源的基于 Java 的 RPC 分布式服务框架,提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。另外,基于 Spring Cloud Alibaba 技术栈的 Spring-cloud-alibaba-dubbo 更是对 dubbo 技术进行了封装,在基于 Spring Cloud Alibaba 提供的 Nacos 注册中心下,提供了 Dubbo 和 Spring Cloud 的整合方案,即 Dubbo Spring Cloud,使得服务内部的 RPC 协议调用几乎是零成本的改造,实现了基于 RPC 的服务调用。
2、dubbo 的执行流程
2.1、dubbo 总体流程:
image.png- 紫色虚线:启动时完成的功能
- 蓝色虚线:运行过程中执行的功能,异步调用
- 蓝色实线:运行过程中执行的功能,同步调用
2.1.1、dubbo 的总体执行流程说明如下:
(1)启动容器,加载,运行服务提供者。
(2)服务提供者在启动时,向注册中心注册自己提供的服务。
(3)服务消费者在启动时,向注册中心订阅自己所需的服务。
(4)注册中心返回服务提供者地址列表给消费者,消费者接收到之后,缓存在本地中,如果内容有变更,注册中心将基于长连接推送变更数据给消费者。
(5)服务消费者,从提供者地址列表中,基于软负载均衡算法,选择一台提供者进行调用,如果调用失败,再选另一台调用。
(6)服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
在consumer中使用了代理模式,创建了一个Provider类的一个代理对象。通过代理对象获取Provider中的真实功能,起到保护Provider真实功能的作用。
2.1.2、dubbo 的整个执行流程可以理解为生产者-消费者模型+注册中心+监控中心,这样设计的原因在于:
- Consumer 与 Provider 解偶,双方都可以横向增减节点数
- 注册中心对本身可做对等集群,可动态增减节点,并且任意一台宕掉后,将自动切换到另一台
- 去中心化,双方不直接依懒注册中心,即使注册中心全部宕机短时间内也不会影响服务的调用
- 服务提供者无状态,任意一台宕掉后,不影响使用
2.2、dubbo 同步调用原理
2.2.1、dubbo 同步调用流程:
(1)客户端线程调用远程接口,向服务端发送请求,同时当前线程应该处于“暂停“状态,即线程不能向后执行了,必需要拿到服务端给自己的结果后才能向后执行
(2)服务端接到客户端请求后,处理请求,将结果给客户端
(3)客户端收到结果,然后当前线程继续往后执行
dubbo 中使用了 Socket 来建立长连接、数据传输,而底层结合了的 Apache mina 框架,Apache mina 框架基于Reactor模型通信框架,基于tcp长连接。dubbo 使用 IoSession.write() 方法进行远程调用与发送消息,这个方法的远程调用过程异步的,即对于当前线程来说,将请求发送出来,线程就可以往后执行了,至于服务端的结果,是服务端处理完成后,再以消息的形式发送给客户端的。于是这里出现了2个问题:
(1)当前线程怎么让它“暂停”,等结果回来后,再向后执行:
先生成一个对象 obj,在一个全局 map 里 put(ID,obj) 存放起来,再用 synchronized 获取 obj 锁,再调用 obj.wait() 让当前线程处于等待状态,然后另一消息监听线程等到服务端处理结果到来,再 map.get(ID) 找到 obj,再用 synchronized 获取obj锁,再调用 obj.notifyAll() 唤醒前面处于等待状态的线程。
(2)Socket通信是一个全双工的方式,当有多个线程同时进行远程方法调用,这时 client 与 server 间的 socket 连接上会有很多双方发送的消息传递,前后顺序也可能是乱七八糟的,server处理完结果后,将结果消息发送给client,client收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的:
使用一个ID,让其唯一,然后传递给服务端,再服务端又回传回来,这样就知道结果是原先哪个线程的了
2.2.2、dubbo同步调用原理:
(1)客户端使用一个线程调用远程接口,生成一个唯一 ID,Dubbo 是使用 AtomicLong 从 0 开始累计数字的
(2)将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象callback,全部封装在一起,组成一个对象object
(3)向专门存放调用信息的全局 ConcurrentHashMap 里面 put(ID, object)
(4)将 ID 和打包的方法调用信息封装成一对象 connRequest,使用 IoSession.write(connRequest) 异步发送出去
(5)当前线程再使用 callback 的 get() 方法试图获取远程返回的结果,在get()内部,则先使用synchronized获取回调对象callback的锁, 检测是否已经获取到结果,如果没有,然后调用 callback 的 wait() 方法,释放 callback 上的锁,让当前线程处于等待状态。
(6)服务端接收到请求并处理后,将结果(包含了唯一ID)回传给客户端,客户端 socket 连接上专门监听消息的线程收到消息后,分析结果,取到ID,再从前面的 ConcurrentHashMap 里面 get(ID),从而找到 callback,将方法调用结果设置到callback对象里。
(7)最后监听线程再获取回调对象 callback 的 synchronized 锁(因为前面调用过wait() 导致释放callback的锁),先使用 notifyAll() 唤醒前面处于等待状态的线程继续执行,这样 callback 的 get( )方法继续执行就能拿到调用结果了,至此,整个过程结束
需要注意的是,这里的callback对象是每次调用产生一个新的,不能共享;另外ID必需至少保证在一个Socket连接里面是唯一的。
3、dubbo 的负载均衡策略
(1)随机调用策略(默认):随机选择服务器节点,该策略可以对不同服务器实例设置不同的权重,权重越大分配流量越高
(2)轮询调用策略:均匀地将请求分配到各个机器上。如果各个机器的性能不一样,容易导致性能差的机器负载过高,所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。
(3)最少活跃数策略:根据服务器的运行状态去选择服务,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器分配更少的请求
(4)一致性哈希算法:相同参数的请求一定会被分发到固定的服务器节点。当某个服务器节点挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。
4、dubbo 的容错机制
Failover(默认):失败自动切换,当出现失败,重试其它服务器,默认为2次。通常用于读操作,但重试会带来更长延迟。
Failfast:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。
Broadcast:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。
5、dubbo支持哪些协议和适用场景?
(1)dubbo:单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者的情况。传输协议 TCP,异步 Hessian 序列化。Dubbo 官方推荐使用 dubbo 协议。但是,dubbo 协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低
(2)RMI: 采用 JDK 标准的 RMI 协议实现,使用 Java 标准序列化机制,传输参数和返回参数对象需要实现 Serializable 接口,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。多个短连接,基于 TCP 协议传输,同步传输,适用常规的远程服务调用和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。
(3)WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。
(4)HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。
(5)Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。
(6)Redis:基于 Redis 实现的RPC协议。
(7)Memcache:基于 Memcache实现的 RPC 协议。
6、dubbo 的通信框架:
dubbo 默认使用 Netty 作为通讯框架
7、dubbo的架构设计:
image.png7.1、图例说明
- 左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
- 图中从下至上分为 10 层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
7.2、各层说明:
(1)接口服务层(Service):该层与实际业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现
(2)配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心
(3)服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory
(4)服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService
(5)路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce
(6)监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService
(7)远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter
(8)信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer
(9)网络 传输 层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec
(10)数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool
网友评论