Dubbo

Dubbo 注册中心

一、注册中心概述

Dubbo 通过注册中心实现了分布式环境中各服务之间的注册与发现,是各个分布式节点之间的纽带。

  • 动态加入:一个服务提供者可以通过注册中心,动态的将自己暴露给其他消费者,无需消费者更新配置文件
  • 动态发现:一个消费者可以动态感知新配置、路由规则和新的服务提供者,无需重启服务使之生效
  • 动态调整:注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点
  • 统一配置:避免了本地配置导致每个服务配置不一致的问题

注册中心流程:

  • Provider 启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息
  • Consumer 启动时,也会向注册中心写入自己的元数据信息,并订阅Provider、Router 和配置元数据信息
  • 服务治理中心 Dubbo Admin 启动时,会同时订阅所有 Consumer、Provider、Router 和配置元数据信息
  • 当有 Provider 离开或加入时,provider 目录会发生变化,变化信息会动态通知给消费者和 dubboAdmin
  • 当 Consumer 发起 invoke 时,会异步将调用统计信息上报给 DubboAdmin

ZooKeeper 的注册中心实现:

在 Service 节点下有四个子目录,分别是 provider,consumer,routers,configurations保存各自的元数据信息。

通过四个目录来存储信息。

二、订阅发布

是整个注册中心的核心功能之一。可以动态的自动的,在 provider 有变化时,更新配置,并且让 consumer 和 admin 都收到注册中心的消息。

1、发布的实现

zk 调用的 zkClient 在注册中心上创建一个目录。

2、订阅的实现

Dubbo采用的是第一次启动拉取方式,后续接收事件重新拉取数据。

  • 在服务暴露时,服务端会订阅 configurators 用于监听动态配置
  • 在消费端启动时,消费端会订阅 providers、routers、configurators 这三个目录,分别对应 provider、router 和 config 的变更通知

dubboAdmin:

  • 客户端第一次连上注册中心,订阅时 会获取全量的数据,后续则通过监听事件进行更新。

  • 服务治理中心会处理所有 service 层的订阅,包括子节点。

Consumer:

  • 根据 URL 的类别,获取一组要订阅的路径:provider、routers、consumers、configurators
  • 然后拉取直接子节点的数据进行通知 notify

三、缓存机制

Consumer 或 Admin 获取注册信息后会做本地缓存,内存和磁盘中都有一份。

1、缓存的加载

服务初始化的时候,AbstractRegistry 构造函数里会从本地磁盘文件中把持久化的注册数据读到 properties 对象里,并加载到内存缓存中。

properties保存了所有服务提供者的 URL,使用 URL#serviceKey()作为 key,providerList、routerList、configList 作为 value。

2、缓存的保存与更新

异步使用线程池保存,同步直接调用 doSaveProperties。

四、重试机制

在FailbackRegistry中定义了五个conHashMap,存放失败情况的数据,调用 retry 方法的时候,会将这五个集合遍历和重试,重试成功就会移除。

五、设计模式

1、注册中心的逻辑部分使用了模板模式。

AbstractRegistry 实现了 Registry 接口中的注册、订阅、查询和通知等方法。但是这些方法只是简单的将 URL 加入对应的集合,没有具体的注册订阅逻辑。

FailBackRegistry 重写了 AbstractRegistry 的四大方法,并且添加了重试机制。

而具体的 ZookeeperRegistry 则实现了 FailBackRegistry 中的 do 的四个模板方法,实现了具体的订阅等逻辑。

2、注册中心的实现,是通过对应的工厂创建的。

ZookeeperRegistryFactory extends AbstractRegistryFactory implements RegistryFactory

ZookeeperRegistryFactory 中实现了 createRegistry 方法,返回一个注册中心的实例。

通过@Adaptive 注解读取配置信息来判断获得哪个具体的工厂实现类。

Dubbo 扩展点加载机制

扩展点就是让使用者在不改变 dubbo 项目的情况下,在应用中增加相应类及配置就可以实现扩展。

一、加载机制概述

