前言
近几年随着微服务化项目的崛起,逐渐成为许多中大型分布式系统架构的主流方式,而RPC在这其中扮演着重要的角色。市面上流行的RPC框架如Dubbo、Thrift、Hession、gRPC等。
本人所在的公司一直使用Dubbo,在使用Dubbo进行开发时,想要快速知道某些dubbo接口的响应结果,但不想启动项目(因为这些项目不一定是你负责的,不会部署而且极其笨重),也不想新建一个dubbo客户端项目(费事且占地方),也不想开telnet客户端连接口(麻烦且有限制)。
因Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议,Dubbo接口大部分都是使用Dubbo协议,而Dubbo协议底层的默认通讯是用netty。所以用Netty写一个Dubbo接口测试的收发客户端,转发请求到各种系统的接口,然后把响应展示到统一页面。这样就可以实现一处输入,到处调用,避免了调试一个接口就要写一个Controller,或者写一个Dubbo客户端或者泛化改造这么麻烦了。
在介绍该测试工具之前,先回顾下RPC、Dubbo等的相关知识。
RPC介绍
什么是RPC
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。现在业界有很多开源的优秀 RPC 框架,例如 Spring Cloud、Dubbo、Thrift 等。
RPC结构
RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出,Nelson 的论文中指出实现 RPC 的程序包括 5 个部分:
UserUser-stubRPCRuntimeServer-stubServer
这里 user 就是 client 端,当 user 想发起一个远程调用时,它实际是通过本地调用 user-stub。
user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。
远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用,调用结果再返回给 user 端。
以上是粗粒度的 RPC 实现概念结构,接下来进一步细化它应该由哪些组件构成,如下图所示。
RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel,并使用RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给RpcProcessor 去控制处理调用过程,最后再委托调用给RpcInvoker 去实际执行并返回调用结果。如下是各个部分的详细职责:
RPC工作原理
RPC的设计由Client,Client stub,Network ,Server stub,Server构成。
RPC 调用分以下两种:
同步调用:客户方等待调用执行完成并返回结果。
异步调用:客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
Dubbo介绍
架构
节点角色
| 节点 | 角色说明 |
|---|---|
| Provider | 暴露服务的服务提供方 |
| Consumer | 调用远程服务的服务消费方 |
| Registry | 服务注册与发现的注册中心 |
| Monitor | 统计服务的调用次数和调用时间的监控中心 |
| Container | 服务运行容器 |
调用关系
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
生态系统
集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
节点关系
这里的 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息;Directory·表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更;Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个;Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等;LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。
容错模式
Failover Cluster
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。
重试次数配置如下:
1 | <dubbo:service retries="2" /> |
或
1 | <dubbo:reference retries="2" /> |
或
1 | <dubbo:reference> |
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
负载均衡
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。
Random LoadBalance
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
RoundRobin LoadBalance
轮询,按公约后的权重设置轮询比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />。
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />。
线程模型
如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。
但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。
如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。
因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:
1 | <dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" /> |
Dispatcher
ThreadPool
dubbo://
从上面的Dubbo生态系统中可以看到Dubbo支持多种协议,缺省协议是dubbo协议,即采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
特性
缺省协议,使用基于 mina 1.1.7 和 hessian 3.2.1 的 tbremoting 交互。
- 连接个数:单连接
- 连接方式:长连接
- 传输协议:TCP
- 传输方式:NIO 异步传输
- 序列化:Hessian 二进制序列化
- 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。
- 适用场景:常规远程服务方法调用
zookeeper 注册中心
流程说明
支持功能
框架设计
整体设计
图例说明:
- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
各层说明
关系说明
依赖关系
图例说明:
- 图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。
- 图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点。
- 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
- 图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。
调用链
领域模型
在 Dubbo 的核心领域模型中:
- Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
- Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
- Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。
Dubbo接口测试工具
整体设计
首先确定项目的总体功能。我们要的功能很简单,输入 ┈━═☆ 转发 ┈━═☆ 展示。
输入部分
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。既然是远程调用,那么必定存在一个服务端,也就是生产者。知道有服务端还不够,还得知道服务端的IP和端口,因为计算机网络都是通过Socket(套接字)来进行通讯的。那么怎样知道服务端的IP和端口的呢?
Dubbo可以使用Zookeeper、Redis等作为注册中心。以Zookeeper为例,它会把IP、端口、接口、方法这些拼接成Url,然后写到Zookeeper的/dubbo/xxx/providers 节点上。
RPC框架不一定需要使用注册中心,但有了它可以做很多东西。我们已经知道了服务端的IP和端口,还需要一种协议,你可以理解为一种服务端和客户端都能听到的语言,也就是接口。而接口会包含接口名称、方法名称还有参数这些东西。
回想一下Dubbo Consumer的使用方法。
1、配置zookeeper地址。
2、引用API的jar包
3、输入接口、方法、参数等。
输入部分跟Dubbo保持一致,这样一来,接口测试工具就可以不改变用户习惯的前提下充当一个泛化Dubbo客户端。
转发部分
1、获取Provider的IP和Port。
前面已经提到,可以通过注册中心拿到服务器的IP和地址,也就是所谓的服务发现。Provider会把自身的一系列参数拼接成url,然后存放到zookeeper的provider节点上。所以只需要从Zookeeper获取Provider的所有节点,稍作解析就能得到Provider列表,从而得到IP和Port。
2、连接Provider。
Consumer和Provider默认使用Netty作为通信框架,Provider暴露服务的时候会启动一个Netty Server,Provider在初始化interface的时候,除了会向zookeeper注册,还会通过export方法在本地启动Netty服务器,监听暴露的端口,等待连接建立。而Consumer则会启动一个Netty客户端和Provider建立单一长连接来收发数据。所以我们可以写一个Netty Client,然后连接上Provider的Netty Server。
3、收发数据
连接上Provider后,下一步就可以收发数据了。Netty连接建立后会返回一个Channel,这就是数据传输的管道。调用write方法就可以写入数据,推送到服务器了。
前面输入部分提到接口、方法、参数,把这些信息封装成规定格式的数据包,然后编码,经过网络传输,然后服务端解码,然后分离出接口类、方法、参数,服务端就可以通过反射执行对应的方法了。
4、响应。
请求包发送出去了,我们得拿到响应,不然后面就没法展示。怎样知道响应报文对应哪个发送报文呢?
Dubbo在Request包中封装了一个mId的流水号,通过它就可以跟踪发送的包。
流水号在分布式系统幂等去重、顺序重整、异步调用跟踪等起着重要的作用。在封装数据包的时候,可以把流水号记录下来。然后可以阻塞等待这个流水号数据包的响应。
可以往Pipeline里添加一个自定义ChannelHandler,用来监听数据接收事件。当服务端执行完方法调用,把数据包返还回来。编码、网络传输、到客户端解码,我们就可以拿到这个流水号了,然后修改这个流水号的等待状态,结束阻塞,这样就可以把数据返回前端页面展示了。
展示部分
示部分比较简单,直接把RpcResult对象转换一下就可以输出到页面了。
功能特性
极简模式:通过dubbo提供的telnet协议收发数据。
普通模式:通过封装netty客户端收发数据。
用例模式:通过缓存数据,方便下一次操作,依赖普通模式。
依赖列表:通过分析pom文件,展示已经加载的jar包。
依赖编辑:可以直接编辑pom文件,新增修改依赖jar。
注册中心:可以添加或删除zookeeper注册中心。
系统配置:可以清空jar或者重新加载jar。