前置知识

RPC和REST

绕不开的话题

RPC是一种远程服务之间调用的思想。

REST往大了说是对互联网资源的一种CRUD抽象,是一种宏观上的风格。但是落地真的很难。又有多少人/项目遵守了REST的原则,或者说写的是正确的restful风格的API,而不是restful-like,http+json草草了事。

代码的时候条条框框或许很鲜明,但是靠人去践行它的时候就会相当的模糊。这里面又涉及到了一大堆哲学理念,水太深,插眼空了细嗦

Restful支持各种语言,但是性能和带宽不如RPC,业务上对于微服务的实现,基本是确定一个组织边界,在该边界内,使用RPC; 边界外,使用Restful。这个边界,可以是业务、部门,甚至是全公司。

SpringCloud和RPC

SpringCloud是支持RPC的,比如gRPC。只不过默认是http协议。

RPC主要是基于TCP/IP协议(传输层协议),REST主要是基于HTTP协议(应用层协议)。

为什么学习RPC

别老想着怎么用好RPC框架,你得多花时间琢磨原理。

只要涉及网络通信,我们就有可能使用RPC

RPC 是解决分布式系统通信问题的一大利器。分布式系统中的网络通信一般都会采用四层的 TCP 协议或七层的 HTTP 协议,TCP 协议的稳定性和高效性高于HTTP。

网络通信说起来简单,但实际上是一个非常复杂的过程,这个过程主要包括:对端节点的查找、网络连接的建立、传输数据的编码解码以及网络连接的管理等等,每一项都很复杂。

rpc的特点:

1、分布式设计

2、部署灵活

3、解耦服务

4、扩展性强

常见RPC框架

1、Dubbo:阿里巴巴,java

2、gRPC:Google,多语言

3、Thrift:Facebook/apache,多语言

4、Spring Cloud:不仅仅是RPC,更多的是微服务架构下的一站式解决方案

RPC的作用及优势

1、RPC框架一般使用长链接,不必每次通信都要3次握手,减少网络开销

2、RPC框架一般都有注册中心,有丰富的监控管理

3、发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作

4、协议私密,安全性较高

5、rpc 能做到协议更简单内容更小,效率更高

6、rpc是面向服务的更高级的抽象,支持服务注册发现,负载均衡,超时重试,熔断降级等高级特性

RPC可以干嘛

比如用户部署在节点A,订单工程部署在节点B.2个节点间要调用。可以通过socket、rmi、http,rpc实现服务与服务之间的调用。而我们可以实现一个RPC框架,让2个服务之间可以通信。

怎么学习RPC

使用RPC像调用本地一样发起远程调用,解决服务间通信问题。了解序列化,编解码,网络传输等

然后了解连接管理、健康监测、负载均衡、优雅启停机、异常重试、业务分组、熔断限流等

然后学会提升RPC的性能以及在分布式下如何定位问题等。

RPC通信原理

分布式节点通信

跨节点通信方式-非消息通信

很多种方式都可以做到不同节点之间通信

  • 原生socket编程
1
Socket 网络编程,使用 TCP/IP 协议栈收发数据,这样不仅可以在本地的进程间通信,也可以在主机、机房之间异地通信。 大方向上这是没错的,但原生的 Socket API 非常底层,要考虑很多细节,如果再加上异步就更复杂了
  • RMI远程方法调用
1
能够让在客户端Java虚拟机上的对象像调用本地对象一样去调用服务端Java虚拟机中的对象上的方法,是面向对象的, 只用于Java,可以把RMI看作是用java语言实现了RPC,EJB时代盛行。
  • 标准公有协议
1
2
利用标准的公有协议进行跨节点服务调用 , 例如: http+xml,RESTful+json,webs ervice等。 
支持EE规范的web容器部署服务 , 提供 web 接口。 RestTemplate,httpclient....., SpringCloud通信组件等。
  • RPC远程过程调用
1
2
3
是一个计算机通信协议/ 规范/标准。
允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员就像调用本地程序一样(就项以前单体一样该怎么调就怎么调),
且是一种Client/Server模式。可以实现进程间的通信,许多技术框架都是基于这种概念而实现的

RPC概述

RPC 的主要功能目标是让构建分布式计算(应用)更容易,是一种通过网络从远程计算机程序上请求服务, 而不需要了解底层网络技术的协议规范,简单的来说就是像调用本地服务一样调用远程服务,对开发者而言是透明的。

RPC 的全称是 Remote Procedure Call,即远程过程调用。简单解读字面上的意思,远程肯定是指要跨机器而非本机,所以需要用到网络编程才能实现。