1、Java SPI :Service Provider Interface

JavaSPI 使用策略模式,一个接口多种实现。我们只声明接口,具体实现通过配置来控制,用于具体实现类的装配:

1)定义一个接口以及对应的方法

2)编写该接口的一个实现类

3)在 META-INF/services目录下,创建一个以接口全路径命名的文件,文件内容为具体实现类的全路径名,用分行符分割

4)在代码中通过 java.util.ServiceLoader 来加载具体实现类

2、Java SPI缺点:

  • 会一次性实例化扩展点的所有实现
  • 加载失败会吞异常

Dubbo SPI 优化:

  • 只是加载配置文件中的类,不会全部立即初始化,增加了对 IOC 和 AOP 的支持
  • 会抛出真实异常并打印日志

3、扩展点的配置规范

在 META_INF/dubbo/下放置对应的 SPI配置文件,文件名为接口的全路径名,内容为 key=value 格式

4、扩展点的分类与缓存

Dubbo 既缓存class,也缓存实例。

5、扩展点的特性

自动包装、自动加载、自适应和自动激活。

二、扩展点注解

Dubbo 启停原理解析

一、配置解析

1、基于 schema 设计解析

2、基于 XML 配置原理解析

3、基于注解配置原理解析

二、服务暴露的实现原理

1、配置承载初始化

2、远程服务的暴露机制

dubbo 框架会根据优先级对配置信息做聚合处理。

第一步: 将 XML 或者注解转换成 serviceBean,然后将持有的服务实例通过动态代理转换成 Invoker,

第二步:把 Invoker 通过具体的协议,比如 Dubbo 协议,转换成 Exporter。

真正进行服务暴露的入口在 ServiceConfig#doExport 中,调用的是 doExportUrlsFor1Protocol 方法:

核心代码:

//第一步
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
//第二步
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);

第一步通过 PROXY_FACTORY 动态代理的方式创建 Invoker 对象,这边使用工厂模式,getInvoker 方法在不同的工厂类中实现。

第二步:

  1. protocol 实例会自动根据服务暴露 URL 做适配,有注册中心时会取出具体协议来创建注册中心,
  2. 然后取出 export 对应的服务 URL,
  3. 最后用 URL 对应的协议,默认是 dubbo 来进行服务暴露,暴露成功之后将服务数据注册到 ZK

细节实现:将服务实例ref 转换成 Invoker 之后,如果有注册中心,再通过 RegistryProtocol#export进行处理:

1)委托具体协议进行服务暴露,创建 NettyServer 监听端口和保存服务实例
2)创建注册中心对象,与注册中心创建 TCP 连接
3)注册服务元数据到注册中心
4)订阅 configurators 节点,监听服务动态属性变更时间
5)服务销毁收尾工作,比如关闭端口,反注册服务信息等

拦截器:

在用 Dubbo 协议进行服务暴露前,框架会做拦截器初始化,调用 ProtocolFilterWrapper#export 方法,触发 buildInvokerChain 进行拦截器构造。类似 mybatis 的拦截器机制。

拦截器之后调用 Dubbo 协议进行暴露:DubboProtocol#export 方法做了什么事呢:

1、根据服务分组、版本、接口和端口构造 key
2、把 exporter 存储到单例 DubboProtocol 中
3、服务初次暴露需要创建监听器
4、创建 NettyServer 进行处理,调用 handler。

整体流程:

1.暴露本地服务

2.暴露远程服务

3.启动netty

4.链接zookeeper

5.到zookeeper注册

6.zookeeper事件通知

三、服务消费的实现原理

第一步:通过持有远程服务实例生成 invoker

通过 ReferenceConfig#createProxy 来实现:

1)判断是否同一个 JVM 内部引用
2)直接使用 injvm 协议从内存中获取实例
3)在注册中心追加消费者元数据信息
4)分别处理单中心和多中心的消费场景,多中心时通过 Cluster 将多个 Invoker 转换成一个 Invoker

