服务注册与发现模型演进过程

Dubbo-go 系列文章(二)

Table of Contents

微服务架构包含了众多的关键性的基础组件,注册中心则是所有基础组件中最为核心的之一,它负责着给“消费者”提供系统中可用的“服务提供者”信息,包括但不限于“服务提供者”的地址信息、元数据信息(metadata)等。在微服务架构日新月异的今天,注册中心也正在一代又一代的快速更新着,本篇文章会向大家介绍注册中心是如何从无到有的,在新一代的架构上注册中心又肩负着什么样的责任。

为什么要服务注册与发现?

服务注册与发现本质上就是命名服务,这种模式其实时刻发生在我们身边。比如我们想要出去吃饭,即我们有吃饭的需求,可以把“我们”理解为服务的消费者,而餐厅则是服务的提供者,在正常思路中我们首先会使用手机中的app搜索附近的餐厅,我们关心的数据不外乎餐厅的地址、评分、口味等等。事实上,手机app就是充当了注册中心的作用,提供者通过注册中心把自己的信息公布出来,当消费者在有需求的时候会从注册中心中搜索符合要求的提供者。试想,如果没有注册中心,消费者只能通过轮训的方式向每个提供者询问,在大规模的集群中这样的做法的代价将是无法承受的。

服务注册与发现模型演进过程

任何事物都不是一蹴而就的,当然服务注册与发现的模型也是随着时间不断改进的。每种方式的出现都有着自身独特的优势,但同时也有其自身的局限,这意味着并不是最新的模型就是最好的。通过介绍模型的演变过程,希望你能从中感受到每种方式的特点,再根据实际情况去选用适合自己的服务注册与发现模型。

远古时代:约定俗成

Golang内置了net/rpc包向使用者提供了最简单RPC服务,事实上这种方式我们不妨称为“远古模式”。在这种方式下,必要的信息需要被事先约定,这些信息包括但不限于IP地址、端口、暴露的服务名称、服务提供的方法、方法接收的参数以及返回值等。既然提到了net/rpc包,下面通过一个简单的例子来体验一下这种模式(注:为了保持简洁以下代码均不对错误进行处理)。

远古模式交换模型

假设我们有一个HelloService,它的Hello方法接收一个字符串,在字符串前加入"hello:"后返回。

启动服务端时我们需要先注册HelloService服务,然后根据提供的IP和端口信息监听连接,这与HTTP服务的启动方式非常相似。

客户端则是根据“事先约定”好的信息发起RPC调用,在这个例子中,这些约定好的信息包括:使用TCP通讯协议、服务端的IP地址是localhost、使用的端口是1234、暴露的服务有HelloService、Hello方法包含在HelloService以及调用时所必须的参数类型和参数结构。

相信你已经从上面的实例中感受到了,这种最原始的方式最大的优势在于简单,但是最大的不足也是过于简单。试想当你有100个不同IP的服务器提供不同的服务时,如何从容的记住它们的信息恐怕就变成了最大的困境。RPC的服务注册与发现模型开始向下一代演变,在借鉴DNS的运行方式后,人们给不同的服务器IP地址一个好记的名字(域名),这就大大的减轻了微服务之间的配置和调用难度。

古代:DNS域名解析

前文提到过,服务注册与发现的本质就是命名服务,所以自然而然就会想到DNS域名解析系统。DNS协议是一个使用非常广泛的协议,几乎被用于任何一个智能设备上,所以它具有天然的跨语言特性。服务在启动时,将自己的关键信息主动向DNS服务器暴露。

那么可供使用的DNS记录类型(DNS Record Type)包括:A记录(A Record)、SRV记录(SRV Record)和TXT记录(TXT Record),相比较下,SRV记录和TXT记录更适合于服务注册于发现。A记录只能记录服务的IP,端口则需要像远古方式一样事先约定。SRV记录可以记录服务的IP、端口、优先级和权重的信息,etcd采用的是这种方式。TXT记录存储的是文本文件,所以存在着更大的定制空间,Eureka采用了这种形式。

虽然基于DNS域名解析的服务注册于发现具有极佳的便捷性和通用性,但是也存在着很多不可忽视的弊端。首先,除了TXT记录外其他的记录形式过于死板,无法记录额外关于服务的元数据信息。其次,DNS服务缺少推送机制,所以要求客户端需要通过轮训的方式获取服务变更消息。最后,也是非常重要的一点,DNS缓存会使注册数据更新不及时而导致数据不一致的问题,对于正常的域名解析情况来说DNS缓存的引入是正向的,因为IP地址与域名的对应关系是基本稳定的,而在微服务架构中,服务的上线下线是一个相对更频繁发生的事情,此时缓存和没有推送订阅机制就会导致数据更新不及时的问题就会被不断放大。