RPC 的作用就是体现在这样两个方面:

  • 屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法;
  • 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑;

RPC 虽然可以帮助开发者屏蔽远程调用跟本地调用的区别,但毕竟涉及到远程网络通信,所以这里还是有很多使用上的区别,比如:

  • 调用过程中超时了怎么处理业务?
  • 什么场景下最适合使用 RPC?
  • 什么时候才需要考虑开启压缩?

无论你是一个初级开发者还是高级开发者,RPC 都应该是你日常开发过程中绕不开的一个话题,所以作为软件开发者的我们,真的很有必要详细地了解 RPC 实现细节。只有这样,才能帮助我们更好地在日常工作中使用 RPC。

RPC架构

RPC架构设计

分布式工程的直接体现就是拆,拆成不同的服务后,不像原来的单体架构,不在同一个JVM内,服务间怎么通信呢?就要进行网络调用。

image-20220720124627213

RPC框架很多种,为了更好的理解它的设计理念。我们想下,假如市面上没有这种框架,需要我们自己开发,设计一款PRC框架,我们需要考虑什么?

image-20220720124901806

以前在一个工程里面,userController通过@autowire主题userservice调用。

现在要设计一款RPC框架,在基于网络的远程调用像本地调用一样,让上层应用编程的时候感知不到底层的远程调用。

1
2
3
4
5
6
7
8
9
10
11
假设我们设计的注解是@Remote,怎么做到加上这个注解就可以调用远程的接口服务呢?

1.想要调用到接口,首先得知道ip端口,找到对端。
2.想要定义到对端的接口,你得知道是哪个接口,哪个方法,哪些参数。不然没有确定唯一接口。
3.假设我们把这些对端接口的信息封装为data对象,但是对象无法在网络中传输,所以们要转为二进制 序列化。
4.data序列化后在网络中基于TCP/IP传输到对端,TCP协议可能是多个包粘在一起发送的。我怎么知道你发过来的消息怎么解析。所以要做一步协议编码。
5.终于,我们可以将编码后的消息通过网络模块传到对端。
6.对端收到消息,首先要把这个编码的消息,解码。得到二进制的数据,就要再反序列化。这样就可以知道,我想调用的是哪个接口,哪个方法,参数是什么。
7.然后 balabala 调用接口做业务操作,要返回呀,返回就反过来,把返回值序列化,协议编码,传到调用方。
9.最后调用方接收到返回值,做协议解码,反序列化,得到返回值。给调用模块。
10.这样,我们就完成了一个远程调用。这些将通过代理技术把繁琐的细节封装到底层,让调用就像在本地一样无感知。

进一步解说:

1
2
3
4
1.对端的ip端口不能写死,服务自身的状态变化,调用方要能够动态的感知(推动)。 => 注册中心
2.引入注册中心,对端的ip端口可以动态管理了,那对端就要支持服务注册(注册),调用方就要支持服务发现(发现)。(调用方服务发现还可以做本地缓存)
3.注册中心对端保持状态的更新,可以用心跳机制监控。
4.调用方从注册中心拿到ip和端口,向对端发起调用时也可以根据心跳机制做健康检查。

设计一个RPC,只是考虑这些还不够。网络调用很复杂,可能应用网络抖动等各种原因导致失败。

1
2
3
4
5
1.调用放调用失败了怎么办?RPC框架要支持容错机制。(调用失败重试 ?返回空?返回异常?)
2.对端可能是考虑高可用,吞吐,会做集群部署。就对应了多个版本或者多个节点。那我选择哪个呢?RPC要支持路由,负载均衡。
3.调用时,对端的业务处理太久了怎么办?RPC要支持超时机制。
4.调用失败次数多了,重试会增加流量(用户不停点/代码中重试),就有可能导致调用方也不行(同步调用占用线程资源),从而导致雪崩。所以PRC框架要支持熔断,降级。
5.对端也要考虑限流机制。

除了注册,发现,注册中心三角结构,还有其他考虑的点

1
2
1.针对注册中心,为了做到动态管理对端的状态(接口上下线,接口权重等),可以增加一个后台管理。
2.针对服务提供,消费者,为了监控调用的状态。可以增加一个监控者monitor。

RPC调用时序

image-20220115153932549

RPC流程图

image-20220115154224821

RPC框架实现要点

实现一个RPC框架的要考虑的点,也就是PRC的学习的点

image-20220115154252338

注册中心

注册只是记录节点和接口等信息,调用还是2个节点间通过网络调用。

服务注册发现的作用

