Dawn's Blogs

分享技术 记录成长

0%

Triple协议 (7) 应用级别的服务发现

在微服务架构中,如何感知后端服务实例的动态上下线,就是服务发现(Service Discovery)。业界比较有代表性的微服务框架如 SpringCloud、Dubbo 等都抽象了强大的动态地址发现能力,并且为了满足微服务业务场景的需求,绝大多数框架的地址发现都是基于自己设计的一套机制来实现。在 SpringCloud 中使用 Eureka 作为注册中心,Dubbo 通常采用 zookeeper 和 nacos 作为注册中心。注册中心不仅仅记录了 IP+Port,还包括微服务的元信息,如序列化类型,实例方法列表,各个方法级的定制化配置等。

服务发现包含三种角色:服务提供者(Provider)、服务消费者(Consumer)和注册中心(Registry)。不同框架之间的区别在于如何组织注册中心中的数据。

dubbo中应用,服务和实例的概念区分:

应用是一个独立的逻辑单元,一个应用可以包含多个服务,每个服务可以包含多个实例

业界服务发现方式

Spring Cloud

Spring Cloud 通过注册中心只同步了应用与实例地址,消费方可以基于实例地址与服务提供方建立链接,但是消费方对于如何发起 HTTP 调用(SpringCloud 基于 rest 通信)一无所知,比如对方有哪些 HTTP endpoint,需要传入哪些参数等。

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

  • 优势: 部署结构清晰、地址推送量小。
  • 缺点: 地址订阅需要指定应用名, provider 应用变更(拆分)需消费端感知;RPC 调用无法全自动同步。

img

Dubbo

Dubbo 通过注册中心同时同步了实例地址和 RPC 方法,因此其能实现 RPC 过程的自动同步,面向 RPC 编程、面向 RPC 治理,对后端应用的拆分消费端无感知,其缺点则是地址推送数量变大,和 RPC 方法成正比。

img

Dubbo + Kubernetes

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

  1. Service Name - > Application Name:Dubbo 应用和 Kubernetes 服务一一对应,对于微服务运维和建设环节透明,与开发阶段解耦(对应于应用服务发现)。
  2. Service Name - > Dubbo RPC Service:维护的 Service 数量变多,一个 Dubbo 应用可以运行多个接口,即一个 Dubbo 应用可以创建多个 Kubernetes Service(如 dubbo-app-service-1,dubbo-app- service-2 等)。

Dubbo 3 服务发现

服务自省

是什么?

以 Dubbo 之前的地址发现数据格式为例,它是“RPC 服务粒度”的,它是以 RPC 服务作为 key,以实例列表作为 value 来组织数据的:

1
2
3
4
5
6
7
8
"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]

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

  1. 数据映射关系变了,从 RPC Service -> Instance 变为 Application -> Instance。
  2. 数据变少了,注册中心没有了 RPC Service 及其相关配置信息。
1
2
3
4
5
"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":{}}
]

Dubbo 之前的服务发现粒度更细,在注册中心产生的数据条目也会更多(与 RPC 服务成正比),同时也存在一定的数据冗余

接着解释它为什么会被叫做“服务自省”?其实这还是得从它的工作原理说起,上面提到,应用粒度服务发现的数据模型有几个以下明显变化:数据中心的数据量少了,RPC 服务相关的数据在注册中心没有了,现在只有 application - instance 这两个层级的数据。

为了保证这部分缺少的 RPC 服务数据仍然能被 Consumer 端正确的感知,我们在 Consumer 和 Provider 间建立了一条单独的通信通道:Consumer 和 Provider 两两之间通过特定端口交换信息,我们把这种 Provider 自己主动暴露自身信息的行为认为是一种内省机制,因此整个机制命名为:服务自省。

为什么需要?

为什么需要服务自省,这会带来以下优势:

  1. 与业界主流微服务模型对齐,比如 SpringCloud、Kubernetes Service 等。
  2. 提升性能与可伸缩性
    • 注册中心的数据减少了,大幅度的减轻注册中心的存储、推送压力,进而减少 Dubbo Consumer 侧的地址计算压力。
    • 服务发现的数据规模,以及集群规模也开始变得可预测、可评估(与 RPC 接口数量无关,只与实例部署规模相关)。

根据统计,平均情况下 Consumer 订阅的 3 个接口来自同一个 Provider 应用,如此计算下来,如果以应用粒度为地址通知和选址基本单位,则平均地址推送和计算量将下降 60% 还要多。而在极端情况下,也就是当 Consumer 端消费的接口更多的来自同一个应用时,这个地址推送与内存消耗的占用将会进一步得到降低,甚至可以超过 80% 以上。

典型的例子是 API 网关,可能一个 API 网关有几十个甚至上百个定义的服务。

工作原理

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

  • 服务提供者启动,首先解析应用定义的“普通服务”并依次注册为 RPC 服务,紧接着注册内建的 MetadataService 服务,最后打开 TCP 监听端口。
  • 启动完成后,将实例信息注册到注册中心(仅限 ip、port 等实例相关数据),提供者启动完成。
  • 服务消费者启动,首先依据其要“消费的 provider 应用名”到注册中心查询地址列表,并完成订阅(以实现后续地址变更自动通知)。
  • 消费端拿到地址列表后,紧接着对 MetadataService 发起调用,返回结果中包含了所有应用定义的“普通服务”及其相关配置信息
  • 至此,消费者可以接收外部流量,并对提供者发起 Dubbo RPC 调用

img

设计原则

接口级服务发现的好处是数据量小,但是接口粒度的服务治理能力还是要继续保留。这就需要两点设计原则:

  1. 新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移,即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理,做到对用户侧完全无感知。
  2. 建立 Consumer 与 Provider 间的自动化 RPC 服务元数据协调机制,解决传统微服务模型无法同步 RPC 级接口配置的缺点。

基本原理

  1. 注册中心的数据组织方式:以应用名为 key,实例列表为 value 组织数据。元数据只包含实例级别的元数据,不包含接口级别的元数据。注册中心的一个实例条目如下,仅仅包含实例地址,端口,实例级别的元数据信息等基本信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"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
}
  1. 服务调用者和提供者自行协商 RPC 方法信息。在注册中心不再同步 RPC 服务信息后,服务自省在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制。服务端实例会暴露一个预定义的 MetadataService RPC 服务,消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。当前 MetadataService 返回的数据格式如下:
1
2
3
4
5
[
"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"
]

关键机制

元数据同步机制

Client 与 Server 间在收到地址推送后的配置同步是服务自省的关键环节,目前针对元数据同步有两种具体的可选方案,分别是:

  • 内建的 MetadataService 服务
  • 独立的元数据中心,通过中心化的元数据集群协调数据。Provider 实例启动后,会尝试将内部的 RPC 服务组织成元数据的格式到元数据中心,而 consumer 则在每次收到注册中心推送更新后,主动查询元数据中心。

img

RPC 服务与应用之间的映射关系

从服务粒度到应用粒度,要想用户完全无感知必须知道 RPC 服务与应用名之间的映射关系。所以为了使整个开发流程对老的 Dubbo 用户更透明,同时避免指定 provider 对可扩展性带来的影响,设计了一套 RPC 服务到应用名的映射关系,以尝试在 consumer 自动完成 RPC 服务到 provider 应用名的转换。

  • consumer 通过配置中心以服务名查询应用名
  • consumer 得到应用名,就可以检索注册中心,进行应用级别的服务发现了。

img