-->通过注册中心消费,也是通过工厂模式, RegistryProtocol#refer 触发数据拉取、订阅和服务 Invoker 转换

1)根据用户执行的注册中心进行协议替换
2)创建具体注册中心实例
3)提取消费方 refer 中保存的元数据信息,进行合并
4)触发真正的服务订阅和 Invoker 转换-->
5)RegistryDirectory实现了 NotifyListener 接口,服务变更会触发这个类回调 notify 方法,重新引用服务
6)将消费方元数据注册到ZK,
7)然后处理 Provider,router 和动态配置订阅
8)通过 Cluster 将多个服务合并,也默认启用 FailoverCluster 策略进行服务调用重试

-->第一次订阅时会进行一次全量数据拉取,同事触发 RegistryDirectory#notify,调用 RegistryDirectory#toInvokers 进行 invoker 转换:

1)根据消费方 protocol 配置,过滤不匹配协议
2)合并 Provider 配置数据,比如服务端 IP 和 port
3)消除重复推送的服务列表,防止重复引用
4)使用具体的协议发起远程连接等操作

-->DubboProtocol#initClient 负责建立客户端连接和初始化 handler
1)使用 lazy 连接,在真实使用 RPC 时创建
2)或者立即发起远程 TCP 连接,默认 netty 传输
3)触发 HeaderExchanger#connect 滴啊用,用于心跳和解码的 handler,最终调用 Trasporters#connect 生成 netty 客户端进行处理

第二步:把 Invoker 通过动态代理转换成实现用户接口的动态代理引用

四、优雅停机原理解析

1、收到 kill 9 进程退出信号,spring 容器会触发容器销毁事件

2、provider 端会取消注册服务元数据信息

3、consumer 端会接收到最新的地址列表

4、Dubbo 协议会发送 readonly 事件报文通知 consumer 服务不可用

5、服务端等待已有任务结束并拒绝新任务

Dubbo负载均衡的实现

2、负载均衡的总体结构

Dubbo 内置了四种负载均衡算法:

  1. Random LoadBalance:按照权重设置随机概率做负载均衡的
    1)计算总权重并判断每个 Invoker 的权重是否一样
    2)如果每个 Invoker 权重相同,则说明每个概率都一样,直接用 nextInt 随机选择
    3)如果权重不同,则首先得到偏移值,根据偏移值找到对应的 Invoker

  2. RoundRobin:权重轮询,分为普通权重轮询和平滑权重轮询
    普通权重会导致某个节点突然被频繁选中,平滑权重会在轮询时穿插其他节点
    1)初始化权重缓存 Map,key=每个 Invoker 的 URL,value=weightedRoundRobin对象,这个对象封装了每个 Invoker 的权重
    2)遍历所有 Invoker,在遍历的过程中把买个 Invoker 的数据填充到 Map 中,预热后进行平滑轮询,每个 Invoker 会把权重加到自己的 current 属性上,并更行当前 Invoker 的 lastUpdate,同时累加每个 Invoker 的权重到 TotalWeight。遍历完后,选出所有 Invoker 中current 最大的作为最终要调用的节点
    3)清除已经没有使用的缓存节点,采用 CopyOnWrite 的方式更新 Map
    4)返回 Invoker,返回之前会把当前 Invoker 的 current 减去总权重

  3. LeastActive:最少活跃调用数负载均衡
    1)框架会记下每个 Invoker 的活跃数,每次只从活跃数最少的 Invoker 里选一个节点
    2)需要配合 ActiveLimitFilter 过滤器来计算每个接口方法的活跃数。是 Random 的加强版
    3)遍历所有 Invoker,不断寻找最小活跃数,如果有多个 Invoker 活跃数相同,则存到一个 map,再随机选一个
    4)ActiveLimitFilter 中,每进来一个请求,该方法的count+1,结束或抛异常 -1

  4. ConsistentHash:一致性 Hash 负载均衡
    Dubbo 框架使用了优化过的 Ketama 一致性 Hash,为每个真实的节点创建了多个虚拟节点,使分布均匀