Dubbo接口测试工具

前言

近几年随着微服务化项目的崛起,逐渐成为许多中大型分布式系统架构的主流方式,而RPC在这其中扮演着重要的角色。市面上流行的RPC框架如DubboThriftHessiongRPC等。

本人所在的公司一直使用Dubbo,在使用Dubbo进行开发时,想要快速知道某些dubbo接口的响应结果,但不想启动项目(因为这些项目不一定是你负责的,不会部署而且极其笨重),也不想新建一个dubbo客户端项目(费事且占地方),也不想开telnet客户端连接口(麻烦且有限制)。

因Dubbo支持dubbormihessianhttpwebservicethriftredis等多种协议,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 个部分:

  1. User
  2. User-stub
  3. RPCRuntime
  4. Server-stub
  5. Server

RPC结构图1

这里 user 就是 client 端,当 user 想发起一个远程调用时,它实际是通过本地调用 user-stub

user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。

远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用,调用结果再返回给 user 端。

以上是粗粒度的 RPC 实现概念结构,接下来进一步细化它应该由哪些组件构成,如下图所示。

RPC结构图2

RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel,并使用RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。

RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给RpcProcessor 去控制处理调用过程,最后再委托调用给RpcInvoker 去实际执行并返回调用结果。如下是各个部分的详细职责:

  1. RpcServer

    负责导出(export)远程接口

  2. RpcClient

    负责导入(import)远程接口的代理实现

  3. RpcProxy

    远程接口的代理实现

  4. RpcInvoker

    客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回

    服务方实现:负责调用服务端接口的具体实现并返回调用结果

  5. RpcProtocol

    负责协议编/解码

  6. RpcConnector

    负责维持客户方和服务方的连接通道和发送数据到服务方

  7. RpcAcceptor

    负责接收客户方请求并返回请求结果

  8. RpcProcessor

    负责在服务方控制调用过程,包括管理调用线程池、超时时间等

  9. RpcChannel

    数据传输通道

RPC工作原理

RPC的设计由ClientClient stubNetworkServer stubServer构成。

其中Client就是用来调用服务的;
Cient stub是用来把调用的方法和参数序列化的(因为要在网络中传输,必须要把对象转变成字节);
Network用来传输这些信息到Server stub;
Server stub用来把这些信息反序列化的;
Server就是服务的提供者,最终调用的就是Server提供的方法。

RPC工作原理

  1. Client像调用本地服务似的调用远程服务;

  2. Client stub接收到调用后,将方法、参数序列化;

  3. 客户端通过sockets将消息发送到服务端;

  4. Server stub 收到消息后进行解码(将消息对象反序列化);

  5. Server stub 根据解码结果调用本地的服务;

  6. 本地服务执行(对于服务端来说是本地执行)并将结果返回给Server stub;

  7. Server stub将返回结果打包成消息(将结果消息对象序列化);

  8. 服务端通过sockets将消息发送到客户端;

  9. Client stub接收到结果消息,并进行解码(将结果消息发序列化);

  10. 客户端得到最终结果。

RPC 调用分以下两种:

同步调用:客户方等待调用执行完成并返回结果。

异步调用:客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。

异步和同步的区分在于是否等待服务端执行完成并返回结果。

Dubbo介绍

架构

Dubbo架构

节点角色

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

调用关系

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

生态系统

Dubbo生态系统1

Dubbo生态系统2

集群容错

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

Dubbo Cluster

节点关系

这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息;
Directory·表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更;
ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个;
Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等;
LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。

容错模式

Failover Cluster

失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。
重试次数配置如下:

1
<dubbo:service retries="2" />

1
<dubbo:reference retries="2" />

1
2
3
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</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 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。

dubbo-protocol

因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:

1
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />

Dispatcher

  • all ▄︻┻═┳一 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。
  • direct ▄︻┻═┳一 所有消息都不派发到线程池,全部在 IO 线程上直接执行。
  • message ▄︻┻═┳一 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
  • execution ▄︻┻═┳一 只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
  • connection ▄︻┻═┳一 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。

ThreadPool

  • fixed ▄︻┻═┳一 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省)
  • cached ▄︻┻═┳一 缓存线程池,空闲一分钟自动删除,需要时重建。
  • limited ▄︻┻═┳一 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。
  • eager ▄︻┻═┳一 优先创建Worker线程池。在任务数量大于corePoolSize但是小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException。(相比于cached:cached在任务数量超过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)

dubbo://

从上面的Dubbo生态系统中可以看到Dubbo支持多种协议,缺省协议是dubbo协议,即采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

特性

缺省协议,使用基于 mina 1.1.7hessian 3.2.1tbremoting 交互。

  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP
  • 传输方式:NIO 异步传输
  • 序列化:Hessian 二进制序列化
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。
  • 适用场景:常规远程服务方法调用

zookeeper 注册中心

zookeeper注册中心

流程说明

  1. 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址;
  2. 服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址;
  3. 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。

支持功能

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息;
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求;
  • 当会话过期时,能自动恢复注册数据,以及订阅请求;
  • 当设置 <dubbo:registry check="false" /> 时,记录失败注册和订阅请求,后台定时重试;
  • 可通过 <dubbo:registry username="admin" password="1234" /> 设置 zookeeper 登录信息;
  • 可通过 <dubbo:registry group="dubbo" /> 设置 zookeeper 的根节点,不设置将使用无根树;
  • 支持 * 号通配符 <dubbo:reference group="*" version="*" />,可订阅服务的所有分组和所有版本的提供者。

框架设计

整体设计

dubbo-framework

图例说明:

  • 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
  • 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
  • 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
  • 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。

各层说明

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类。
  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
  • protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

关系说明

  • 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。

  • 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。

  • Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。

  • Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。

  • Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。

  • Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。

依赖关系

dubbo-relation

图例说明:

  • 图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。
  • 图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点。
  • 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
  • 图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。

调用链

dubbo-extension

领域模型

在 Dubbo 的核心领域模型中:

  • Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
  • Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
  • Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。

Dubbo接口测试工具

整体设计

首先确定项目的总体功能。我们要的功能很简单,输入 ┈━═☆ 转发 ┈━═☆ 展示。

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。

本文标题:Dubbo接口测试工具

文章作者:王洪博

发布时间:2019年10月03日 - 18:10

最后更新:2019年10月11日 - 09:10

原始链接:http://whb1990.github.io/posts/aea42d14.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%