近几年随着微服务化项目的崛起,逐渐成为许多公司中大型分布式系统架构的主流方式,而今天所说的 RPC 在这其中扮演着至关重要的角色。
什么是RPC?
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。现在业界有很多开源的优秀 RPC 框架,例如 Spring Cloud、Dubbo、Thrift 等。
RPC 结构
如下是各个部分的详细职责:
1. RpcServer
负责导出(export)远程接口
2. RpcClient
负责导入(import)远程接口的代理实现
3. RpcProxy
远程接口的代理实现
4. RpcInvoker
客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回
服务方实现:负责调用服务端接口的具体实现并返回调用结果
5. RpcProtocol
负责协议编/解码
6. RpcConnector
负责维持客户方和服务方的连接通道和发送数据到服务方
7. RpcAcceptor
负责接收客户方请求并返回请求结果
8. RpcProcessor
负责在服务方控制调用过程,包括管理调用线程池、超时时间等
9. RpcChannel
数据传输通道
RPC工作原理
RPC的设计由Client,Client stub,Network ,Server stub,Server构成。 其中Client就是用来调用服务的,Cient stub是用来把调用的方法和参数序列化的(因为要在网络中传输,必须要把对象转变成字节),Network用来传输这些信息到Server stub, Server stub用来把这些信息反序列化的,Server就是服务的提供者,最终调用的就是Server提供的方法。
调用过程:
- Client像调用本地服务似的调用远程服务;
- Client stub接收到调用后,将方法、参数序列化
- 客户端通过Socket将消息发送到服务端
- Server stub 根据解码结果调用本地的服务
- 本地服务执行(对于服务端来说是本地执行)并将结果返回给Server stub
- Server stub将返回结果打包成消息(将结果消息对象序列化)
- 服务端通过Socket将消息发送到客户端
- Client stub接收到结果消息,并进行解码(将结果消息发序列化)
- 客户端得到最终结果。
RPC 调用分以下两种:
- 同步调用:客户方等待调用执行完成并返回结果。
- 异步调用:客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
Dubbo概述
Dubbo 是一个开源分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案以及 SOA 服务治理方案。Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。Dubbo 推荐的注册中心是 ZooKeeper。
调用关系说明:
- 服务容器负责启动、加载、运行服务提供者,服务提供者在启动时向注册中心注册自己提供的服务;
- 服务消费者在启动时,向注册中心订阅自己所需的服务;
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长链接推送变更数据给消费者;
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用;
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Dubbo 架构具有以下几个特点:
Dubbo特点 | 说明 |
连通性 | 1)注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小; 2)监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示; 3)服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销; 4)服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销; 5)注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外; 6)注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者; 7)注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表; 8)注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。 |
健状性 | 1)监控中心宕掉不影响使用,只是丢失部分采样数据; 2)数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务; 3)注册中心对等集群,任意一台宕掉后,将自动切换到另一台; 4)注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯; 5)服务提供者无状态,任意一台宕掉后,不影响使用; 6)服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。 |
伸缩性 | 1)注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心; 2)服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者。 |
升级性 | 当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力。 下图是未来可能的一种架构:节点角色说明: Deployer:自动部署服务的本地代理 Repository:仓库用于存储服务应用发布包 Scheduler:调度中心基于访问压力自动增减服务提供者 Admin:统一管理控制台 Registry:服务注册与发现的注册中心 Monitor:统计服务的调用次数和调用时间的监控中心 |
Dubbo 核心如下:
Dubbo核心 | 说明 |
远程通讯 | 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式 |
集群容错 | 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持 |
自动发现 | 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器 |
Dubbo 通讯协议推荐使用 Dubbo 协议(dubbo://)。Dubbo 协议采用单一长连接和 NIO 异步通讯,使用基于 mina 1.1.7 和 hessian 3.2.1 的 tbremoting 交互。适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
dubbo:// | 说明 |
连接个数 | 单连接 |
连接方式 | 长连接 |
传输协议 | TCP |
传输方式 | NIO 异步传输 |
序列化 | Hessian 二进制序列化 |
适用范围 | 传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。 |
适用场景 | 常规远程服务方法调用 |
Dubbo的使用
服务接口(API)
api(服务接口,服务模型,服务异常)需单独打包,服务提供方和消费方依赖于 api。api 对服务消费方隐藏实现。下来定义统一服务接口 (如果返回的是 pojo,pojo 必须实现 Serializable 接口):
public interface DemoService {
String sayHello(String name);
}
服务提供者(Provider)
在服务提供方实现接口:
import org.springframework.stereotype.Service;
@Service
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
}
在 Provider 上尽量多配置 Consumer 端属性,在 Provider 配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可以作为 Consumer 的缺省值。否则,Consumer 会使用 Consumer 端的全局设置,这对于 Provider 不可控的,并且往往是不合理的。在 Provider 上可以配置的 Consumer 端属性有:
属性 | 说明 |
timeout | 方法调用超时。 |
retries | 失败重试次数,缺省是 2。 |
loadbalance | 负载均衡算法,缺省是随机 random,还可以有轮询 roundrobin、最不活跃优先 leastactive。 |
actives | 消费者端最大并发调用限制,缺省是 0,不限制。即当 Consumer 对一个服务的并发调用到上限后,新调用会等待,直到超时。 |
配置声明暴露服务 resources/spring-config-dubbo.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="dubbo-demo-provider"/>
<!-- 使用zookeeper注册中心暴露服务地址,多个地址逗号隔开 -->
<dubbo:registry client="curator" protocol="zookeeper" address="127.0.0.1:2181"/>
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880"/>
<import resource="spring-config-dubbo-provider.xml"/>
</beans>
resources/spring-config-dubbo-provider.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.example.server.soa.service.DemoService" group ="bj" version="1.0.0" ref="demoServiceImpl"
timeout="200" retries="2" loadbalance="random" actives="0"/>
</beans>
然后在项目启动类中导入配置即可:
@SpringBootApplication
@ImportResource(
"classpath:spring-config-dubbo.xml"
)
服务消费者(Consumer)
配置引用远程服务 resources/spring-config-rpc-dubbo.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="dubbo-demo-consumer"/>
<!-- 使用zookeeper注册中心暴露发现服务地址,多个地址逗号隔开 -->
<dubbo:registry client="curator" protocol="zookeeper" address="127.0.0.1:2181"/>
<import resource="spring-config-rpc-dubbo-consumer.xml"/>
</beans>
resources/spring-config-rpc-dubbo-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.example.server.soa.service.DemoService" group ="bj" version="1.0.0"/>
</beans>
然后在项目启动类中导入配置即可:
@SpringBootApplication
@ImportResource(
"classpath:spring-config-rpc-dubbo.xml"
)
接下来开始调用远程服务:
@RestController
public class DemoController {
@Resource
private DemoService demoService;
@RequestMapping("/sayHello")
public String sayHello(@RequestParam String name) {
return demoService.sayHello(name);
}
}
分别运行 Provider 和 Consumer 工程,浏览器访问 Consumer 对外提供的 RESTful API,即可成功调用远程服务。这里我们也可以看出,Dubbo 对应用没有任何 API 侵入。
服务化最佳实践
- | 说明 |
粒度 | 1)服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持; 2)服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。 |
版本 | 1)每个接口都应定义版本号,为后续不兼容升级提供可能; 2)建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本; 3)当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。 |
异常 | 1)建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,以及语义更友好。 2)如果担心性能问题,在必要时,可以通过 override 掉异常类的 fillInStackTrace() 方法为空方法,使其不拷贝栈信息。 3)查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try…catch,并且不能进行有效处理。 4)服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。 |
常见问题
RPC和HTTP对比?
- | 说明 | RPC | HTTP |
传输协议 | 可以基于TCP协议,也可以基于HTTP协议 | 基于HTTP协议 | |
传输效率 | 使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率 | 如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以后是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理 | |
性能消耗 | 主要在于序列化和反序列化的耗时 | 可以基于thrift实现高效的二进制传输 | 大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能 |
负载均衡 | 基本都自带了负载均衡策略 | 需要配置Nginx,HAProxy来实现 | |
服务治理 | 下游服务新增,重启,下线时如何不影响上游调用者 | 能做到自动通知,不影响上游 | 需要事先通知,修改Nginx/HAProxy配置 |
RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。
zookeeper宕机与dubbo直连的情况?
在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。
dubbo的健壮性表现:
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
常见RPC框架?
- RMI(JDK自带): JDK自带的RPC,有很多局限性,不推荐使用。
- Dubbo: Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。目前 Dubbo 已经成为 Spring Cloud Alibaba 中的官方组件。
- gRPC :gRPC是可以在任何环境中运行的现代开源高性能RPC框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。
- Hessian: Hessian是一个轻量级的remotingonhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
- Thrift: Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。