近代:接口级服务发现与注册模型

在详细描述接口级服务发现与注册模型之前,首先需要统一几个术语:

  • 接口与服务是相同的概念,可以互换;
  • 应用是指一个独立的进程,可以通过虚拟化等技术在一个服务器中部署多个应用;
  • 应用与接口的数量关系是1对N,比如一个登陆程序既可以提供登陆服务,也可以提供修改密码服务。

为了解决DNS的无法主动推送以及数据不一致的弊端,注册中心作为RPC框架的重要组件登上了历史舞台。注册中心本质上是一个分布式的数据库,记录了当前系统中可用的服务,包括服务的必要定位信息和元数据信息,其中必要定位信息是指IP、端口、接口名等发起RPC调用所必须的信息,元数据信息是指一些额外的、辅助性的数据,如服务版本、超时时间等,以键值对的形式保存在数据中心中。

注册中心架构图

从架构层面来看,除了消费者和生产者外,注册中心作为新的组成部分被加入到系统中。系统的运作过程如上图所示,被划分为四个步骤:

  1. 生产者对外暴露服务,即将定位信息和元数据信息注册到注册中心中;
  2. 消费者订阅注册中心的服务信息;
  3. 在生产者服务上下线后会更新注册中心的信息,此时注册中心将变化信息主动通知消费者;
  4. 消费者根据注册信息向生产者发起调用。

在引入注册中心之后,基于DNS的模型弊端都被解决了,但是与此同时又引入了两个新的问题:

  1. 选择什么中间件作为注册中心?
  2. 向注册中心注册的信息格式是什么?

关于第一个问题,当前有多种中间件可以选择,本文将在稍后位置另起单独段落详细对比各方案,现在我们将更多的聚焦于第二个问题。在当前阶段,大多国内的RPC框架(Dubbo 2.x)使用的是接口级注册模型。一个应用通常会提供多个接口,在接口级注册模型中,所有接口将单独作为一个RPC服务被注册到注册中心中,比如一个应用App1,提供了服务Service1、Service2两个服务,应用App2也同样提供了Service1、Service2两个服务,那么在注册中心中则会有四行注册信息。

interfaceregistry-keJtSp.png)

接口级注册模型的特点是注册粒度细,优点是简单直观,但是带来的问题是当应用提供的服务越来越多时需要向注册中心的写入更多条信息。假设一个服务提供30个服务,那么一个应用上下线就需要在注册中心中添加或删除30行,那么在大规模集群的场景中,有N个应用同时上下线时影响的数据行数将多达30*N条,这直接会导致注册中心单点故障问题,在生产环境中是不可接受的。

现代:应用级服务发现与注册模型

观察接口级服务发现与注册模型的注册信息时,我们不难发现对于统一应用提供的接口一般具有相同的IP和端口,元数据也大体上的保持一致,所以在接口级模型中大量的信息是冗余的,自然而然的我们希望把应用内相同的信息只保留一份,应用级服务发现与注册模型逐渐成为主流。在已有的主流微服务框架中(如Spring Cloud等)也都是采用应用级模型,在Dubbo-go 3.0中也提供了对应用级服务发现与注册的支持。

applicationregistry-HvpFoM

上图介绍了在使用应用级模型时注册中心的状态,无论一个应用有多少个服务,在注册中心中的信息数量永远是当前系统中应用的数量,在极大规模的集群中可以很好的减轻注册中心的压力。细心的读者可能会疑惑,其他的信息不存放在注册中心,那应该存放在哪呢?这个问题不同的框架可能实现的方式不同,一般来说需要消费者与生产者自行协商数据的获取方式,在Dubbo-go 3.0利用“服务自省”的模型实现剩余消息的互通,这部分将在稍后章节详细说明。

综上,应用级服务发现与注册模型通过消除冗余接口信息达到了减轻注册中心压力的目的,同时主流的微服务中间件也都采用了应用级模型,因此Dubbo-go 3.0在支持应用级模型后将具有更好的性能和兼容性。但是应用级模型也并不是完美无缺的,相比于接口级模型也带来了服务不易拆分、灵活性降低的问题。因此在选择接口级模型还是应用级模型上时,更应该具体情况具体分析选择一个最适合的模型。

All rights reserved
Except where otherwise noted, content on this page is copyrighted.