vlambda博客
学习文章列表

Dubbo 迈出云原生重要一步 - 应用级服务发现解析


作者 | 刘军(陆龟)  Apache Dubbo PMC


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

概述


社区版本 Dubbo 从 2.7.5 版本开始,新引入了一种基于实例(应用)粒度的服务发现机制,这是我们为 Dubbo 适配云原生基础设施的一步重要探索。版本发布到现在已有近半年时间,经过这段时间的探索与总结,我们对这套机制的可行性与稳定性有了更全面、深入的认识;同时在 Dubbo 3.0 的规划也在全面进行中,如何让应用级服务发现成为未来下一代服务框架 Dubbo 3.0 的基础服务模型,解决云原生、规模化微服务集群扩容与可伸缩性问题,也已经成为我们当前工作的重点。


既然这套新机制如此重要,那它到底是怎么工作的呢?今天我们就来详细解读一下。在最开始的社区版本,我们给这个机制取了一个神秘的名字 - 服务自省,下文将进一步解释这个名字的由来,并引用服务自省代指这套应用级服务发现机制。


熟悉 Dubbo 开发者应该都知道,一直以来都是面向 RPC 方法去定义服务的,并且这也是 Dubbo 开发友好性、治理功能强的基础。既然如此,那我们为什么还要定义个应用粒度的服务发现机制呢?这个机制到底是怎么工作的?它与当前机制的区别是什么?它能给我们带来哪些好处那?对适配云原生、性能提升又有哪些帮助?


带着所有的这些问题,我们开始本文的讲解。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

服务自省是什么?


首先,我们先来解释文章开篇提到的问题:


  1. 应用粒度服务发现是到底是一种怎样的模型,它与当前的 Dubbo 服务发现模型的区别是什么?

  2. 我们为什么叫它服务自省?



Dubbo 迈出云原生重要一步 - 应用级服务发现解析



"RPC Service1": [
  {"name":"instance1""ip":"127.0.0.1""metadata":{"timeout":1000}},
  {"name":"instance2""ip":"127.0.0.1""metadata":{"timeout":2000}},
  {"name":"instance3""ip":"127.0.0.1""metadata":{"timeout":3000}},
]
"RPC Service2": [Instance list of RPC Service2],
"RPC ServiceN": [Instance list of RPC ServiceN]


而我们新引入的“应用粒度的服务发现”,它以应用名(Application)作为 key,以这个应用部署的一组实例(Instance)列表作为 value。这带来两点不同:


  1. 数据映射关系变了,从 RPC Service -> Instance 变为 Application -> Instance;

  2. 数据变少了,注册中心没有了 RPC Service 及其相关配置信息。


"application1": [
  {"name":"instance1""ip":"127.0.0.1""metadata":{}},
  {"name":"instance2""ip":"127.0.0.1""metadata":{}},
  {"name":"instanceN""ip":"127.0.0.1""metadata":{}}
]


要进一步理解新模型带来的变化,我们看一下应用与 RPC 服务间的关系,显而易见的,1 个应用内可能会定义 n 个 RPC Service。因此 Dubbo 之前的服务发现粒度更细,在注册中心产生的数据条目也会更多(与 RPC 服务成正比),同时也存在一定的数据冗余。


简单理解了应用级服务发现的基本机制,接着解释它为什么会被叫做“服务自省”?


其实这还是得从它的工作原理说起,上面我们提到,应用粒度服务发现的数据模型有几个以下明显变化:数据中心的数据量少了,RPC 服务相关的数据在注册中心没有了,现在只有 application - instance 这两个层级的数据。为了保证这部分缺少的 RPC 服务数据仍然能被 Consumer 端正确的感知,我们在 Consumer 和 Provider 间建立了一条单独的通信通道:Consumer 和 Provider 两两之间通过特定端口交换信息,我们把这种 Provider 自己主动暴露自身信息的行为认为是一种内省机制,因此从这个角度出发,我们把整个机制命名为:服务自省。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

为什么需要服务自省?


