Dubbo笔记(二)
四.Dubbo调用过程
或许目前有些同学还不能理解整个组件穿起来的工作工程,所以先以服务暴露/注册为例子简单描述下。首先服务端(Provider服务提供者)在框架启动时,会初始化服务实例,通过Proxy组件调用具体协议(Protocol),把服务端要暴露的接口封装成Invoker(真实类型时AbstractProxyInvoker),然后转换成Exporter,这个时候框架会打开服务端口等,并记录服务实例到内存中,最后通过Registry把服务元数据注册到注册中心(比如Zookeeper)。这就是服务端整个接口暴露的过程。关于这里提到了几个组件,在做一下具体的说明:
(1)Proxy组件:我们知道Dubbo中只需哟引用一个接口就可以调用远程的服务。其实是Dubbo框架为我们生成了代理类,调用方法其实也是Proxy组件为我们生成的代理方法,最后会自动发起远程/本地调用,并返回结果,整个过程对我们是完全透明的。
(2)Protocol:其实协议就是对数据的一种约定。它可以把我们对接口的配置通过不同的协议转换成不同的Invoker对象。例如:用DubboProtocol可以把XML文件中一个远程接口的配置转换成一个DubboInvoker。
(3)Exporter:用于暴露到注册中心的对象,它的内部属性持有了Invoker对象,我们可以认为它是在Invoker上包了一层。
(4)Registry:把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 有四种注册中心的实现,分别是 ZooKeeper,Redis,Simple 和 Multicast。
这里着重介绍一下 ZooKeeper 的实现。ZooKeeper 是负责协调服务式应用的。
它通过树形文件存储的 ZNode 在 /dubbo/Service 目录下面建立了四个目录,分别是:
- Providers 目录下面,存放服务提供者 URL 和元数据。
- Consumers 目录下面,存放消费者的 URL 和元数据。
- Routers 目录下面,存放消费者的路由策略。
- Configurators 目录下面,存放多个用于服务提供者动态配置 URL 元数据信息。
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。
根据 ZooKeeper 客户端的特性,会在对应 ZNode 的目录上注册一个 Watcher,同时让客户端和注册中心保持 TCP 长连接。
如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据 ZNode 节点中版本变化进行。
六.Dubbo 集群容错
分布式服务多以集群形式出现,Dubbo 也不例外。在消费服务发起调用的时候,会涉及到 Cluster,Directory,Router,LoadBalance 几个核心组件。
先看看他们是如何工作的:
①生成 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 过滤链条进行过滤以后,再去调用服务实体。最后,将信息返回给服务消费者。