用户怎么调用dubbo?

Dubbo用户使用总结(一)

Dubbo采用全Spring的配置方式,对应用没有任何API的侵入,需要Spring加载Dubbo的配置(基于Schema扩展进行加载 具体Schema的扩展 )。

dubbo的两种配置方式:

其中返回值中的RpcContext类中的各种方法会在下文中的“上下文信息”中具体介绍。

该类是加载Spring配置,并启动provider。具体XML路径根据项目而定。

[图片上传失败...(image-847139-1590845170673)]

用户怎么调用dubbo?  第1张

7.Dubbo远程调用(要配合下一篇一起看)

如果我们手动写一个简单的RPC调用,一般需要把服务调用的信息传递给服务端,包括每次服务调用的一些共用信息包括服务调用接口、方法名、方法参数类型和方法参数值等,在传递方法参数值时需要先序列化对象并经过网络传输到服务端,在服务端接受后再按照客户端序列化的顺序再做一次反序列化,然后拼装成请求对象进行服务反射调用,最终将调用结果传给客户端。Dubbo的实现也基本是相同的原理,下图是Dubbo一次完整RPC调用中经过的步骤:

首先在客户端启动时,会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列表聚合成一个Invoker,每次RPC调用前会通过Directory#list获取providers地址(已经生成好的Invoker地址),获取这些服务列表给后续路由和负载均衡使用。对应上图①中将多个服务提供者做聚合。在框架内部实现Directory接口的是RegistryDirectory类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取和订阅服务提供者、动态配置和路由项。

在Dubbo发起服务调用时,所有路由和负载均衡都是在客户端实现的。客户端服务调用首先会触发路由操作,然后将路由结果得到的服务列表作为负载均衡参数,经过负载均衡后会选出一台机器进行RPC调用,这3个步骤一次对应图中②③④。客户端经过路由和负载均衡后,会将请求交给底层IO线程池(如Netty)进行处理,IO线程池主要处理读写、序列化和反序列化等逻辑,因此这里一定不能阻塞操作,Dubbo也提供参数控制(decode.in.io)参数,在处理反序列化对象时会在业务线程池中处理。在⑤中包含两种类似的线程池,一种是IO线程池(Netty),另一种是Dubbo业务线程池(承载业务方法调用)。

目前Dubbo将服务调用和Telnet调用做了端口复用,子啊编解码层面也做了适配。在Telnet调用时,会新建立一个TCP连接,传递接口、方法和json格式的参数进行服务调用,在编解码层面简单读取流中的字符串(因为不是Dubbo标准头报文),最终交给Telnet对应的Handler去解析方法调用。如果不是Telnet调用,则服务提供方会根据传递过来的接口、分组和版本信息查找Invoker对应的实例进行反射调用。在⑦中进行了端口复用,Telnet和正常RPC调用不一样的地方是序列化和反序列化使用的不是Hessian方式,而是直接使用fastjson进行处理。

讲解完主要调用原理,接下来开始探讨细节,比如Dubbo协议、编解码实现和线程模型等,本篇重点主要放在⑤⑥⑦。

Dubbo协议参考了现有的TCP/IP协议,每一次RPC调用包括协议头和协议体两部分。16字节长的报文头部主要包含魔数(0xdabb),以及当前请求报文是否是Request、Response、心跳和事件的信息,请求时也会携带当前报文体内序列化协议编号。除此之外,报文头还携带了请求状态,以及请求唯一标识和报文体长度。

在消息体中,客户端严格按照序列化顺序写入消息,服务端也会遵循相同的顺序读取消息,客户端发起的请求消息体一次依次保存下列内容:Dubbo版本号、服务接口名、服务接口版本、方法名、参数类型、方法参数值和请求额外参数(attachment)。

服务端返回的响应消息体主要包含回值状态标记和返回值,其中回值状态标记包含6中:

我们知道在网络通信中(TCP)需要解决网络粘包/解包的问题,常用的方法比如用回车、换行、固定长度和特殊分隔符进行处理,而Dubbo是使用特殊符号0xdabb魔法数来分割处理粘包问题。

在实际场景中,客户端会使用多线程并发调用服务,Dubbo如何做到正确响应调用线程呢?关键在于协议头的全局请求id标识,先看原理图:

当客户端多个线程并发请求时,框架内部会调用DefaultFuture对象的get方法进行等待。在请求发起时,框架内部会创建Request对象,这时候会被分配一个唯一id,DefaultFuture可以从Request中获取id,并将关联关系存储到静态HashMap中,就是上图中的Futures集合。当客户端收到响应时,会根据Response对象中的id,从Futures集合中查找对应DefaultFuture对象,最终会唤醒对应的线程并通知结果。客户端也会启动一个定时扫描线程去探测超时没有返回的请求。

