0%

RPC框架整体理解

RPC框架整体理解

In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is written as if it were a normal (local) procedure call, without the programmer explicitly writing the details for the remote interaction.

— wikipedia

RPC(remote procedure call)远程过程调用,通过封装底层网络通信细节,向跨网络的服务提供如同本地调用一般的远程调用接口

  • RPC来源于微服务架构的发展,越来越多业务系统被拆解为多个相互协作的微服务,借助RPC工具能够方便实现不同服务之间相互调用。
  • 常见的RPC框架有:Dubbo、Thrift、GRPC、Spring Cloud(部分)

根据RPC在微服务架构中所处的位置和作用,容易知道如果要实现一个RPC框架所需要解决的问题包括:

  1. 如何使得远程调用用起来像“本地调用”?动态代理
  2. 如何找到被调用的服务在哪里? —— 服务注册与发现
  3. 微服务高级功能如何支持?—— 负载均衡/流量控制/调用链路监控
  4. 内存数据如何转化为网络传输的二进制数据? —— 序列化方法
  5. 调用双方如何通信?—— 通信协议设计
  6. 底层通信逻辑如何设计? —— 网络IO设计

综上所属,一个典型的RPC框架的架构,如下图所示:

  • 接入层:通过动态代理将本地调用转化为远程调用,基于过滤器链进行调用
  • 服务治理层:负责包括注册发现,负载均衡,路由/鉴权等RPC服务治理功能,或者说辅助rpc功能
  • 协议层:主要负责实现从内存对象到网络传输数据的相互转换,并按照约定的通信协议进行封装/拆封
  • 传输层:基于特定IO模型和底层通信协议实现调用数据的传输。

一个RPC框架的主要业务逻辑包括:

  1. 服务注册和上线
  2. RPC调用执行

常用RPC框架

常用的RPC框架包括Dubbo、Thrift、GRPC、Spring Cloud等,下面简单总结本人对于不同RPC框架的学习

Thrift

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently

本部分的大部分内容来自:Thrift: The Missing Guide

Thrift 作为Apache旗下的顶级项目,具备支持多种语言,多种消息格式,同步异步通信等特点,其基本架构为:

  • Server层:以Client-Server模式定义RPC通信逻辑,支持单/多线程,阻塞非阻塞通信模式
  • Processor层:封装从输入流读取/写入输出流的操作,是协议和流之间的转化层。
  • Protocol层:定义传输协议(内存对象到传输数据的转化)+ 序列化,其中序列化方式包括Binary 协议/Compact 协议/json
  • Transport层:对网络读写提供了一个简单的抽象接口,不负责消息的序列化/反序列化等操作(TCP/HTTP)。
1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Thrift的使用也比较简单,参考官方文档即可。

gRPC

gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统

  • 基于Protocol Buffer协议实现序列化功能
  • 底层通信基于HTTP2.0实现

其他基本和thrift大差不差,不再赘述,使用也比较简单,参考官方文档即可

Dubbo

Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,相较于上述两个RPC框架,Dubbo最大的区别在于其涵盖了更多的服务治理功能:

  • 服务开发框架 :定义了一套微服务定义,服务间调用模型,服务发现,负载均衡策略,流量路由和管控的范式,其中一部分功能由Dubbo自身实现(服务定义,调用模型,负载均衡),另一部分功能可与其他组件配合实现(地址发现,链路追踪,认证鉴权等)。
  • RPC 通信协议实现 :Dubbo 从设计上不绑定任何一款特定通信协议,HTTP/2、REST、gRPC、JsonRPC、Thrift、Hessian2 等几乎所有主流的通信协议。

与上述两个rpc框架的区别在于,Dubbo整体上是一个微服务治理框架,rpc只是微服务治理中的一个重要问题。

实现一个RPC框架?

根据上文中对于RPC架构的总结,我们可以类比计网中OSI七层协议的学习思路,自底向上分析每一层会遇到的问题以及如何实现。

网络通信模型