上面讲服务自省的大概原理的时候也提到了它给注册中心带来的几点不同,这几点不同体现在 Dubbo 框架侧(甚至整个微服务体系中),有以下优势:


  • 与业界主流微服务模型对齐,比如 SpringCloud、Kubernetes Native Service 等;


1. 对齐主流微服务模型



  • RPC 方法定义,服务消费方需要知道 RPC 服务的具体定义,不论服务类型是 rest 或 rmi 等。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析


对于 RPC 实例间借助注册中心的数据同步,REST 定义了一套非常有意思的成熟度模型,感兴趣的朋友可以参考这里的链接 :

https://www.martinfowler.com/articles/richardsonMaturityModel.html

按照文章中的 4 级成熟度定义,Dubbo 当前基于接口粒度的模型可以对应到 L4 级别。



2. Spring Cloud



RPC 服务这部分信息目前都是通过线下约定或离线的管理系统来协商的。这种架构的优缺点总结如下:



Dubbo 迈出云原生重要一步 - 应用级服务发现解析


3. Dubbo



Dubbo 迈出云原生重要一步 - 应用级服务发现解析


4. Dubbo + Kubernetes


Dubbo 要支持 Kubernetes native service,相比之前自建注册中心的服务发现体系来说,在工作机制上主要有两点变化:


  • 服务注册由平台接管,provider 不再需要关心服务注册;


Kubernetes Service 作为一个抽象概念,怎么映射到 Dubbo 是一个值得讨论的点

Service Name - > Application Name,Dubbo 应用和 Kubernetes 服务一一对应,对于微服务运维和建设环节透明,与开发阶段解耦。


apiVersion: v1
kind: Service
metadata:
  name: provider-app-name
spec:
  selector:
    app: provider-app-name
  ports:
    - protocol: TCP
      port:
      targetPort: 9376


Service Name - > Dubbo RPC Service,Kubernetes 要维护调度的服务与应用内建 RPC 服务绑定,维护的服务数量变多。


---
apiVersion: v1
kind: Service
metadata:
  name: rpc-service-1
spec:
  selector:
    app: provider-app-name
  ports: ##
...
---
apiVersion: v1
kind: Service
metadata:
  name: rpc-service-2
spec:
  selector:
    app: provider-app-name
  ports: ##
...
---
apiVersion: v1
kind: Service
metadata:
  name: rpc-service-N
spec:
  selector:
    app: provider-app-name
  ports: ##
...


Dubbo 迈出云原生重要一步 - 应用级服务发现解析



即 REST 成熟度模型中的 L3 级别。


对比起来 Dubbo 则相对是比较特殊的存在,更多的是从 RPC 服务的粒度去设计的。


对应 REST 成熟度模型中的 L4 级别。


如我们上面针对每种模型做了详细的分析,每种模型都有其优势和不足。而我们最初决定 Dubbo 要做出改变,往其他的微服务发现模型上的对齐,是我们最早在确定 Dubbo 的云原生方案时,我们发现要让 Dubbo 去支持 Kubernetes Native Service,模型对齐是一个基础条件;另一点是来自用户侧对 Dubbo 场景化的一些工程实践的需求,得益于 Dubbo 对多注册、多协议能力的支持,使得 Dubbo 联通不同的微服务体系成为可能,而服务发现模型的不一致成为其中的一个障碍,这部分的场景描述请参见


5. 更大规模的微服务集群 - 解决性能瓶颈


这部分涉及到和注册中心、配置中心的交互,关于不同模型下注册中心数据的变化,之前原理部分我们简单分析过。为更直观的对比服务模型变更带来的推送效率提升,我们来通过一个示例看一下不同模型注册中心的对比:


Dubbo 迈出云原生重要一步 - 应用级服务发现解析



  • 对于 Spring Cloud 和 Kubernetes 模型,注册中心只会存储一条 DEMO - 10.210.134.30+metadata 的数据;

  • 对于老的 Dubbo 模型,注册中心存储了三条接口粒度的数据,分别对应三个接口 DemoService 1 2 3,并且很多的址数据都是重复的。