在高可用的生产环境中,服务一般都以集群方式提供服务,集群里面的IP等重要参数信息可能随时会发生变化,节点也可能会动态扩缩容,客户端需要能够及时感知服务端的变化,获取集群最新服务节点的连接信息,而这些变化要求是要对调用方应用无感知的

image-20220115154736227

主流服务注册工具

  • Zookeeper
  • Consul
  • Nacos
  • …..

比如zookeeper作为注册中心,Dubbo作为RPC框架

image-20220115154742194

image-20220720123715097

调用方一般会保存对端的缓存,如果注册中心如果挂了,本地可能不能及时感知,调用还是会失败的。所以一般要保证注册中心的高可用。

代理技术

为什么要用代理:

因为调用放,只有接口,需要一个实现类,所以需要代理实现类。

如果让开发自己搞构造请求,序列化,协议,网络通信的话整个API不友好。怎么屏蔽RPC细节?

Spring的AOP核心是动态代理,通过字节码增强对方法进行拦截增强,添加额外的逻辑处理。也可以应用代理到RPC解决问题。

RPC的调用对用户来讲是透明的,内部核心技术采用的就是代理技术,RPC 会自动给接口生成一个代理实现,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理实现。在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样就可以在生成的代理类里面,加入其他调用处理逻辑

image-20220115154843219

代理技术:

  • JDK动态代理

在运行期动态的创建代理类,它是通过接口生成代理类的,与静态代理相比更加灵活,但是也有一定的限制 ,第一是代理对象必须实现一个接口,否则会报异常。第二是有性能问题,因为是通过反射来实现调用的,所以比正常的直接调用来得慢,并且通过生成类文件也会多消耗部分方法区空间,可能引起Full GC

  • ASM

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为(也就是生成的代码可以覆盖原来的类也可以是原始类的子类)。不过ASM在创建class字节码的过程中,操纵的是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。

但是它的性能高(越底层的性能一般都越高,为了便利使用,往上封装好用的工具,终归是牺牲了一点性能。)

  • CGLIB

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库。其原理是动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法,在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快

image-20220115154945761

  • bytebuddy

Byte Buddy本身也是基于 ASM API 实现的,是一个较高层级的抽象的字节码操作工具,通过使用 Byte Buddy,任何熟悉 Java 编程语言的人都有望非常容易地进行字节码操作。

  • Javassist

Javassist 使操作Java字节码变得简单,一个可以用于编辑Java字节码的类库,提供了两种级别的API:源码级别和字节码级别。如果用户使用源码级API,他们可以在不需要过多了解Java字节码规范的前提下使用它提供的基于java语言的API来编辑字节码文件。如果使用字节码级API则允许用户直接编辑字节码文件。Javassist在复杂的字节码级操作上提供了更高级别的抽象层。另外Javassist使用了反射机制,这使得它在运行时比ASM慢。

image-20220115155042645

序列化技术

在网络传输中,数据必须采用二进制形式, 但调用方请求的出入参数都是对象。对象是肯定没法直接在网络中传输的,需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。

所以在RPC调用过程中, 需要采用序列化技术,对入参和出参进行序列化与反序列化

序列化技术很多,选择序列化技术考虑的点:

image-20220115155104335

安全、稳定也是要考虑的

image-20220720123755061

常见序列化技术框架:

  • java :JDK原生序列化
1
2
1、JAVA语言本身提供,使用比较方便和简单 
2、不支持跨语言处理,性能相对不是很好, 序列化以后产生的数据相对较大
  • JSON :轻量级数据交换格式
1
2
3
1、可读性好,方便阅读和调试,多语言支持, 
2、JSON 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销
3、效率相对不高,但对比XML序列化后的字节流更小,在企业运用普遍,特别是对前端和三方提供api。
  • Hessian2 [ˈheʃn]: 二进制
1
2
1、Hessian 是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。 
2、Hessian 性能上要比 JDK、JSON 序列化高效很多,并且生成的字节数也更小。 有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议
  • Protobuf : 开源,高效
1
2
3
1、Google 推出的开源序列库,它是一种轻便、 高效的结构化数据存储格式,多语言支持。 
2、速度快,压缩比高,体积小,序列化后体积相比 JSON、Hessian 小很多
3、消息格式的扩展、升级和兼容性都不错,可以做到向后兼容

调用肯定要有网络调用,避免不掉的就是协议问题。IO问题。

RPC通信协议

回忆下粘包拆包的问题,我们需要一个协议,约定好数据格式,表名数据包的类型和长度。这样既可以正确的接收到信息了。

image-20220115155314175

RPC框架主要是在应用层(4)的协议,一般不用共有的HTTP协议,而是自定义的 私有协议

image-20220624162753359

系统IO