RPC网络通信实际上就是Cilent与Server之间的网络IO,《UNIX 网络编程卷 I》中根据同/异步,阻塞/非阻塞定义了五种IO通信模型:

  1. 阻塞IO模型:发起IO端(客户端/应用程序)在发出IO请求后,阻塞等待当前请求返回后,再向下执行
  2. 非阻塞IO:发起IO端(客户端/应用程序)在发出IO请求后,继续执行,在合适的时机询问IO是否执行完毕。
  3. 异步IO:发起IO端(客户端/应用程序)在发出IO请求后,等待IO结束,通知自己数据准备完毕。
  4. IO复用:select/poll/epoll。
  5. 信号驱动IO:数据在内核中准备完毕后,以事件的形式通知用户程序

Java 在上述网络模型的基础上定义了自身的IO模型:

  1. BIO(Blocking IO),同步阻塞IO,每当服务器接收到一个客户端连接请求,创建新线程处理
  2. NIO(Non Blocking IO),同步非阻塞IO,底层基于LInux内核函数的多路复用函数(select,poll,epoll)
  3. AIO(Asynchronous IO),异步非阻塞IO

另外Netty基于 Reactor模型 实现了更高性能的网络通信,是目前常用的网络IO框架

Thrift实现

Thrift基于java原声socket模型实现了底层的io通信机制,服务端包含四种通信模式的server:

  • TSimpleServer:单线程服务器端,阻塞IO
  • TThreadPoolServer:多线程服务器端,阻塞IO
    • 持有一个线程池,scoket接收到连接后交给线程池处理
  • TNonblockingServer:单线程服务器端,使用非阻塞式I/O
    • 底层基于java NIO实现,单线程Reactor模型,处理连接/读取/写入等均由单一线程执行
  • THsHaServer:半同步半异步服务器端,基于非阻塞式IO读写和多线程工作任务处理
    • 在TNonblockingServer的基础上增加了线程池处理具体的任务逻辑,主线程处理连接建立/读写请求,即多线程Reactor模型
  • TThreadedSelectorServer:多线程选择器服务器端,对THsHaServer在异步IO模型上进行增强
    • 将主线程的任务进一步拆分,主Reactor线程负责建立连接等,从Reactor线程负责读写,其余交由线程池处理,即主从Reactor模型

客户端支持两种通信模式的server:

  • TSocket:阻塞IO
  • TNonblockingSocket:非阻塞IO

另外 TFramedTransport 包装类支持数据以帧进行传输,具体传输IO模式依赖内部包装的类,如Tsocket。

gRPC实现

gRPC基于基于Netty4.1 的 HTTP/2 协议栈框架构建,在以往传统的RPC调用方式上,额外支持了基于HTTP/2.0的stream调用方式。其中服务端Server 基于Reactor模型实现:

  • 主线程监听指定的 port,来等待Client连接请求, 分给 worker 线程池处理.
  • HTTP/2请求消息的请求和响应发送都由Netty负责(NioEventLoop)
  • gRPC 负责消息的序列化和反序列化、以及应用服务接口的调用

客户端线程模型分为:

  1. 同步阻塞服务调用:普通的请求响应模型
  2. 同步非阻塞服务调用:基于Future机制实现
  3. 异步非阻塞调用:基于回调函数
  4. 基于HTTP/2.0的stream调用(协议层详细了解)

Dubbo中的实现

内容来自,我只是简化总结强化记忆:Dubbo服务端线程模型

Dubbo 底层 IO 框架基于Netty实现,其中服务端根据 通信的不同阶段是否在IO线程执行 分为了五种通信线程模型:

  1. All:在 IO 线程(Netty handler主线程)上执行sent/序列化response 操作,在Dubbo线程池中执行其他操作和反序列化
  2. Direct:所有操作均在IO线程执行
  3. Execution:IO 线程执行 sent/connected/disconnected/caught和序列化 response 操作,Dubbo 线程池中执行r eceived 和反序列化request
  4. Message Only
  5. Connection Ordered

上述线程模型在配置文件中配置,Dubbo通过SPI机制实现动态设置。

客户端线程模型为:

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
  3. 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
  4. 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
  5. 业务线程拿到结果直接返回

总结

结合上文对于不同框架中网络IO模型的分析,我们很容易得到如下结论:

  1. 服务端IO基于Reactor模型实现,结合线程池实现高性能网络IO,常用Netty实现底层IO
  2. 客户端往往支持阻塞/非阻塞/异步三种通信模式

不同RPC框架的底层IO原理没有很大的区别,区别在于多线程和线程池的使用。

