Dubbo笔记(二)

四.Dubbo调用过程

或许目前有些同学还不能理解整个组件穿起来的工作工程,所以先以服务暴露/注册为例子简单描述下。首先服务端(Provider服务提供者)在框架启动时,会初始化服务实例,通过Proxy组件调用具体协议(Protocol),把服务端要暴露的接口封装成Invoker(真实类型时AbstractProxyInvoker),然后转换成Exporter,这个时候框架会打开服务端口等,并记录服务实例到内存中,最后通过Registry服务元数据注册到注册中心(比如Zookeeper)。这就是服务端整个接口暴露的过程。关于这里提到了几个组件,在做一下具体的说明:

1Proxy组件:我们知道Dubbo中只需哟引用一个接口就可以调用远程的服务。其实是Dubbo框架为我们生成了代理类,调用方法其实也是Proxy组件为我们生成的代理方法,最后会自动发起远程/本地调用,并返回结果,整个过程对我们是完全透明的。
2Protocol:其实协议就是对数据的一种约定。它可以把我们对接口的配置通过不同的协议转换成不同的Invoker对象。例如:用DubboProtocol可以把XML文件中一个远程接口的配置转换成一个DubboInvoker
3Exporter:用于暴露到注册中心的对象,它的内部属性持有了Invoker对象,我们可以认为它是在Invoker上包了一层。
4Registry:把Exporter注册到注册中心。

 

以上就是整个服务暴露的过程,如果Consumer在启动时在注册中心订阅了服务端的元数据(实例的IP、端口、实例上暴露的接口信息),这样Consumer就可以得到刚才暴露的服务了。

 

那么消费者的调用过程呢?

首先,调用者从Proxy开始,Proxy持有一个Invoker对象。然后触发Invoker调用。在Invoker调用中,需要使用Cluster负责容错,比如失败重试。Cluster在调用之前会通过Directory获取所有可以调用的远程服务Invoker列表(一个接口可能有多个实例/节点提供服务)。由于可以调用的服务有很多,此时如果用户配置了路由规则(如制定某些方法只能调用某个节点),那么Cluster还会根据路由规则将Invoker列表过滤一遍。

 

然后,存活下来的Invoker可能还会有很多,此时需要调用哪一个呢?这时候会通过loadBalance方位做负载均衡,最终选出一个可以调用的Invoker。这个Invoker在调用前又会经过一个FilterChain(过滤器链),这个FilterChain通常负责处理上下文、限流、计数等。

接着,会使用Client做数据传输,如我们常见的Netty Client等(Socket通信)。传输之前肯定需要做一个私有协议的构造,此时会用到Codec接口(主要作用是负责协议的编解码)。在这之后便是序列化过程了(Serialication)、然后传输到服务提供端。服务提供者接受到数据包,也会使用Codec处理协议头及一些包信息等。处理完之后在对整个报文做反序列化处理。 

随后这个请求会被分配到线程池(TheadPool)中进行处理,Server会处理这些请求,根据请求查找赌赢的Exporter(它内部持有了Invoker)。Invoker是被封装器模式一层一层套了非常多的Filter的,因此在调用最终的实现类之前,又会经过一个服务提供者端的过滤器链。

最终得到了具体的接口的真实实现并调用,在原路返回结果。

这里的话整个RPC的调用过程就结束了。

 

五.注册中心

说完服务暴露,再回头来看看注册中心。Dubbo 通过注册中心实现了分布式环境中服务的注册和发现。

其主要作用如下:

  • 动态载入服务。服务提供者通过注册中心,把自己暴露给消费者,无须消费者逐个更新配置文件。
  • 动态发现服务。消费者动态感知新的配置,路由规则和新的服务提供者。
  • 参数动态调整。支持参数的动态调整,新参数自动更新到所有服务节点。
  • 服务统一配置。统一连接到注册中心的服务配置。

 

配置中心工作流

