Dubbo-服务导出导入


  • 服务导出

    • 接口级服务注册
    • 应用级服务注册
  • 服务引入

    • 接口级服务引入
    • 应用级服务引入
    • MigrationInvoker的生成
  • @EnableDubbo -> @DubboComponentScan -> DubboComponentScanRegistrar -> DubboSpringInitializer#initialize -> DubboDeployApplicationListener -> ContextRefreshedEvent -> DefaultModuleDeployer#start -> 服务导出与导入

  • 在做完服务导出与服务引入后,还会做几件非常重要的事情

    • 导出一个应用元数据服务(就是一个MetadataService服务,这个服务也会注册到注册中心),或者将应用元数据注册到元数据中心
    • 生成当前应用的实例信息对象ServiceInstance,比如应用名、实例ip、实例port,并将实例信息注册到注册中心,也就是应用级注册

服务导出

  • 只要扫描到了@DubboService,就会解析对应的类,得到服务相关的配置信息
    • 把这些配置信息封装成为一个ServiceConfig对象,并调用其export()进行服务导出
    • 此时一个ServiceConfig对象就表示一个Dubbo服务
  • 服务导出,主要就是完成三件事情
    • 确定服务的最终参数配置
    • 按不同协议启动对应的Server(服务暴露)
    • 将服务注册到注册中心(服务注册)
  • dubbo.application.register-mode
    • instance:表示只进行应用级注册
    • interface:表示只进行接口级注册
    • all:表示应用级注册和接口级注册都进行,默认

接口级服务注册

  • 当确定好了最终的服务配置后,Dubbo就会根据这些配置信息生成对应的服务URL
    • tri://192.168.65.221:20880/org.apache.dubbo.springboot.demo.DemoService?application=dubbo-springboot-demo-provider&timeout=3000
    • 服务消费者只要能获得到这个服务URL,就知道了关于这个Dubbo服务的全部信息,包括服务名、支持的协议、ip、port、各种配置
  • RegistryProtocol#export方法中,服务URL存在了Zookeeper的/dubbo/接口名/providers目录下
  • 如果利用动态配置功能修改了服务的参数,就要重新生成服务URL并重新注册到注册中心
  • key是接口名,value就是服务URL