通信协议

在进行通信前通信双方必须事先约定好通信的方式,才能理解从网络上传送来的二进制数据,这就是传输协议的作用,RPC框架位于应用层,需要基于网络层的TCP/UDP协议实现通信,因此在通信时会遇到一下通信单位与应用层传输单位的不对应问题。

例如:一次RPC调用对应 TCP流的一段数据 或 UDP的一到多个数据包,如何确定数据的终点(TCP拆包问题),不同调用之间的分隔(TCP粘包问题),所以通信协议实际上用来解决以下问题:

  • 确定一次RPC通信传输数据长度和边界。
  • 确定通信传输数据的存储和解析方式。

参考IP数据报的设计方式,我们不难想到通信协议包括。

  • 协议头:整体长度,数据长度,以及其他如版本,消息ID等的控制信息。
  • 协议体:携带的数据本身。

TCP拆包粘包

TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小

  • 粘包:如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送
  • 拆包:如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包

常见的解决方法包括:

  • 以固定长度形式传输。每次通信传输固定长度的数据,不足则补0
  • 发送端在每个包的末尾使用固定的分隔符。FTP命令的一般报文格式是:命令 选项参数 \r\nFTP应答的一般报文格式为:状态码 报文选项 \r\n
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。

现有RPC框架中的通信协议

Thrift

内容来自 Thrift Binary protocol encoding等文档

thrift支持多种通信协议,包括:

  • TBinaryProtocol:二进制协议
  • TCompactProtocl:带压缩的二进制协议

整体协议分为head和body两部分,具体格式如下:

其中head包括:

  1. magic(4byte):包含版本号,消息类型等,严格模式首位取1,非严格模式首位取0。消息类型包括:
    • CALL = 1 调用消息,如0x80010001
    • REPLY = 2 应答消息,如0x80010002
    • EXCEPTION = 3 异常消息,如0x80010003
    • ONEWAY = 4 单向消息,属于调用消息,但是不需要应答,如0x80010004
  2. mehod name length(4byte):调用方法名长度
  3. mehod name (Nbyte):调用方法名
  4. seqid(4byte):序列号

Body对应方法参数/返回值,对应一个struct类型,struct针对常用数据类型分别定义序列化方式,如string类型占4+N字节,格式如下:

1
2
3
4
--------------------
| size | content |
| 4 | N |
--------------------

grpc

gropc基于http2.0协议实现,http2.0通过引入二进制分帧在兼容http1.x的基础上实现了性能提升,其新特性包括:

  1. 二进制分帧。在应用层和网络层之间添加了二进制分帧操作,对于http1.x报文划分为头部帧和数据帧分别发送
  2. 首部压缩+首部缓存。通信双方缓存首部帧,传输时只需要发送变化的头部和数据帧即可。另外基于HPACK算法(传输索引+存储参数表)对首部进行压缩。
  3. 多路复用:引入“流”概念将一个逻辑上的http2请求拆分为多个流复用,提供了通过单一的http/2 连接发起多重的请求的能力。
  4. 请求优先级。分帧后可根据帧携带数据内容调整传输顺序提升通信效率,每个流都可以带有一个31比特的优先值,根据流优先级进行帧传输
  5. 服务端推送。服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。

其余内容参考gRPC系列(三) 如何借助HTTP2实现传输我写也是cv不写了,其中不同数据存储在不同帧:

  • 请求的Method在header中传递
  • 参数用DATA帧
  • 返回状态用HEADER帧
  • 返回数据用DATA帧

通信过程:client发送header帧携带method信息,server返回header帧后,通过data帧交换数据

Dubbo

Dubbo除去提供Triple,Dubbo2两种通信协议外,同时支持任意第三方通信协议,如官方支持的 gRPC、Thrift、REST、JsonRPC、Hessian2 等。

  • Triple协议同时支持基于http1(unary),http2(stream)的请求协议,其中http2在实现上与标准gRPC协议基本一致。
  • Dubbo2协议类似于常规的通信协议,如下图所示,基本属性包括版本号(magic number),请求响应表示(res/requst),序列化标识(serilization ID),响应状态(Status id),请求ID(request id),Variable Part(请求中携带方法名,服务名,参数列表等,响应携带返回值和异常)