上面的协议定义好了数据在网络传输的时候用什么样方式组装,对端接收到后用什么样的方式读取。

而IO决定了对端机器从网卡中读数据的时候用什么IO(同步异步阻塞非阻塞多路复用等)

IO选型

RPC的调用过程中涉及到网络IO的操作,一般来说网络IO往往会成为系统的瓶颈所在,而不管上层应用如何使用,底层都是基于操作系统的IO模型。

image-20220115155346367

线程模型

调用方Controller -> 动态代理 -> RPC 框架 -> 发起网络请求

一般RPC框架发起网络请求这里用的时候异步线程,然后调用可以继续坐别的事情。

但是最终,用户线程要阻塞等待结果返回。

从用户角度看是阻塞的(整体),从底层PRC调用细节处看是异步的。

image-20220115155408053

异步如何实现

常用的方式就是Future 方式,它是返回 Future 对象,通过GET方式获取结果; 或者采用入参为 Callback 对象的回调方式,处理结果。

image-20220115155429421

超时重试机制

前面说过请求发出去RPC框架要去检测是否超时了。怎么检测超时?

1
2
每个发起请求,开个异步线程,在线程中定时器,比如10s检测一次。  => 不行,每个请求开个线程,太吃资源
开一个线程,把每次请求的定时检测封装成task任务,放到集合列表中。然后每隔ns去轮询检测要执行的task。 => 每个任务的过期时间是不一样的(请求加入的时间不一样)。所以会有很多无意义的轮询。

问题:有很多无意义的遍历操作开销,浪费CPU,怎么办?

image-20220624162911005

时间轮算法

在时钟轮机制中,有时间槽和时钟轮的概念,时间槽就相当于时钟的刻度;而时钟轮就相当于指针跳动的一个周期,我们可以将每个任务放到对应的时间槽位上。

image-20220624162939935

这种数据结构用链表是可以,但是链表的查询慢,所以用循环数组来做。

循环数组模拟时间轮盘

image-20220115155623892

同一个时间节点的下可能会存多个任务,但是这样终究是提升了很多,不用每次全部遍历了。而且,就算这个不够用,还有更精细的秒级轮等。

优势:

1、每个任务会按要求只扫描执行一次,能很好的解决CPU浪费的问题

2、秒级轮,分钟轮,小时轮

除了用于检测rpc调用是否超时,也可以将定时心跳的任务添加到时间轮中,当前时间的心跳执行完后再将下一秒的心跳任务添加到时间轮中,这样就能做到每秒的定时心跳

负载均衡策略

用途 :RPC Server为了高可用,可用选择做集群,因此在RPC Client端调用时要使用相应的均衡策略,这属于客户端负载均衡。

image-20220115155736726

熔断

作用:熔断器如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试恢复调用操作

image-20220115155805020

限流

作用:实际生产环境中,每个服务节点都可能由于访问量过大而引起一系列问题,就需要业务提供方能够进行自我保护,从而保证在高访问量、高并发的场景下,系统依然能够稳定,高效运行。限流器的作用是用来限制其请求的速率,保护后台响应服务,以免服务过载导致服务不可用现象出现。

令牌桶

image-20220115155835929

请求的速率和令牌发放的速率一致

漏桶

image-20220115155841749

漏桶更容易应对突发流量峰值。令牌桶假设100w都有令牌,那就100w全过来了。而漏桶,100w过来了,可能只有50w进去。

滑动窗口算法

TCP做拥塞控制,流量控制。

比如1min不超过200,可能解决方案是记1min的请求个数,1min结束后再记录请求个数。

存在弊端,第一次1min的后30s有100个请求,第二次的1min前30s有100个请求。这么1min中内就有了200个请求了。

所以要用滑动窗口算法解决该问题。

image-20220115155908711

限流组件

image-20220115155925679

RPC框架在使用时要注意哪些问题?

对象构造得过于复杂:属性很多,并且存在多层的嵌套

对象过于庞大:我经常遇到业务过来咨询,为啥他们的 RPC 请求经常超时,排查后发现他们的入参对象非常得大

用序列化框架不支持的类作为入参类:比如 Hessian 框架,他天然是不支持 LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList

对象有复杂的继承关系:大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性

在使用 RPC 框架的过程中,我们构造入参、返回值对象,主要记住以下几点:

  1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
  2. 入参对象与返回值对象体积不要太大,更不要传太大的集合;
  3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
  4. 对象不要有复杂的继承关系,最好不要有父子类的情况。

前面知道了IO模型,怎么通过Netty在网络中收发消息.这篇整一下怎么手写一个RPC框架,促进理解.为后面学习Dubbo源码做铺垫。