先看看注册中心调用的流程图:

  • 提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
  • 消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
  • 服务治理中心(dubbo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
  • 当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。

注册中心工作原理

Dubbo 有四种注册中心的实现,分别是 ZooKeeperRedisSimple Multicast

这里着重介绍一下 ZooKeeper 的实现。ZooKeeper 是负责协调服务式应用的。

它通过树形文件存储的 ZNode /dubbo/Service 目录下面建立了四个目录,分别是:

 

  • Providers 目录下面,存放服务提供者 URL 和元数据。
  • Consumers 目录下面,存放消费者的 URL 和元数据。
  • Routers 目录下面,存放消费者的路由策略。
  • Configurators 目录下面,存放多个用于服务提供者动态配置 URL 元数据信息。

客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。

根据 ZooKeeper 客户端的特性,会在对应 ZNode 的目录上注册一个 Watcher,同时让客户端和注册中心保持 TCP 长连接。
如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据 ZNode 节点中版本变化进行。

 

六.Dubbo 集群容错

 

分布式服务多以集群形式出现,Dubbo 也不例外。在消费服务发起调用的时候,会涉及到 ClusterDirectoryRouterLoadBalance 几个核心组件。

先看看他们是如何工作的:

生成 Invoker 对象。根据 Cluster 实现的不同,生成不同类型的 ClusterInvoker 对象。通过 ClusertInvoker 中的 Invoker 方法启动调用流程。

获取可调用的服务列表,可以通过 Directory  List 方法获取。这里有两类服务列表的获取方式。

 

分别是 RegistryDirectory  StaticDirectory

 

  • RegistryDirectory:属于动态 Directory 实现,会自动从注册中心更新 Invoker 列表,配置信息,路由列表。
  • StaticDirectory:它是 Directory 的静态列表实现,将传入的 Invoker 列表封装成静态的 Directory 对象。 

 Directory 获取所有 Invoker 列表之后,会调用路由接口(Router)。其会根据用户配置的不同策略对 Invoker 列表进行过滤,只返回符合规则的 Invoker

假设用户配置接口 A 的调用,都使用了 IP  192.168.1.1 的节点,则 Router 会自动过滤掉其他的 Invoker,只返回 192.168.1.1  Invoker

这里介绍一下 RegistryDirectory 的实现,它通过 Subscribe  Notify 方法,订阅和监听注册中心的元数据。

Subscribe,订阅某个 URL 的更新信息。Notify,根据订阅的信息进行监听。包括三类信息,配置 Configurators,路由 Router,以及 Invoker 列表。

管理员会通过 dubbo-admin 修改 Configurators 的内容,Notify 监听到该信息,就更新本地服务的 Configurators 信息。

同理,路由信息更新了,也会更新服务本地路由信息。如果 Invoker 的调用信息变更了(服务提供者调用信息),会根据具体情况更新本地的 Invoker 信息。

通过前面三步生成的 Invoker 需要调用最终的服务,但是服务有可能分布在不同的节点上面。所以,需要经过 LoadBalance

Dubbo 的负载均衡策略有四种:

 

  • Random LoadBalance,随机,按照权重设置随机概率做负载均衡。
  • RoundRobinLoadBalance,轮询,按照公约后的权重设置轮询比例。
  • LeastActiveLoadBalance,按照活跃数调用,活跃度差的被调用的次数多。活跃度相同的 Invoker 进行随机调用。
  • ConsistentHashLoadBalance,一致性 Hash,相同参数的请求总是发到同一个提供者。

最后进行 RPC 调用。如果调用出现异常,针对不同的异常提供不同的容错策略。Cluster 接口定义了 9 种容错策略,这些策略对用户是完全透明的。

 

用户可以在标签上通过 Cluster 属性设置:

  • Failover,出现失败,立即重试其他服务器。可以设置重试次数。
  • Failfast,请求失败以后,返回异常结果,不进行重试。
  • Failsafe,出现异常,直接忽略。
  • Failback,请求失败后,将失败记录放到失败队列中,通过定时线程扫描该队列,并定时重试。
  • Forking,尝试调用多个相同的服务,其中任意一个服务返回,就立即返回结果。
  • Broadcast,广播调用所有可以连接的服务,任意一个服务返回错误,就任务调用失败。
  • Mock,响应失败时返回伪造的响应结果。
  • Available,通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果。
  • Mergable,将多个节点请求合并进行返回。

七.远程调用

服务消费者经过容错,Invoker 列表,路由和负载均衡以后,会对 Invoker 进行过滤,之后通过 Client 编码,序列化发给服务提供者。

 

服务消费者调用服务提供者的前后,都会调用 Filter(过滤器)。
可以针对消费者和提供者配置对应的过滤器,由于过滤器在 RPC 执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。

 

Dubbo 系统有自带的系统过滤器,服务提供者有 11 个,服务消费者有 5 个。过滤器的使用可以通过 @Activate 的注释,或者配置文件实现。

过滤器的使用遵循以下几个规则:

  • 过滤器顺序,过滤器执行是有顺序的。例如,用户定义的过滤器的过滤顺序默认会在系统过滤器之后。又例如,上图中 filter=“filter01, filter02”filter01 过滤器执行就在 filter02 之前。
  • 过滤器失效,如果针对某些服务或者方法不希望使用某些过滤器,可以通过“-”(减号)的方式使该过滤器失效。例如,filter=“-filter01”
  • 过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效。

由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上 Dubbo 采用了装饰器模式,将 Invoker 进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样。

调用请求经过过滤以后,会以 Invoker 的形式对 Client 进行调用。Client 会交由底层 I/O 线程池处理,其包括处理消息读写,序列化,反序列化等逻辑。

同时会对 Dubbo 协议进行编码和解码操作。Dubbo 协议基于 TCP/IP 协议,包括协议头和协议体。

协议体包含了传输的主要内容,其意义不言而喻,它是由 16 字节长的报文组成,每个字节包括 8 个二进制位。

服务消费者在调用之前会将上述服务消息体,根据 Dubbo 协议打包好。框架内部会调用 DefaultFuture 对象的 get 方法进行等待。

在准备发送请求的时候,才创建 Request 对象,这个对象会保存在一个静态的 HashMap 中,当服务提供者处理完 Request 之后,将返回的 Response 放回到 Futures HashMap 中。 

HashMap 中会找到对应的 Request 对象,并且返回给服务消费者。

协议打包好以后就需要给协议编码和序列化。这里需要用到 Dubbo 的编码器,其过程是将信息传化成字节流。

Dubbo 协议编码请求对象分为使用 ExchangeCodec 中的两个方法,encodeRequest 负责编码协议头和 encodeRequestData 编码协议体。

同样通过 encodeResponse 编码协议头,encodeResponseData 编码协议体。

服务消费者和提供者都通过 decode decodeBody 两个方法进行解码,不同的是解码有可能在 IO 线程或者 Dubbo 线程池中完成。

 

虽然,编码和解码的细节在这里不做展开,但是以下几点需要注意:

  • 构造 16 字节的协议头,特别是需要创建前面两个字节的魔法数,也就是“0xdabb”,它是用来分割两个不同请求的。
  • 生成唯一的请求/响应 ID,并且根据这个 ID 识别请求和响应协议包。
  • 通过协议头中的 19-23 位的描述,进行序列化/反序列化操作。
  • 为了提高处理效率,每个协议都会放到 Buffer 中处理。

当服务提供者收到请求协议包以后,先将其放到 ThreadPool 中,然后依次处理。

由于服务提供者在注册中心是通过 Exporter 的方式暴露服务的,服务消费者也是通过 Exporter 作为接口进行调用的。

Exporter 是将 Invoker 进行了包装,将拆开的 Invoker 进行 Filter 过滤链条进行过滤以后,再去调用服务实体。最后,将信息返回给服务消费者。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/harpoonJava/p/13126659.html