/dev-guide/images/dubbo_protocol_header.jpg

总结

从上面对现有协议的总结,我们能够大体知道目前rpc协议的主要方式包括:

  1. 基于TCP自定义通信协议,一般结构为:协议头+数据
  2. 基于http/http2协议实现通信,方法名/服务名等数据存放在请求头,参数/返回等数据存放在请求体

自定义数据格式通常由一下字段组成:

  1. 魔术位:表明这是什么协议,协议的版本等,为了兼容不同通信协议
  2. 长度信息:无论是整体/数据长度,解决粘包拆包问题
  3. 消息ID,消息类型:定位此次消息,以及消息是什么类型(响应/请求)
  4. 序列化方式:标识携带数据序列化方式,兼容不同的序列化方式。

内存对象->网络传输

在进行网络传输前,由于调用参数/返回参数均为内存中的对象,需要转化为可在网络上传输数据形式后才能按照协议封装传输,序列化方法解决了内存到传输数据形式的转化,目前主要的序列化方法包括:

  1. JDK原生
  2. JSON:将对象的属性名-属性值以kv形式存储为json文件形式
    • 以纯文本json文件形式传输,文本形式空间开销较大,不适合大量rpc调用情况.
    • JSON 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决。
  3. Thrift:上文中简单提及了Thrift序列化方式,与Hessian属于一流派
  4. Hessian
  5. Kryo
  6. Protobuf

JDK原生

JDK原生提供了java对象的序列化,通过ObejctOutputStreamObejctInputStream提供的方法接口可以序列化/反序列化实现了Serializable的接口,主要的注意事项有:

  1. 一旦变量被transient修饰,变量将不再是对象持久化的一部分。
  2. 只会记录第一次序列化的编号,不会重复序列化,这会导致最新变量变更不会体现在序列化文件中。
  3. 实现 Externalizable 接口可自定义序列化和反序列化方法。

其序列化格式为类似于上文中的协议格式。

JDK原生序列化协议存在空间利用效率较低,无法跨平台使用等问题

Hessian

来自Dubbo Hessian介绍

Hessian is a dynamically-typed, binary serialization and Web Services protocol designed for object-oriented transmission.

  1. 自描述序列化类型。不依赖外部描述文件或者接口定义,将所有类字段信息都放入序列化字节数组中,直接利用字节数组进行反序列化
  2. 把复杂对象的所有属性存储在一个Map中进行序列化。所以在父类、子类存在同名成员变量的情况下,Hessian序列化时,先序列化子类,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖
  3. 兼容字段增、减,序列化和反序列化;不支持一部分java类型序列化:Linked 系列,LinkedHashMap、LinkedHashSet 等;Locale 类,可以通过扩展 ContextSerializerFactory 类修复Byte/Short 反序列化的时候变成 Integer。

Kryo

Kryo 是一个快速高效的 Java 二进制对象图序列化框架,该项目的目标是高速、小尺寸和易于使用的 API。

  • 使用变长的int和long保证这种基本数据类型序列化后尽量小
  • Kryo对Class的序列化只需要化Class的全路径名,在反序列化时根据Class通过类加载进行加载
  • 不是线程安全的,要通过ThreadLocal或者创建Kryo线程池来保证线程安全
  • 不需要实现Serializable接口
  • 字段增、减,序列化和反序列化时无法兼容
  • 必须拥有无参构造函数

Protobuf

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。目前提供了 C++、Java、Python 三种语言的 API。

  • 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
  • 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
  • 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序

使用Protobuf协议需要先定义IDL(Interface description language),根据IDL内容生成对应的序列化反序列化工具

总结

该部分没有深入的去学具体的序列化协议内部原理,只是简单了解了主要的几种序列化方法(偷了一部分网上的八股内容)。

优点 缺点
Kryo 速度快,序列化后体积小 跨语言支持较复杂
Hessian 默认支持跨语言 较慢
Protostuff 速度快,基于protobuf 需静态编译
Protostuff-Runtime 无需静态编译,但序列化前需预先传入schema 不支持无默认构造函数的类,反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值
Java 使用方便,可序列化所有类 速度慢,占空间

如何找到服务?