先了解一下编解码器的类关系图:

如上,AbstractCodec主要提供基础能力,比如校验报文长度和查找具体编解码器等。TransportCodec主要抽象编解码实现,自动帮我们去调用序列化、反序列化实现和自动cleanup流。我们通过Dubbo编解码继承结构可以清晰看到,DubboCodec继承自ExchageCodec,它又再次继承了TelnetCodec实现。我们前面说过Telnet实现复用了Dubbo协议端口,其实就是在这层编解码做了通用处理。因为流中可能包含多个RPC请求,Dubbo框架尝试一次性读取更多完整报文编解码生成对象,也就是图中的DubboCountCodec,它的实现思想比较简单,依次调用DubboCodec去解码,如果能解码成完整报文,则加入消息列表,然后触发下一个Handler方法调用。

编码器的作用是将Java对象转成字节流,主要分两部分,构造报文头部,和对消息体进行序列化处理。所有编辑码层实现都应该继承自ExchangeCodec,当Dubbo协议编码请求对象时,会调用ExchangeCodec#encode方法。我们来看下这个方法是如何对请求对象进行编码的:

如上,是Dubbo将请求对象转成字节流的过程,其中encodeRequestData方法是对RpcInvocation调用对象的编码,主要是对接口、方法、方法参数类型、方法参数等进行编码,在DubboCodec#encodeRequestData中对此方法进行了重写:

如上,响应编码与请求编码的逻辑基本大同小异,在编码出现异常时,会将异常响应返回给客户端,防止客户端只能一直等到超时。为了防止报错对象无法在客户端反序列化,在服务端会将异常信息转成字符串处理。对于响应体的编码,在DubboCodec#encodeResponseData方法中实现:

注意不管什么样的响应,都会先写入1个字节的标识符,具体的值和含义前面已经讲过。

解码相对更复杂一些,分为2部分,第一部分是解码报文的头部,第二部分是解码报文体内容并将其转换成RpcInvocation对象。我们先看服务端接受到请求后的解码过程,具体解码实现在ExchangeCodec#decode方法:

可以看出,解码过程中需要解决粘包和半包问题。接下来我们看一下DubboCodec对消息题解码的实现:

如上,如果默认配置在IO线程解码,直接调用decode方法;否则不做解码,延迟到业务线程池中解码。这里没有提到的是心跳和事件的解码,其实很简单,心跳报文是没有消息体的,事件又消息体,在使用Hessian2协议的情况下默认会传递字符R,当优雅停机时会通过发送readonly事件来通知客户端当前服务端不可用。

接下来,我们分析一下如何把消息体转换成RpcInvocation对象,具体在DecodeableRpcInvocation#decode方法中:

解码请求时,严格按照客户端写数据的顺序处理。

解码响应和解码请求类似,调用的同样是DubboCodec#decodeBody,就是上面省略的部分,这里就不赘述了,重点看下响应体的解码,即DecodeableRpcResult#decode方法:

如果读者熟悉Netty,就很容易理解Dubbo内部使用的ChannelHandler组件的原理,Dubbo内部使用了大量的Handler组成类似链表,依次处理具体逻辑,包括编解码、心跳时间戳和方法调用Handler等。因为Nettty每次创建Handler都会经过ChannelPipeline,大量的事件经过很多Pipeline会有较多开销,因此Dubbo会将多个Handler聚合成一个Handler。(个人表示,这简直bullshit)

Dubbo的Channelhandler有5中状态:

Dubbo针对每个特性都会实现对应的ChannelHandler,在讲解Handler的指责之前,我们Dubbo有哪些常用的Handler:

Dubbo提供了大量的Handler去承载特性和扩展,这些Handler最终会和底层通信框架做关联,比如Netty等。一次完整的RPC调用贯穿了一系列的Handler,如果直接挂载到底层通信框架(Netty),因为整个链路比较长,则需要大量链式查找和事件,不仅低效,而且浪费资源。

下图展示了同时具有入站和出站ChannelHandler的布局,如果一个入站事件被触发,比如连接或数据读取,那么它会从ChannelPipeline头部一直传播到ChannelPipeline的尾端。出站的IO事件将从ChannelPipeline最右边开始,然后向左传播。当然ChannelPipeline传播时,会检测入站的是否实现了ChannelInboundHandler,出站会检测是否实现了ChannelOutboundHandler,如果没有实现,则自动跳过。Dubbo框架中实现这两个接口类主要是NettyServerHandler和NettyClientHandler。Dubbo通过装饰者模式包装Handler,从而不需要将每个Handler都追加到Pipeline中。因此NettyServer和NettyClient中最多有3个Handler,分别是编码、解码和NettyHandler。