应用级服务注册

  • 为了降低注册中心的压力,Dubbo3.0支持了应用级注册,同时也兼容接口级注册
  • 只记录应用所对应的实例信息(IP+绑定的端口),而不关心一个应用中到底有多少个Dubbo服务
    • 注册中心存储的数据变少了,注册中心中数据的变化频率变小了
    • 使得 Dubbo3 能实现与异构微服务体系如Spring Cloud等在地址发现层面更容易互通
  • 消费者根据接口名找到所对应的应用名
    • 在进行服务导出的过程中,会在Zookeeper中存一个映射关系,在服务导出的最后一步(ServiceConfig#exported)会保存这个映射关系:接口名:应用名
    • 这个映射关系存在Zookeeper的/dubbo/mapping目录下
  • 消费者需要从服务提供者的元数据服务获取服务的配置信息
    • 在应用启动过程中会进行服务导出和服务引入,然后就会暴露一个应用元数据服务
    • 这个应用元数据服务就是一个Dubbo服务(Dubbo框架内置的)
    • 消费者可以调用这个服务来获取某个应用中所提供的所有Dubbo服务以及服务配置信息
  • 根据所配置的注册中心生成两个URL
    • registry 代表接口级服务注册
    • service-discovery-registry 表应用级服务注册
  • ServiceDiscoveryRegistry在注册一个服务URL时(应用级服务注册)
    • 并不会往注册中心存数据,而只是把服务URL存到到一个MetadataInfo对象中
    • MetadataInfo对象保存了当前应用中所有Dubbo服务信息(服务名、支持的协议、绑定的端口、timeout等)
  • 在应用启动的最后,才会进行应用级注册
    • 应用的名字
    • 获取应用元数据的方式
    • 当前实例的ip和port
    • 当前实例支持哪些协议以及对应的port
  • 把实例信息存入注册中心的/services/应用名目录下
  • 如何确定实例的ip和端口:一个实例上可能支持多个协议以及多个端口
    • 获取MetadataInfo对象中保存的所有服务URL,优先取dubbo协议对应ip和port
    • 没有dubbo协议则所有服务URL中的第一个URL的ip和port
  • 一般一个协议一般只会对应一个端口
    • 如果对应了多个:最终存入endpoint中的会保证一个协议只对应一个端口,另外那个将被忽略
  • 元数据服务:dubbo.metadata.storage-type 默认 local
    • 如果为local,那就会启用应用元数据服务,最终服务消费者就会调用元数据服务获取到应用元数据信息
    • 如果为remote,那就不会暴露应用元数据服务,那么服务消费者从元数据中心获取应用元数据
      • 元数据中心可以用Zookeeper来实现,在暴露完元数据服务之后,在注册实例信息到注册中心之前,就会把MetadataInfo存入元数据中心
      • 记录了当前实例上提供了哪些服务以及对应的协议,注意并没有保存对应的端口
        • endpoint中记录了协议对应的端口
    • 元数据中心是集中式的,元数据服务式分散在各个提供者实例中的
  • 应用级服务注册的流程
    • 在导出某个Dubbo服务URL时,会把服务URL存入MetadataInfo中
    • 导出完某个Dubbo服务后,就会把服务接口名:应用名存入元数据中心(可以用Zookeeper实现)
    • 导出所有服务后,完成服务引入后
    • 判断要不要启动元数据服务,如果要就进行导出,固定使用Dubbo协议
    • 将MetadataInfo存入元数据中心
    • 确定当前实例信息(应用名、ip、port、endpoint)
    • 将实例信息存入注册中心,完成应用注册
  • 服务暴露:根据不同的协议启动不同的Server
    • 比如dubbo和tri协议启动的都是Netty
    • Dubbo2.7中的http协议启动的就是Tomcat

服务引入

  • 利用@DubboReference注解来引入某一个Dubbo服务
  • 应用在启动过程中,进行完服务导出之后,就会进行服务引入

接口级服务引入

  • 服务消费者会利用接口名从注册中心找到该服务接口所有的服务URL
  • 服务消费者会根据每个服务URL的protocol、ip、port生成对应的Invoker对象
    • 比如生成TripleInvoker、DubboInvoker等
    • 调用这些Invoker的invoke()方法就会发送数据到对应的ip、port
  • 生成好所有的Invoker对象之后,就会把这些Invoker对象进行封装并生成一个服务接口的代理对象
  • 代理对象调用某个方法时,会把所调用的方法信息生成一个Invocation对象
    • 并最终通过某一个Invoker的invoke()方法把Invocation对象发送出去
  • 所以代理对象中的Invoker对象是关键,服务引入最核心的就是要生成这些Invoker对象
  • Invoker是非常核心的一个概念,也有非常多种类
    • TripleInvoker:表示利用tri协议把Invocation对象发送出去
    • DubboInvoker:表示利用dubbo协议把Invocation对象发送出去
    • ClusterInvoker:有负载均衡功能
    • MigrationInvoker:迁移功能,Dubbo3.0新增的
  • 像TripleInvoker和DubboInvoker对应的就是具体服务提供者,包含了服务提供者的ip地址和端口
    • 会负责跟对应的ip和port建立Socket连接,后续就可以基于这个Socket连接并按协议格式发送Invocation对象
  • 接口级服务引入核心就是要找到当前所引入的服务有哪些服务URL,然后根据每个服务URL生成对应的Invoker
    • 根据当前引入的服务接口生成一个RegistryDirectory对象,表示动态服务目录
      • 用来查询并缓存服务提供者信息
    • RegistryDirectory对象会根据服务接口名去注册中心查找所有的服务URL
      • 比如Zookeeper中的/dubbo/服务接口名/providers/节点
    • 根据每个服务URL生成对应的Invoker对象,并把Invoker对象存在RegistryDirectory对象的invokers属性中
    • RegistryDirectory对象也会监听/dubbo/服务接口名/providers/节点的数据变化
      • 一旦发生了变化就要进行相应的改变
    • 最后将RegistryDirectory对象生成一个ClusterInvoker对象
      • 到时候调用ClusterInvoker对象的invoke()方法就会进行负载均衡选出某一个Invoker进行调用

应用级服务引入

  • Dubbo中找到应用的实例地址还远远不够
    • 因为直接使用的接口,所以在Dubbo中最终还是得找到服务接口有哪些服务提供者
  • 在进行应用级注册时是按照一个协议对应一个port存的
    • 但是接口级服务一个协议可以对应多个port
  • 服务消费者端寻找服务URL的逻辑更复杂了
    • 根据当前引入的服务接口生成一个ServiceDiscoveryRegistryDirectory对象,表示动态服务目录
      • 用来查询并缓存服务提供者信息
    • 根据接口名去获取/dubbo/mapping/服务接口名节点的内容,拿到的就是该接口所对应的应用名
    • 有了应用名之后,再去获取/services/应用名节点下的实例信息
    • 依次遍历每个实例,每个实例都有一个编号revision
      • 根据metadata-type进行判断
        • 如果是local:则调用实例上的元数据服务获取应用元数据(MetadataInfo)
        • 如果是remote:则根据应用名从元数据中心获取应用元数据(MetadataInfo)
      • 获取到应用元数据之后就进行缓存,key为revision,MetadataInfo对象为value
      • 为什么要去每个实例上获取应用的元数据信息?
        • 因为有可能不一样,虽然是同一个应用,但是在运行不同的实例的时候,可以指定不同的参数,比如不同的协议,不同的端口,虽然在生产上基本不会这么做,但是Dubbo还是支持了这种情况
    • 根据从所有实例上获取到的MetadataInfo以及endpoint信息,就能知道所有实例上所有的服务URL
      • 一个接口+一个协议+一个实例 : 对应一个服务URL
    • 拿到了这些服务URL之后,就根据当前引入服务的信息进行过滤
      • 会根据引入服务的接口名+协议名
      • 消费者可以在@DubboReference中指定协议,表示只使用这个协议调用当前服务
      • 如果没有指定协议,那么就会去获取tri、dubbo、rest这三个协议对应的服务URL
        • Dubbo3.0默认只支持这三个协议
    • 经过过滤之后,就得到了当前所引入的服务对应的服务URL了
    • 根据每个服务URL生成对应的Invoker对象
    • 并把Invoker对象存在ServiceDiscoveryRegistryDirectory对象的invokers属性中
    • 最后将ServiceDiscoveryRegistryDirectory对象生成一个ClusterInvoker对象
      • 到时候调用ClusterInvoker对象的invoke()方法就会进行负载均衡选出某一个Invoker进行调用

MigrationInvoker的生成

  • 接口级服务引入和应用级服务引入,最终都是得到某个服务对应的服务提供者Invoker
  • dubbo.application.service-discovery.migration
    • FORCE_INTERFACE,强制使用接口级服务引入
    • FORCE_APPLICATION,强制使用应用级服务引入
    • APPLICATION_FIRST,智能选择是接口级还是应用级,默认就是这个
  • 在进行某个服务的服务引入时
    • 会统一利用InterfaceCompatibleRegistryProtocol#refer来生成一个MigrationInvoker对象
  • MigrationInvoker中有三个属性
    • invoker 用来记录接口级 ClusterInvoker
    • serviceDiscoveryInvoker 用来记录应用级的 ClusterInvoker
    • currentAvailableInvoker 用来记录当前使用的 ClusterInvoker,要么是接口级,要么应用级
  • 一开始构造出来的MigrationInvoker对象中三个属性都为空
    • 接下来会利用MigrationRuleListener来处理MigrationInvoker对象,也就是给这三个属性赋值
  • 在MigrationRuleListener的构造方法中,会从配置中心读取DUBBO_SERVICEDISCOVERY_MIGRATION组下面的"当前应用名+.migration"的配置项,配置项为yml格式,对应的对象为MigrationRule,也就是可以配置具体的迁移规则,比如某个接口或某个应用的MigrationStep(FORCE_INTERFACE、APPLICATION_FIRST、FORCE_APPLICATION)
    • 如果没有配置迁移规则,则会看当前应用中是否配置了migration.step
      • 如果没有,那就从全局配置中心读取dubbo.application.service-discovery.migration来获取MigrationStep
      • 如果也没有配置,那MigrationStep默认为APPLICATION_FIRST
    • 如果没有配置迁移规则,则会看当前应用中是否配置了migration.threshold,如果没有配,则threshold默认为-1
      • 表示一个阈值,比如配置为2,表示应用级Invoker数量是接口级Invoker数量的两倍时才使用应用级Invoker,不然就使用接口级数量
dubbo:
 application:
   name: dubbo-springboot-demo-consumer 
   parameters:
     migration.step: FORCE_APPLICATION 
     migration.threshold: 2
  • 确定了step和threshold之后,就要真正开始给MigrationInvoker对象中的三个属性赋值了,先根据step调用不同的方法
switch (step) {
  case APPLICATION_FIRST:
    // 先进行接口级服务引入得到对应的ClusterInvoker,并赋值给invoker属性
    // 再进行应用级服务引入得到对应的ClusterInvoker,并赋值给serviceDiscoveryInvoker属性
    // 再根据两者的数量判断到底用哪个,并且把确定的ClusterInvoker赋值给 currentAvailableInvoker属性
    migrationInvoker.migrateToApplicationFirstInvoker(newRule); 
    break;
 case FORCE_APPLICATION:
    // 只进行应用级服务引入得到对应的ClusterInvoker
    // 并赋值给serviceDiscoveryInvoker和currentAvailableInvoker属性
    success = migrationInvoker.migrateToForceApplicationInvoker(newRule);
    break;
 case FORCE_INTERFACE: 
 default:
    // 只进行接口级服务引入得到对应的ClusterInvoker
    // 并赋值给invoker和 currentAvailableInvoker属性
    success = migrationInvoker.migrateToForceInterfaceInvoker(newRule); 
}
  • 得到了接口级ClusterInvoker和应用级ClusterInvoker之后,就会利用DefaultMigrationAddressComparator来进行判断
    • 如果应用级ClusterInvoker中没有具体的Invoker,那就表示只能用接口级Invoker
    • 如果接口级ClusterInvoker中没有具体的Invoker,那就表示只能用应用级Invoker
    • 如果应用级ClusterInvoker和接口级ClusterInvoker中都有具体的Invoker,则获取对应的Invoker个数
    • 如果在迁移规则和应用参数中都没有配置threshold,那就读取全局配置中心的dubbo.application.migration.threshold参数,如果也没有配置,则threshold默认为0(不是-1了)
    • 用应用级Invoker数量 / 接口级Invoker数量,得到的结果如果大于等于threshold,那就用应用级ClusterInvoker,否则用接口级ClusterInvoker
  • threshold默认为0,那就表示在既有应用级Invoker又有接口级Invoker的情况下,就一定会用应用级Invoker
    • 两个正数相除,结果肯定为正数
  • 当然你自己可以控制threshold,如果既有既有应用级Invoker又有接口级Invoker的情况下
    • 想在应用级Invoker的个数大于接口级Invoker的个数时采用应用级Invoker,那就可以把threshold设置为1,表示个数相等,或者个数相除之后的结果大于1时用应用级Invoker,否者用接口级Invoker
  • 这样MigrationInvoker对象中的三个数据就能确定好值了,和在最终的接口代理对象执行某个方法时,就会调用MigrationInvoker对象的invoke
    • 在这个invoke方法中会直接执行currentAvailableInvoker对应的invoker的invoker方法
    • 从而进入到了接口级ClusterInvoker或应用级ClusterInvoker中
    • 从而进行负载均衡,选择出具体的DubboInvoer或TripleInvoker
    • 完成真正的服务调用

导入导出流程图:
https://www.processon.com/view/link/62c441e80791293dccaebded


文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录