为了解决服务之间的可见性,rpc框架往往需要基于第三方组件提供服务的注册发现功能,目前了解过的服务注册中心有:

  1. ZooKeeper:服务注册为Zookeeper中的ZNode,并给予Watcher机制实现服务发现。
  2. Nacos:阿里提供的开源服务注册发现组件,提供了基于Http和gRPC机制服务注册发现接口
    • Nacos将服务分为持久服务和临时服务,临时服务基于gRPC连接发送心跳信息保活,持久服务由注册中心发送心跳保活
    • 调用方订阅服务时会在服务本地和注册中心分别维护一个订阅列表,订阅信息的维护分为推逻辑和拉逻辑
      • 服务端推送逻辑:当订阅服务发生变更(上下线/元信息变更)是,注册中心会主动向订阅客户端推送变更服务信息(只告诉客户端变了,让他自己拉取)
      • 客户端拉取逻辑:客户端会周期性向服务端请求最新服务信息,若服务发生变更,则拉取变更服务信息
    • nacos同时提供保证强一致性Raft协议实现AP,和弱一致性的Distro实现CP。对于持久化实例,采用raft保证AP。对于配置中心和服务注册发现中的临时实例场景,采用DIstro协议实现CP

保证可靠的调用?

负载均衡

常见的负载均衡策略(Niginx)包括:

  1. 轮询(round robin):按照顺序逐个调用服务节点
  2. 加权平滑轮询:指定权重轮询,每次调用后按照一定规则修改权重值,避免所有请求打在权重高的服务节点上
    • 每个节点初始化一个当前值和权重值
    • 每次选择当前值+权重值最大的节点,选择该节点后,修改该节点当前值=当前值-总权重
    • 重复这个过程,能够保证不会一直选择权重最大的节点
  3. ip_hash:根据ip hash值分配到对应服务器,解决session不共享问题

Dubbo中提供的负载均衡策略包括:

  • 加权随机(Weighted Random)
  • 加权轮询(round robin):借鉴自niginx
  • 最少活跃优先(LeastActive):活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数=请求发送数 - 响应返回数
  • 最短响应优先:在最近一个滑动窗口中,响应时间越短(时间窗口内的平均数),越优先调用。相同响应时间的进行加权随机。
  • 一致性哈希:相同参数的请求总是发到同一提供者(特殊需求)

从上述负载均衡算法我们容易看到,负载均衡的目标实际上就是将调用按照处理能力均匀的分配到对应服务节点上,分为两种派别:

  1. 无权/固定加权 轮询/随机:随机/轮询不一定很好,但是一定不会太差
  2. 根据参数动态确定权重:根据服务节点相关指标计算权重
    • 调用方:请求-响应次数,响应时间
    • 被调用方发送给调用方:cpu负载,内存占用,CPU核数、内存大小、请求处理的耗时指标(如请求平均耗时、TP99、TP999)、服务节点的状态指标(如正常、亚健康)等指标,例如Dubbo对应负载策略

调用异常处理

RPC调用由于需要经过网络传输,相较于普通本地调用会出现由网络导致的异常等问题,这些也是RPC框架需要解决的问题,下面研究一下Dubbo中的异常定义和处理方法,以对RPC调用的异常处理有更加清晰的认知。

Dubbo异常处理

Dubbo将RPC过程的异常定义为RPCException,其枚举状态包括:网络相关异常/业务异常/权限异常/路由异常等

1
2
3
4
5
6
7
8
public static final int UNKNOWN_EXCEPTION = 0;
public static final int NETWORK_EXCEPTION = 1;
public static final int TIMEOUT_EXCEPTION = 2;
public static final int BIZ_EXCEPTION = 3;
public static final int FORBIDDEN_EXCEPTION = 4;
public static final int SERIALIZATION_EXCEPTION = 5;
public static final int NO_INVOKER_AVAILABLE_AFTER_FILTER = 6;
。。。。省略

Dubbo在provider端定义了ExceptionFilter处理服务端方法抛出的异常(代码),其处理逻辑为:

  • 对于客户端可识别的异常直接抛出。什么是可识别异常,即Dubbo认为客户端知道的异常,包括RuntimeException,方法名上声明的异常,JDK本身异常,异常和api定义在一个jar包的异常,dubbo异常(RPCException)
  • 对于客户端不可识别的异常,包装为RuntimeException包装返回给客户端。