讲完Handler的流转机制后,我们再来探讨RPC调用Provider方处理Handler的逻辑,在DubboProtocol中通过内部类继承自ExchangeHandlerAdapter,完成服务提供方Invoker实例的查找并进行服务的真实调用。

如上是触发业务方法调用的关键,在服务暴露时服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中,客户端调用过来时必须携带相同信息构造的key,找到对应Exporter(里面持有Invoker)然后调用。

我们先跟踪getInvoker的实现,会发现服务端唯一标识的服务由4部分组成:端口、接口名、接口版本和接口分组。

如上,Dispatcher是线程池的派发器。这里需要注意的是,Dispatcher真实的职责是创建有线程派发能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHanlder,其本身并不具备线程派发能力。

Dispatcher属于Dubbo中的扩展点,这个扩展点用来动态产生Handler,以满足不同的场景,目前Dubbo支持一下6种策略调用:

具体需要按照使用场景不同启用不同的策略,建议使用默认策略,如果在TCP连接中需要做安全或校验,则可以使用ConnectionOrderedDispatcher策略。如果引入新的线程池,则不可避免的导致额外的线程切换,用户可在Dubbo配置中指定dispatcher属性让具体策略生效。

在Dubbo内部,所有方法调用都被抽象成Request/Response,每次调用都会创建一个Request,如果是方法调用则返回一个Response对象。HeaderExceptionExchangeHandler就是用了处理这种场景,主要负责4中事情:

(1) 更新发送和读取请求时间戳。

(2) 判断请求格式或编解码是否有错,并响应客户端失败的具体原因。

(3) 处理Request请求和Response正常响应。

(4) 支持Telnet调用。

我们先来看一下HeaderExchangeHandler#received实现:

dubbo泛化调用使用及原理解析

通常我们想调用别人的dubbo服务时,我们需要在项目中引入对应的jar包。而泛化调用的作用是,我们无需依赖相关jar包,也能调用到该服务。

这个特性一般使用在网关类项目中,在业务开发中基本不会使用。

假设我现在要调用下面的接口服务

在xml文件做以下配置

然后注入使用

在两种调用方式中,我们都需要使用被调用接口的字符串参数生成GenericService,通过GenericService的$invoke间接调用目标接口的接口。

$invoke的三个参数分别为,方法名,方法参数类型数组,方法参数数组。

可以看到泛化调用的一个复杂性在于$invoke的第三个参数的组装,下面介绍几种复杂入参的调用方式

首先丰富提供者接口

与入参相似,虽然$invoke的返回定义为Object,实际上针对不同类型有不同的返回。

泛化调用和直接调用在消费者者端,在 使用上的区别 是,我们调用服务时使用的接口为GenericService,方法为$invoker。在 底层的区别 是,消费者端发出的rpc报文发生了变化。

在使用上,不管哪种配置方式,我们都需要配置generic=true

设置generic=true后,RefereceConfig的interfaceClass会被强制设置为GenericService

这也使得我们的RefereanceBean返回的是GenericService类型的代理。

生成的代理是GenericService的代理只是我们使用方式上的变化,更为核心的是,底层发送的rpc报文发生了什么变化。

Dubbo的rpc报文分为header和body两部分。我们这边只需要关注body部分。构造逻辑如下

那么我们通过直接调用与泛化调用ByeService的bye方法在报文上有啥区别呢?

我一开始以为报文中的path是GenericeService,其实并没有,path就是我们调用的目标方法。

path来源???todo

而报文中的方法名,方法参数类型以及具体参数,还是按照GenericeService的$invoke方法入参传递的。

这么个二合一的报文,发送到提供者那边,它估计也会很懵逼,我应该怎么执行?

所以针对泛化调用报文还会把generic=true放在attchment中传递过去

具体逻辑在GenericImplFilter中。

GenericImplFilter中有很多其他逻辑,比如泛化调用使用的序列化协议,正常接口走泛化调用的模式,我们只需要设置attachment的那部分。

知道消费者端报文发生了什么变化,那么接下来就去看提供者端如何处理这个改造后的报文。

总结一下ReferenceConfig中interfaceClass和interfaceName的区别?(这道面试题好像不错)

interfaceClass用于指定生成代理的接口

interfaceName用于指定发送rpc报文中的path(告诉服务端我要调用那个服务)