而对于基于接口粒度的模型,数据量是和接口数量正相关的,鉴于一个应用通常发布多个接口的现状,这个数量级本身比应用粒度是要乘以倍数的;另外一个关键点在于,接口粒度导致的集群规模评估的不透明,相对于实i例、应用增长都通常是在运维侧的规划之中,接口的定义更多的是业务侧的内部行为,往往可以绕过评估给集群带来压力。




Dubbo 迈出云原生重要一步 - 应用级服务发现解析

工作原理


1. 设计原则


上面一节我们从服务模型及支撑大规模集群的角度分别给出了 Dubbo 往应用级服务发现靠拢的好处或原因,但这么做的同时接口粒度的服务治理能力还是要继续保留,这是 Dubbo 框架编程模型易用性、服务治理能力优势的基础。


以下是我认为我们做服务模型迁移仍要坚持的设计原则:


  • 新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移,即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理,做到对用户侧完全无感知;

  • 建立 Consumer 与 Provider 间的自动化 RPC 服务元数据协调机制,解决传统微服务模型无法同步 RPC 级接口配置的缺点。


2. 基本原理详解



这里主要的不同有以下两点:


  • 注册中心数据以“应用 - 实例列表”格式组织,不再包含 RPC 服务信息;


Dubbo 迈出云原生重要一步 - 应用级服务发现解析


以下是每个 Instance metadata 的示例数据,总的原则是 metadata 只包含当前 instance 节点相关的信息,不涉及 RPC 服务粒度的信息。



{
  "name""provider-app-name",
  "id""192.168.0.102:20880",
  "address""192.168.0.102",
  "port"20880,
  "sslPort"null,
  "payload": {
    "id"null,
    "name""provider-app-name",
    "metadata": {
      "metadataService""{\"dubbo\":{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"2.7.5\",\"port\":\"20881\"}}",
      "endpoints""[{\"port\":20880,\"protocol\":\"dubbo\"}]",
      "storage-type""local",
      "revision""6785535733750099598",
    }
  },
  "registrationTimeUTC"1583461240877,
  "serviceType""DYNAMIC",
  "uriSpec"null
}


  • Client – Server 自行协商 RPC 方法信息。


在注册中心不再同步 RPC 服务信息后,服务自省在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制,这也是“服务自省”这个名字的由来。服务端实例会暴露一个预定义的 MetadataService RPC 服务,消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析


当前 MetadataService 返回的数据格式如下:


[
  "dubbo://192.168.0.102:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider&timestamp=1583469714314", 
 "
dubbo://192.168.0.102:20880/org.apache.dubbo.demo.HelloService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider&timestamp=1583469714314",
  "
dubbo://192.168.0.102:20880/org.apache.dubbo.demo.WorldService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider&timestamp=1583469714314"
]


熟悉 Dubbo 基于 RPC 服务粒度的服务发现模型的开发者应该能看出来,服务自省机制机制将以前注册中心传递的 URL 一拆为二:


  • 一部分和实例相关的数据继续保留在注册中心,如 ip、port、机器标识等;

  • 另一部分和 RPC 方法相关的数据从注册中心移除,转而通过 MetadataService 暴露给消费端。


理想情况下是能达到数据按照实例、RPC 服务严格区分开来,但明显可以看到以上实现版本还存在一些数据冗余,有些也数据还未合理划分。尤其是 MetadataService 部分,其返回的数据还只是简单的 URL 列表组装,这些 URL其实是包含了全量的数据。


以下是服务自省的一个完整工作流程图,详细描述了服务注册、服务发现、MetadataService、RPC 调用间的协作流程。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析


  1. 服务提供者启动,首先解析应用定义的“普通服务”并依次注册为 RPC 服务,紧接着注册内建的 MetadataService 服务,最后打开 TCP 监听端口;

  2. 启动完成后,将实例信息注册到注册中心(仅限 ip、port 等实例相关数据),提供者启动完成;

  3. 至此,消费者可以接收外部流量,并对提供者发起 Dubbo RPC 调用。