客户端根据不同类型的异常进行不同处理:

  1. 服务端异常:在根据返回结果中的异常类型进行异常重放。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Override
    public Object recreate() throws Throwable {
    if (exception != null) {
    // fix issue#619
    try {
    Object stackTrace = exception.getStackTrace();
    if (stackTrace == null) {
    exception.setStackTrace(new StackTraceElement[0]);
    }
    } catch (Exception e) {
    // ignore
    }
    if ((exception instanceof RpcException) && !(exception instanceof com.alibaba.dubbo.rpc.RpcException)) {
    com.alibaba.dubbo.rpc.RpcException recreated =
    new com.alibaba.dubbo.rpc.RpcException(((RpcException) exception).getCode(),
    exception.getMessage(), exception.getCause());
    recreated.setStackTrace(exception.getStackTrace());
    throw recreated;
    }
    throw exception;
    }
    return result;
    }
  2. 客户端调用异常:在invoke方法中处理(这里展示的默认invoker)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try {
    //**远程调用代码
    } catch (TimeoutException e) {
    throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + RpcUtils.getMethodName(invocation) + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    } catch (RemotingException e) {
    String remoteExpMsg = "Failed to invoke remote method: " + RpcUtils.getMethodName(invocation) + ", provider: " + getUrl() + ", cause: " + e.getMessage();
    if (e.getCause() instanceof IOException && e.getCause().getCause() instanceof SerializationException) {
    throw new RpcException(RpcException.SERIALIZATION_EXCEPTION, remoteExpMsg, e);
    } else {
    throw new RpcException(RpcException.NETWORK_EXCEPTION, remoteExpMsg, e);
    }
    }

Dubbo 服务在尝试调用一次之后,如出现非业务异常(服务突然不可用、超时等),Dubbo 默认会进行额外的最多2次重试,其实现位于FailbackClusterInvoker.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
Invoker<T> invoker = null;
URL consumerUrl = RpcContext.getServiceContext().getConsumerUrl();
try {
invoker = select(loadbalance, invocation, invokers, null);
// Asynchronous call method must be used here, because failback will retry in the background.
// Then the serviceContext will be cleared after the call is completed.
return invokeWithContextAsync(invoker, invocation, consumerUrl);
} catch (Throwable e) {
logger.error(CLUSTER_FAILED_INVOKE_SERVICE,"Failback to invoke method and start to retries",
"","Failback to invoke method " + RpcUtils.getMethodName(invocation) +
", wait for retry in background. Ignored exception: "
+ e.getMessage() + ", ",e);
if (retries > 0) {
addFailed(loadbalance, invocation, invokers, invoker, consumerUrl);
}
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // ignore
}
}

降级/熔断/限流

降级是指当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心业务正常运作或高效运作。说白了,就是尽可能的把系统资源让给优先级高的服务。

熔断是指在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。

服务降级和服务熔断万字讲解,从0到1,边学边实战

当服务面临段时间大量请求调用系统资源耗尽无法有效响应请求时,服务需要采取一定的机制保证自身的高可用性,降级和熔断是从两个不同角度保护服务的机制:

  1. 降级(服务方):当我自身的服务能力跟不上请求来的速度,我是不是应该“降低我服务标准/数量”
  2. 熔断(请求方):如果我调用的服务响应很慢/总是报错等,我是不是应该停一会再调用
  3. 限流:限制单位时间调用服务的请求数量,可以理解为降级的一种方法

博客介绍的比较全面,不再赘述,简单总结一下主要的限流算法

  1. 令牌桶
    • 算法描述:为系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
    • 算法特点:流量均匀,桶中令牌能够一定程度上应对突发流量
  2. 漏桶
    • 算法描述:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝
    • 算法特点:流量均匀,但是无法应对突发流量
  3. 固定/滑动窗口

具体实现有:

  1. 代码实现
  2. 使用Guava RateLimiter限流入门到深入

参考

Thrift: The Missing Guide

gRPC官方文档

gRPC基本原理

Reactor 模型|为啥 Redis 单线程模型也能效率这么高?

Apache thrift 之网络模型

Thrift协议介绍

面试题:聊聊TCP的粘包、拆包以及解决方案

深入理解http2.0协议,看这篇就够了!

gRPC系列(三) 如何借助HTTP2实现传输