消费者泛化调用的rpc报文传递到提供者还不能直接使用,虽然path是对的,但是实际的方法名,参数类型,参数要从rpc报文的参数中提取出来。

GenericFilter就是用来做这件事情。

在提供者这边,针对泛化调用的逻辑全部封装到了GenericFilter,解耦的非常好。

注意第4个条件,一开始很疑惑,后来发现rpc报文中的path是目标接口的,这边invoker.getInterface()返回的肯定就是实际接口了

这边有个疑问,为什么这边还要再次反序列化一次,netty不是有decoder么??

嗯,你别忘了,针对一个POJO你传过来是一个Map,从Map转换为POJO需要这边进一步处理。

这边需要注意一下!!针对接口的泛化调用,抛出的异常都会经过GenericException包装一下。

从功能上来看,泛化调用提供了在没有接口依赖情况下进行的解决方案,丰富框架的使用场景。

从设计上来看,泛化调用的功能还是通过扩展的方式实现的,侵入性不强,值得学习借鉴。

使用spring boot构建的客户端项目wolf调用dubbo服务

lion:dubbo服务的提供方,即服务端

项目地址:

wolf:dubbo服务的调用方,即客户端

项目地址:

wolf项目也是基于spring boot搭建的,结构和lion类似,下面我主要说下,对dubbo服务的调用,作为客户端这一侧,要做哪些配置。

1、在wolf-rpc模块依赖服务端的一些接口jar包,主要是lion-domain和lion-export

2、在wolf-rpc中增加dubbo调用侧的一些配置spring-dubbo.xml,spring-goods-consumer.xml

其中spring-dubbo.xml文件中主要放置的是对注册中心的一些参数配置,内容如下:

?xml version="1.0" encoding="UTF-8"?

beans xmlns=" " xmlns:xsi=" " xmlns:dubbo=" "

xsi:schemaLocation="

"

dubbo:application name="${server.name}"/

dubbo:protocol name="dubbo" port="${dubbo.port}" /

dubbo:provider timeout="3000" threadpool="fixed" threads="1000" accepts="1000" /

dubbo:registry id="registry" protocol="zookeeper" address="${zookeeper.address}" /

/beans

spring-goods-consumer.xml中主要是对远端提供侧服务的配置,内容如下

?xml version="1.0" encoding="UTF-8"?

beans xmlns=" "

xmlns:xsi=" " xmlns:dubbo=" "

xsi:schemaLocation="

"

dubbo:reference id="helloService" interface="org.lion.export.HelloService" version="${dubbo.version}" timeout="${dubbo.timeout}"/

/beans

3、在service层使用这个helloService

@Service("itemService")