在以上流程中,我们只考虑了一切顺利的情况,但在更详细的设计或编码实现中,我们还需要严格约定一些异常场景下的框架行为。比如,如果消费者 MetadataService 调用失败,则在重试知道成功之前,消费者将不可以接收外部流量。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

服务自省中的关键机制


1. 元数据同步机制



  • 内建 MetadataService:MetadataService 通过标准的 Dubbo 协议暴露,根据查询条件,会将内存中符合条件的“普通服务”配置返回给消费者。这一步发生在消费端选址和调用前;

 

  • 元数据中心:复用 2.7 版本中引入的元数据中心,provider 实例启动后,会尝试将内部的 RPC 服务组织成元数据的格式到元数据中心,而 consumer 则在每次收到注册中心推送更新后,主动查询元数据中心。



Dubbo 迈出云原生重要一步 - 应用级服务发现解析


2. RPC 服务 < - > 应用映射关系



老的 Consumer 开发与配置示例:


<!-- 框架直接通过 RPC Service 1/2/N 去注册中心查询或订阅地址列表 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:reference interface="RPC Service 1" />
<dubbo:reference interface="RPC Service 2" />
<dubbo:reference interface="RPC Service N" />


新的 Consumer 开发与配置示例:


<!-- 框架需要通过额外的 provided-by="provider-app-x" 才能在注册中心查询或订阅到地址列表 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181?registry-type=service"/>
<dubbo:reference interface="RPC Service 1" provided-by="provider-app-x"/>
<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />
<dubbo:reference interface="RPC Service N" provided-by="provider-app-y" />


以上指定 provider 应用名的方式是 Spring Cloud 当前的做法,需要 consumer 端的开发者显示指定其要消费的 provider 应用。


以上问题的根源在于注册中心不知道任何 RPC 服务相关的信息,因此只能通过应用名来查询。


为了使整个开发流程对老的 Dubbo 用户更透明,同时避免指定 provider 对可扩展性带来的影响(参见下方说明),我们设计了一套 RPC 服务到应用名的映射关系,以尝试在 consumer 自动完成 RPC 服务到 provider 应用名的转换。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析


Dubbo 之所以选择建立一套“接口-应用”的映射关系,主要是考虑到 service - app 映射关系的不确定性。一个典型的场景即是应用/服务拆分,如上面提到的配置 <dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />,PC Service 2 是定义于 provider-app-x 中的一个服务,未来它随时可能会被开发者分拆到另外一个新的应用如 provider-app-x-1 中,这个拆分要被所有的 PC Service 2 消费方感知到,并对应用进行修改升级,如改为 <dubbo:reference interface="RPC Service 2" provided-by="provider-app-x-1" />,这样的升级成本不可否认还是挺高的。


到底是 Dubbo 框架帮助开发者透明的解决这个问题,还是交由开发者自己去解决,当然这只是个策略选择问题,并且 Dubbo 2.7.5+ 版本目前是都提供了的。其实我个人更倾向于交由业务开发者通过组织上的约束来做,这样也可进一步降低 Dubbo 框架的复杂度,提升运行态的稳定性。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

总结与展望




作者简介


刘军,Github 账号 Chickenlj,Apache Dubbo PMC,项目核心开发,见证了Dubbo从重启开源到Apache毕业的整个流程。现任职阿里云云原生应用平台团队,参与服务框架、微服务相关工作,目前主要在推动 Dubbo 3.0 - Dubbo 云原生。


Dubbo 迈出云原生重要一步 - 应用级服务发现解析

活动推荐


6 月 12 日 - 13 日
阿里巴巴内部研发 效能峰会首次对外直播!
7 大论坛,35 个议题,1300 分钟技术干货!
39 位技术大咖,4 万阿里工程师,邀你共享研发效能盛宴!



戳原文,立即参会!