微服务7:通信之RPC
★微服务系列
1 什么是RPC通信
RPC:Remote Procedure Call Protocol,指的是远程过程调用协议,一般使用在分布式业务或者微服务架构风格中。
即一个节点通过网络调用的方式来请求另一个节点提供的服务的过程,也可以简单的理解为client访问server上提供的函数(像调用本地函数一样,去调用一个远端服务)。
2 RPC通信详解
2.1 RCP角色和职能
在RPC框架中主要有三个角色:Provider、Consumer和Registry。如下图所示:
节点角色说明,这边看起来,跟其他的服务注册与发现框架原理差不多(如 Eureka、Consul):
Service(provider): 暴露服务的服务提供方。
Client(consumer): 调用远程服务的服务消费方。
Registry: 服务注册与发现的注册中心。
2.2 RPC调用流程
RPC(Remote Procedure Call)远程过程调用,即一个节点通过网络调用的方式来请求另一个节点提供的服务的过程,也可以简单的理解为client访问server上提供的函数。
他的基本调用流程如下:
上面是一次完整的RPC调用流程(这边指的是同步调用情况下),步骤顺序如下:
- 客户端(client)以本地调用方式(即以接口的方式)调用服务;
- 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制 byte[]);
- 客户端通过sockets将消息发送到服务端;
- 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
- 服务端存根( server stub)根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给服务端存根( server stub);
- 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
- 服务端(server)通过sockets将消息发送到客户端;
- 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息反序列化);
- 客户端(client)得到最终结果。
RPC的目标是要把2、3、4、7、8、9这些步骤都封装起来。
无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流,而数据的接收方则需要把二进制流再恢复为对象。
2.3 多
1、动态代理(Spring中重点了解下)
生成 client stub和server stub需要用到 Java 动态代理技术 ,我们可以使用JDK原生的动态代理机制,可以使用一些开源字节码工具框架 如:CgLib、Javassist等。
2、序列化
序列化:将Java对象转换成byte[]的过程,也就是编码的过程;
反序列化:将byte[]转换成Java对象的过程;
3、NIO
当前很多RPC框架都直接基于netty这一IO通信框架,比如阿里巴巴的HSF、dubbo,Hadoop Avro,推荐使用 Netty 作为底层通信框架。
4、服务注册中心
可选技术:Redis 、Zookeeper,
一般的RPC框架会接入注册中心来进行注册与发现的管理,比如Dubb与Zookeeper 的完美结合。
所以实现RPC调用过程,结构分为三部分:client、grpc、server。
内容项 | RPC调用 | 本地调用 |
函数寻址 | IP端口路由(NamingService + LoadBalancer)+函数路由 | 内存指针 |
传递数据 | 序列化后的数据流 | 内存对象 |
调用方异常处理 | timeout、retry、curcuit breaker | 抛出Exception / 函数返回固定异常标识的数据 |
被调用方异常处理 | 认证鉴权、过载保护 | 入参检查 / 执行异常捕获和处理 |
问题定位 | 分布式Trace、监控、日志中心 | 日志记录 / 断点调试跟踪 |
性能优化 | 连接池、多路复用、线程池、轻量级线程、non-block IO 等 | 编译器优化(inline等) |
这里可以看出rpc比函数调用复杂的多,比如:
- 函数寻址:你怎样调到你想要的哪个函数?在本地调用中其实就是一个函数指针,但是在RPC场景下,你要找到这个函数其实非常复杂,一个服务一般有多个下游实例,首先要选择一个下游实例,一般这个是由NamingService+LB来实现,到达对应的实例后,服务端还要解析请求体,找到函数名,然后做函数路由。
- 数据传递:在本地调用过程中其实就是传递一个指针或者值,在RPC场景下其实是通过网络传递的,网络上需要传递一个内存对象序列化之后的一个二进制网络数据流,response回来的时候也需要经过一个反序列化的过程。
- 异常处理:本地调用的情况下,无非就是判断下这个函数的返回值或者有没有抛一些异常,但是在RPC场景下就很复杂,比如网络拥塞了,服务端处理慢了或者超时,还有很多异常的情况,所以我们要做很多系统容错的事情,比如:超时、重试等策略来解决这些问题。本地调用的时候我只需要检查参数是否合法,但是在RPC的场景下我们要做一些类似认证鉴权,过载保护等策略,避免流量过大将server打挂。
- 问题定位:本地调用方法很多,比如:断点调试,打本地日志。但是在RPC场景下,这些方法其实是行不通的,我们需要分布式tracing、监控和分布式日志中心来帮助我们定位问题。
- 性能优化:本地调用其实我们不用关心太多,因为编译器会帮我们做一些列的优化,但是在RPC的场景下,就需要我们自己优化通信效率,常用的优化手段比如:连接池、多路复用、线程池等等很多方法,这些方法实现起来都非常的复杂。现在大家应该能理解RPC场景是非常复杂的
正因为有如此的复杂性,所以我们需要一个RPC框架来处理这些复杂的事情,让RPC看起来就像本地调用一样简单。
RPC 框架调用流分析
2.6.1 RPC框架功能(简单版本)
实现的过程:
- client初始化一个channel,监听NamingService,从服务名字中解析出来服务真正的上游实例地址
- 客户端将请求的的数据进行序列化
- 上游可能多个实例,需要LB去选择一个下游的IP+Port,选出来之后需要和上游实例建立连接和发送请求
- 建立连接之后发送请求
- 服务端接着接受连接和接受数据,收到数据之后将二进制数据反序列化为一个内存对象request
- 然后再调用server的响应方法进行处理
- 服务端通过sockets将消息发送到客户端;
- 客户端接收到结果消息,并进行解码(将结果消息反序列化)
2.6.2 RPC框架功能(复杂版本)
有些RPC框架不只是处理通信相关的工作(如数据的序列化和反序列化,协议的解析/打包,数据的压缩解压缩,数据的加密和解密),还可以做很多微服务治理的工作。
比如Dubbo支持对服务的治理,包括 服务注册与发现、故障注入、超时重试、负载均衡、连接管理和健康检查等。除此之外,服务端还有认证鉴权、并发流量限制、函数路由、协议适配和参数校验等等复杂的策略。
所以一个成熟的RPC框架也可以是一个非常复杂全面的分布式系统,在一定程度上协助工程进行微服务建设。
对比项 | Dubbo | gRPC | brpc | Thrift |
公司 | Ali | Baidu | ||
通讯协议 | tcp/http | http2 | 多种协议 | tcp/http |
序列化协议 | 可扩展 | protobuf | protobuf/json/mcpack | 可扩展 |
开发语言 | Java | 跨语言 | C++ / Java | 跨语言 |
主要特点 | 服务治理、扩展性 | 跨语言、性能 | 高性能、扩展性 | 跨语言 |
github star |
36.9K | 33.5K | 12.9K | 8.9K |
2.8 与RESTful API 的区别
RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。
HTTP 主要用于对外的异构环境,浏览器接口调用,App 接口调用,第三方接口调用等。
RPC 使用场景(大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC):
- 长链接。不必每次通信都要像 HTTP 一样去 3 次握手,减少了网络开销。
- 注册发布机制。RPC 框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。
- 安全性,没有暴露资源操作。
- 微服务支持。就是最近流行的服务化架构、服务化治理,RPC 框架是一个强力的支撑。
3 总结
通过本篇我们详细学习了RPC的概念和原理,以及它能够提供的能力。也对目前业内主流的RPC的框架有了一定的了解。后面一篇我们以Dobbo为例子,来学习下怎么使用RPC框架来进行服务之间的通信。