public class ItemServiceImpl implements ItemService{

@Resource

private ItemDraftMapper itemDraftMapper;

使用@Resource注入该远端服务(实际上此时注入的是远端服务的一个代理类)

4、增加测试controller

@Controller

@RequestMapping("dubbo")

public class DubboTestController {

@Resource

private ItemService itemService;

}

5、修改wolf项目端口为8082,启动项目后测试

6、看看duboo-admin上,客户端是否注入

下图可以看到客户端项目wolf已经可以看到了。

Dubbo的简要执行流程

1. 服务器启动,运行服务提供者。

2. 服务提供者在启动时,向注册中心(zookeeper)注册自己提供的服务。

3. 服务消费者在启动时,向注册中心订阅自己所需的服务。

4. 注册中心返回服务提供者地址列表给消费者,(若有变更,注册中心将基于长连接推送变更数据给消费者)

5.服务的消费者,从地址列表中,基于负载均衡,选一台提供者的服务器进行调用,若是失败,在从 地址列表中,选择另一台调用.

6.期间Dubbo的监控中心,会记录定时消费者和提供者,的调用次数和时间.

Dubbo——服务调用、服务暴露、服务引用过程

1、InvokerInvocationHandler jdk动态代理

5、RegistryDirector返回Invokers

Router分为:Script 脚本路由、Condition 条件路由

6、通过MockInvokersSelector的route方法(getNormalInvokers)拿到能正常执行的invokers

8、当回到AbstractClusterInvoker后,执行(默认FailoverClusterInvoker,根据配置的是,Failfast Cluster(快速失败) , Failsafe Cluster(失败安全) , Failback Cluster(失败自动恢复) , Forking Cluster(并行调用多个服务器,只要一个成功即返回) , Broadcast Cluster(广播调用所有提供者,逐个调用,任意一台报错则报错))doInvoker方法

9、FailoverClusterInvoker调用AbstractClusterInvoker的select方法

10、执行doSelect方法

11、调用AbstractLoadbalance的select方法

12、根据配置的负载均衡策略调用对应的(如RoundRobinLoadBalance)类的doSelect方法

13、返回invokers.get()方法

14、调用FailoverClusterInvoker的invoke方法

均继承自抽象类AbstractDirectory

Directory 获取 invoker 是从 methodInvokerMap 中获取的,主要都是读操作,那它的写操作是在什么时候写的呢?就是在回调方法 notify 的时候操作的,也就是注册中心有变化,则更新 methodInvokerMap 和 urlInvokerMap 的值

根据dubbo-admin配置的路由规则来过滤相关的invoker,当我们对路由规则点击启用,就会触发 RegistryDirectory 类的 notify 方法。

notify方法调用refreshInvoker方法。

route方法的实现类为ConditionRoute 根据条件进行过滤

1、调用mathThen方法

2、调用matchCondition方法

3、调用isMatch判断

4、调用isMatchGlobPattern方法

集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是Dubbo Cluster集群的作用。

通过cluster来指定集群容错方式

其实就是应对出错情况采取的策略

用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非提供者挂了,再连另一台,自动开启延迟链接,以减少长接数

启动时服务提供者将当前进程启动时间注册到ZK;服务消费者发现该节点后计算服务启动时间(相对当前时间),在默认预热时间的前20%时间内,该节点权重始终固定为2,这样客户端的负载均衡器只会分发极少的请求至节点。

在预热时间之后的80%时间内,该节点权重将随着时间的推移而线性增长;待预热时间到期后,权重自动恢复为默认值100;负载均衡器的内核是一个标准的WLC算法模块,即加权最少连接算法;

如果某个节点Hang住或宕机,其权重会迅速自动调节降低,避免持续性影响;当节点下线时,服务端提前触发权重调节,重载默认权重至1并发布到注册中心,服务消费者将迅速感知到该事件;

服务提供者优雅下线步骤(注意这套逻辑仅在服务端执行)在ok.htm?down=true对应的controller中加入下列逻辑,注意要判断down是否为true,因为正常来说false表示启动验证而不是关机

服务者消费者配置

dubbo服务支持参数动态调整,例如动态调整权重,但dubbo实现方式较为特殊,并不是常规思路。

ServiceConfig类拿到对外提供服务的实际类ref,然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转换(javassistProxyFacory、JdkProxyFactory),接着要做Invoker转换到Export的过程

服务发布:本地暴露、远程暴露

为什么会有 本地暴露 和 远程暴露 呢?不从场景考虑讨论技术的没有意义是.在dubbo中我们一个服务可能既是 Provider ,又是 Consumer ,因此就存在他自己调用自己服务的情况,如果再通过网络去访问,那自然是舍近求远,因此他是有 本地暴露 服务的这个设计.从这里我们就知道这个两者的区别

1、spring启动,解析配置文件

2、创建dubbo标签解析器

3、解析dubbo标签

4、ServiceBean解析

5、容器创建完成,触发ContextRefrestEvent

6、export暴露服务

7、duExportUrls

8、doExportUrlsFor1Protocol

9、getInvoker

10、protocol.export

11、开启服务器 openServer()如nettyServer

12、注册服务到注册中心 registerProvider

Filter 在服务暴露前,做拦截器初始化,在加载所有拦截器时会过滤支队provider生效的数据。

可以。zookeeper的信息会缓存到本地作为一个缓存文件,并且转换成 properties 对象方便使用。建立线程池,定时检测并连接注册中心,失败了就重连。

注册服务到zk其实就是在zk上创建临时节点,当节点下线或者down掉时,即会删除临时节点,从而使服务从可用列表中剔除。

持久节点

临时节点

1、export的时候进行zk订阅

2、设置监听回调的地址,回调给FailbackRegistry的notify

3、创建持久节点

4、设置对该节点的监听

5、更新新的服务信息,服务启动和节点更新回调,都会调用到这里

6、更新缓存文件

7、对比新旧信息是否有变化,有则重新暴露服务

高并发大业务量情况下,暂时屏蔽边缘业务

MockClusterInvoker

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。

以上内容为新媒号(sinv.com.cn)为大家提供!新媒号,坚持更新大家所需的互联网后端知识。希望您喜欢!

版权申明:新媒号所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请发送邮件至 k2#88.com(替换@) 举报,一经查实,本站将立刻删除。

(0)
上一篇 2023-09-23 13:41
下一篇 2023-09-23 13:41

相关推荐

发表回复

登录后才能评论