【微服务】(七)—— Nacos注册中心

目录

一、Nacos 注册中心的设计原理

1、数据模型

2、数据⼀致性

3、负载均衡

4、健康检查

二、Nacos 注册中心服务数据模型

1、服务(Service)和服务实例(Instance)

1)定义服务

 3)定义实例

4)实例元数据

5)持久化属性

2、集群(Cluster)

1、定义集群

3、生命周期

1)服务的⽣命周期

2)实例的⽣命周期

3)集群的⽣命周期

4)元数据的⽣命周期


一、Nacos 注册中心的设计原理

1、数据模型

        注册中心的核心数据是服务的名字和它对应的网络地址,当服务注册了多个实例时,我们需要对不健康的实例进行过滤或者针对实例的⼀些特征进行流量的分配,那么就需要在实例上存储⼀些例如健康状态、权重等属性。随着服务规模的扩大,渐渐的又需要在整个服务级别设定⼀些权限规则、以及对所有实例都生效的⼀些开关,于是在服务级别又会设立⼀些属性。再往后,我们又发现单个服务的实例又会有划分为多个子集的需求,例如⼀个服务是多机房部署的,那么可能需要对每个机房的实例做不同的配置,这样又需要在服务和实例之间再设定⼀个数据级别。
        Zookeeper 没有针对服务发现设计数据模型,它的数据是以⼀种更加抽象的树形 K-V 组织的,因此理论上可以存储任何语义的数据。而 Eureka 或者 Consul 都是做到了实例级别的数据扩展,这可以满足大部分的场景,不过无法满足大规模和多环境的服务数据存储。Nacos 在经过内部多年生产经验后提炼出的数据模型,则是⼀种服务-集群-实例的三层模型。如上文所说,这样基本可以满足服务在所有场景下的数据存储和管理。

        Nacos 的数据模型虽然相对复杂,但是它并不强制你使用它里面的所有数据,在大多数场景下,你可以选择忽略这些数据属性,此时可以降维成和 Eureka 和 Consul ⼀样的数据模型。另外⼀个需要考虑的是数据的隔离模型,作为⼀个共享服务型的组件,需要能够在多个用户或者业务方使用的情况下,保证数据的隔离和安全,这在稍微大⼀点的业务场景中非常常见。另⼀方面服务注册中心往往会支持云上部署,此时就要求服务注册中心的数据模型能够适配云上的通用模型。
        Zookeeper、Consul 和 Eureka 在开源层面都没有很明确的针对服务隔离的模型,Nacos 则在⼀开始就考虑到如何让用户能够以多种维度进行数据隔离,同时能够平滑的迁移到阿里云上对应的商业化产品。

        Nacos 提供了四层的数据逻辑隔离模型,用户账号对应的可能是⼀个企业或者独立的个体,这个数据⼀般情况下不会透传到服务注册中心。⼀个用户账号可以新建多个命名空间,每个命名空间对应⼀个客户端实例,这个命名空间对应的注册中心物理集群是可以根据规则进行路由的,这样可以让注册中心内部的升级和迁移对用户是无感知的,同时可以根据用户的级别,为用户提供不同服务级别的物理集群。再往下是服务分组和服务名组成的二维服务标识,可以满足接口级别的服务隔离。
        Nacos 1.0.0 介绍的另外⼀个新特性是:临时实例和持久化实例。在定义上区分临时实例和持久化实例的关键是健康检查的方式。临时实例使用客户端上报模式,而持久化实例使用服务端反向探测模式。临时实例需要能够自动摘除不健康实例,而且无需持久化存储实例,那么这种实例就适用于类 Gossip 的协议。右边的持久化实例使用服务端探测的健康检查方式,因为客户端不会上报心跳, 那么自然就不能去自动摘除下线的实例

        在大中型的公司里,这两种类型的服务往往都有。⼀些基础的组件例如数据库、缓存等,这些往往不能上报心跳,这种类型的服务在注册时,就需要作为持久化实例注册。而上层的业务服务,例如微服务或者 Dubbo 服务,服务的 Provider 端支持添加汇报心跳的逻辑,此时就可以使用动态服务的注册方式。
        Nacos 2.0 中继续沿用了持久化及非持久化的设定,但是有了⼀些调整。Nacos 1.0 中持久化及非持久化的属性是作为实例的⼀个元数据进行存储和识别。这导致同⼀个服务下可以同时存在持久化实例和非持久化实例。但是在实际使用中,我们发现这种模式会给运维人员带来极大的困惑和运维复杂度;与此同时,从系统架构来看,⼀个服务同时存在持久化及非持久化实例的场景也是存在⼀定矛盾的。这就导致该能力事实上并未被广泛使用。为了简化 Nacos 的服务数据模型,降低运维人员的复杂度,提升 Nacos 的易用性,在 Nacos2.0 中我们将是否持久化的数据抽象至服务级别,且不再允许⼀个服务同时存在持久化实例和非持久化实例,实例的持久化属性继承自服务的持久化属性。

2、数据⼀致性

        数据⼀致性是分布式系统永恒的话题,Paxos 协议的复杂更让数据⼀致性成为程序员大牛们吹水的常见话题。不过从协议层面上看,⼀致性的选型已经很长时间没有新的成员加入了。目前来看基本可以归为两家:⼀种是基于 Leader 的非对等部署的单点写⼀致性,⼀种是对等部署的多写⼀致性。当我们选用服务注册中心的时候,并没有⼀种协议能够覆盖所有场景,例如当注册的服务节点不会定时发送心跳到注册中心时,强⼀致协议看起来是唯⼀的选择,因为无法通过心跳来进行数据的补偿注册,第⼀次注册就必须保证数据不会丢失。而当客户端会定时发送心跳来汇报健康状态时,第⼀次的注册的成功率并不是非常关键(当然也很关键,只是相对来说我们容忍数据的少量写失败),因为后续还可以通过心跳再把数据补偿上来,此时 Paxos 协议的单点瓶颈就会不太划算了,这也是Eureka 为什么不采用 Paxos 协议而采用自定义的 Renew 机制的原因。这两种数据⼀致性协议有各自的使用场景,对服务注册的需求不同,就会导致使用不同的协议。在这里可以发现,Zookeeper 在 Dubbo 体系下表现出的行为,其实采用 Eureka 的 Renew 机制更加合适,因为 Dubbo 服务往 Zookeeper 注册的就是临时节点,需要定时发心跳到 Zookeeper来续约节点,并允许服务下线时,将 Zookeeper 上相应的节点摘除。Zookeeper 使用 ZAB 协议虽然保证了数据的强⼀致,但是它的机房容灾能力的缺乏,无法适应⼀些大型场景。

        Nacos 因为要支持多种服务类型的注册,并能够具有机房容灾、集群扩展等必不可少的能力,在1.0.0 正式支持 AP 和 CP 两种⼀致性协议并存。1.0.0 重构了数据的读写和同步逻辑,将与业务相关的 CRUD 与底层的⼀致性同步逻辑进行了分层隔离。然后将业务的读写(主要是写,因为读会直接使用业务层的缓存)抽象为 Nacos 定义的数据类型,调用⼀致性服务进行数据同步。在决定使用 CP 还是 AP ⼀致性时,使用⼀个代理,通过可控制的规则进行转发。目前的⼀致性协议实现,⼀个是基于简化的 Raft 的 CP ⼀致性,⼀个是基于自研协议 Distro 的AP ⼀致性。Raft 协议不必多言,基于 Leader 进行写入,其 CP 也并不是严格的,只是能保证⼀半所见⼀致,以及数据的丢失概率较小。Distro 协议则是参考了内部 ConfigServer 和开源 Eureka,在不借助第三方存储的情况下,实现基本大同小异。Distro 重点是做了⼀些逻辑的优化和性能的调优。

3、负载均衡

        负载均衡严格的来说,并不算是传统注册中心的功能。⼀般来说服务发现的完整流程应该是先从注册中心获取到服务的实例列表,然后再根据自身的需求,来选择其中的部分实例或者按照⼀定的流量分配机制来访问不同的服务提供者,因此注册中心本身⼀般不限定服务消费者的访问策略。Eureka、Zookeeper 包括 Consul,本身都没有去实现可配置及可扩展的负载均衡机制,Eureka 的负载均衡是由 ribbon 来完成的,而 Consul 则是由 Fabio 做负载均衡
        服务端的负载均衡,给服务提供者更强的流量控制权,但是无法满足不同的消费者希望使用不同负载均衡策略的需求。而不同负载均衡策略的场景,确实是存在的。而客户端的负载均衡则提供了这种灵活性,并对用户扩展提供更加友好的支持。但是客户端负载均衡策略如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务提供者。
        抛开负载均衡到底是在服务提供者实现还是在服务消费者实现,我们看到目前的负载均衡有基于权重、服务提供者负载、响应时间、标签等策略。其中 Ribbon 设计的客户端负载均衡机制,主要是选择合适现有的 IRule、ServerListFilter 等接口实现,或者自己继承这些接口,实现自己的过滤逻辑。这里 Ribbon 采用的是两步负载均衡,第⼀步是先过滤掉不会采用的服务提供者实例,第二步是在过滤后的服务提供者实例里,实施负载均衡策略。Ribbon 内置的几种负载均衡策略功能还是比较强大的,同时又因为允许用户去扩展,这可以说是⼀种比较好的设计。
        基于标签的负载均衡策略可以做到非常灵活,Kubernetes 和 Fabio 都已经将标签运用到了对资源的过滤中,使用标签几乎可以实现任意比例和权重的服务流量调配。但是标签本身需要单独的存储以及读写功能,不管是放在注册中心本身或者对接第三方的 CMDB。

4、健康检查

        Zookeeper 和 Eureka 都实现了⼀种 TTL 的机制,就是如果客户端在⼀定时间内没有向注册中心发送心跳,则会将这个客户端摘除。Eureka 做的更好的⼀点在于它允许在注册服务的时候,自定义检查自身状态的健康检查方法。这在服务实例能够保持心跳上报的场景下,是⼀种比较好的体验,在Dubbo 和 SpringCloud 这两大体系内,也被培养成用户心智上的默认行为。Nacos 也支持这种TTL 机制,不过这与 ConfigServer 在阿里巴巴内部的机制又有⼀些区别。Nacos 目前支持临时实例使用心跳上报方式维持活性,发送心跳的周期默认是 5 秒,Nacos 服务端会在 15 秒没收到心跳后将实例设置为不健康,在 30 秒没收到心跳时将这个临时实例摘除。
        不过正如前文所说,有⼀些服务无法上报心跳,但是可以提供⼀个检测接口,由外部去探测。这样的服务也是广泛存在的,而且以我们的经验,这些服务对服务发现和负载均衡的需求同样强烈。服务端健康检查最常见的方式是 TCP 端口探测和 HTTP 接口返回码探测,这两种探测方式因为其协议的通用性可以支持绝大多数的健康检查场景。在其他⼀些特殊的场景中,可能还需要执行特殊的接口才能判断服务是否可用。例如部署了数据库的主备,数据库的主备可能会在某些情况下切换,需要通过服务名对外提供访问,保证当前访问的库是主库。此时的健康检查接口,可能就是⼀个检查数据库是否是主库的 MYSQL 命令了。
 
        客户端健康检查和服务端健康检查有⼀些不同的关注点。客户端健康检查主要关注客户端上报心跳的方式、服务端摘除不健康客户端的机制。而服务端健康检查,则关注探测客户端的方式、灵敏度及设置客户端健康状态的机制。从实现复杂性来说,服务端探测肯定是要更加复杂的,因为需要服务端根据注册服务配置的健康检查方式,去执行相应的接口,判断相应的返回结果,并做好重试机制和线程池的管理。这与客户端探测,只需要等待心跳,然后刷新 TTL 是不⼀样的。同时服务端健康检查无法摘除不健康实例,这意味着只要注册过的服务实例,如果不调用接口主动注销,这些服务实例都需要去维持健康检查的探测任务,而客户端则可以随时摘除不健康实例,减轻服务端的压力。
 
        Nacos 既支持客户端的健康检查,也支持服务端的健康检查,同⼀个服务可以切换健康检查模式。我们认为这种健康检查方式的多样性非常重要,这样可以支持各种类型的服务,让这些服务都可以使用到 Nacos 的负载均衡能力

二、Nacos 注册中心服务数据模型

 

1、服务(Service)和服务实例(Instance)

1)定义服务

在 Nacos 中,服务的定义包括以下几个内容:

  • 命名空间(Namespace):Nacos 数据模型中最顶层、也是包含范围最广的概念,用于在类似环境或租户等需要强制隔离的场景中定义。Nacos 的服务也需要使用命名空间来进行隔离。 
  • 分组(Group):Nacos 数据模型中次于命名空间的⼀种隔离概念,区别于命名空间的强制隔离属性,分组属于⼀个弱隔离概念,主要用于逻辑区分⼀些服务使用场景或不同应用的同名服务,最常用的情况主要是同⼀个服务的测试分组和生产分组、或者将应用名作为分组以防止不同应用 提供的服务重名。 
  • 服务名(Name):该服务实际的名字,⼀般用于描述该服务提供了某种功能或能力。
        之所以 Nacos 将服务的定义拆分为命名空间、分组和服务名,除了方便隔离使用场景外,还有方便用户发现唯⼀服务的优点。在注册中心的实际使用场景上,同个公司的不同开发者可能会开发出类似作用的服务,如果仅仅使用服务名来做服务的定义和表示,容易在⼀些通用服务上出现冲突,比如登陆服务等。
 
        通常推荐使用由运行环境作为命名空间、应用名作为分组和服务功能作为服务名的组合来确保该服务的天然唯⼀性,当然使用者可以忽略命名空间和分组,仅使用服务名作为服务唯⼀标示,这就需要使用者在定义服务名时额外增加自己的规则来确保在使用中能够唯⼀定位到该服务而不会发现到错误的服务上。

2)服务元数据

        服务的定义只是为服务设置了⼀些基本的信息,用于描述服务以及方便快速的找到服务,而服务的元数据是进⼀步定义了 Nacos 中服务的细节属性和描述信息。主要包含:
  • 健康保护阈值(ProtectThreshold):为了防止因过多实例故障,导致所有流量全部流入剩余实例,继而造成流量压力将剩余实例被压垮形成的雪崩效应。应将健康保护阈值定义为⼀个 0 到 1之间的浮点数。当域名健康实例数占总服务实例数的比例小于该值时,无论实例是否健康,都会将这个实例返回给客户端。这样做虽然损失了⼀部分流量,但是保证了集群中剩余健康实例能正常工作。 
  • 实例选择器(Selector):用于在获取服务下的实例列表时,过滤和筛选实例。该选择器也被称为路由器,目前 Nacos 支持通过将实例的部分信息存储在外部元数据管理 CMDB 中,并在发现服务时使用 CMDB 中存储的元数据标签来进行筛选的能力。 
  • 拓展数据(extendData):用于用户在注册实例时自定义扩展的元数据内容,形式为 K-V 。可以在服务中拓展服务的元数据信息,方便用户实现自己的自定义逻辑。

 3)定义实例

        由于服务实例是具体提供服务的节点,因此 Nacos 在设计实例的定义时,主要需要存储该实例的⼀些网络相关的基础信息,主要包含以下内容:
  • 网络 IP 地址:该实例的 IP 地址,在 Nacos2.0 版本后支持设置为域名。 
  • 网络端口:该实例的端口信息。 
  • 健康状态(Healthy):用于表示该实例是否为健康状态,会在 Nacos 中通过健康检查的手段进行维护,具体内容将在 Nacos 健康检查机制章节中详细说明,读者目前只需要该内容的含义即可。 
  • 集群(Cluster):用于标示该实例归属于哪个逻辑集群,有关于集群的相关内容,将在后文详细说明。 
  • 拓展数据(extendData):用于用户自定义扩展的元数据内容,形式为 K-V。可以在实例中拓展该实例的元数据信息,方便用户实现自己的自定义逻辑和标示该实例。

4)实例元数据

        和服务元数据不同,实例的元数据主要作用于实例运维相关的数据信息。主要包含:
  • 权重(Weight):实例级别的配置。权重为浮点数,范围为 0-10000。权重越大,分配给该实例的流量越大。 
  • 上线状态(Enabled):标记该实例是否接受流量,优先级大于权重和健康状态。用于运维人员在不变动实例本身的情况下,快速地手动将某个实例从服务中移除。
  • 拓展数据(extendData):不同于实例定义中的拓展数据,这个拓展数据是给予运维人员在不变动实例本身的情况下,快速地修改和新增实例的扩展数据,从而达到运维实例的作用。

        在 Nacos2.0 版本中,实例数据被拆分为实例定义和实例元数据,主要是因为这两类数据其实是同⼀个实例的两种不同场景:开发运行场景及运维场景。对于上下线及权重这种属性,⼀般认为在实例已经在运行时,需要运维人员手动修改和维护的数据,而 IP,端口和集群等信息,⼀般情况下在实例启动并注册后,则不会在进行变更。将这两部分数据合并后,就能够得到实例的完整信息,也是 Nacos1.0 版本中的实例数据结构。

        
        同时在 Nacos2.0 版本中,定义实例的这部分数据,会受到持久化属性的的影响,而实例元数据部分,则⼀定会进行持久化;这是因为运维操作需要保证操作的原子性,不能够因为外部环境的影响而导致操作被重置,例如在 Nacos1.0 版本中,运维人员因为实例所处的网络存在问题,操作⼀个实例下线以此摘除流量,但是同样因为网络问题,该实例与 Nacos 的通信也收到影响,导致实例注销后重新注册,这可能导致上线状态被重新注册而覆盖,失去了运维人员操作的优先级。
        
        当然,这部分元数据也不应该无限制的存储下去,如果实例确实已经移除,元数据也应该移除,为此,在 Nacos 2.0 版本后,通过该接口更新的元数据会在对应实例删除后,依旧存在⼀段时间,如果在此期间实例重新注册,该元数据依旧生效;您可以通过 nacos.naming.clean.expired-metadata.expired-time 及 nacos.naming.clean.expired-metadata.interval 对记忆时间进行修改。

5)持久化属性

        如 Nacos 注册中心的设计原理文中所述,Nacos 提供两种类型的服务:持久化服务和非持久化服务,分别给类 DNS 的基础的服务组件场景和上层实际业务服务场景使用。为了标示该服务是哪种类型的服务,需要在创建服务时选择服务的持久化属性。考虑到目前大多数使用动态服务发现的场景为非持久化服务的类型(如 Spring Cloud,Dubbo,Service Mesh 等),Nacos 将缺醒值设置为了非持久化服务。

        在 Nacos2.0 版本后,持久化属性的定义被抽象到服务中,⼀个服务只能被定义成持久化服务或非持久化服务,⼀旦定义完成,在服务生命周期结束之前,无法更改其持久化属性。 持久化属性将会影响服务及实例的数据是否会被 Nacos 进行持久化存储,设置为持久化之后,实例将不会再被自动移除,需要使用者手动移除实例。

2、集群(Cluster)

        集群是 Nacos 中⼀组服务实例的⼀个逻辑抽象的概念,它介于服务和实例之间,是⼀部分服务属性的下沉和实例属性的抽象。

1、定义集群

        在 Nacos 中,集群中主要保存了有关健康检查的⼀些信息和数据:
  • 健康检查类型(HealthCheckType):使用哪种类型的健康检查方式,目前支持:TCP,HTTP,MySQL;设置为 NONE 可以关闭健康检查。 
  • 健康检查端口(HealthCheckPort):设置用于健康检查的端口。 是否使用实例端口进行健康检查(UseInstancePort):如果使用实例端口进行健康检查,将会使用实例定义中的网络端口进行健康检查,而不再使用上述设置的健康检查端口进行。 
  • 拓展数据(extendData):用于用户自定义扩展的元数据内容,形式为 K-V 。可以自定义扩展该集群的元数据信息,方便用户实现自己的自定义逻辑和标示该集群。

3、生命周期

        在注册中心中,实例数据都和服务实例的状态绑定,因此服务实例的状态直接决定了注册中心中实例数据的生命周期。而服务作为实例的聚合抽象,生命周期也会由服务实例的状态来决定。

1)服务的⽣命周期

        服务的生命周期相对比较简单,是从用户向注册中心发起服务注册的请求开始。在 Nacos 中,发起服务注册有两种方式,⼀种是直接创建服务,⼀种是注册实例时自动创建服务;前者可以让发起者在创建时期就制定⼀部分服务的元数据信息,而后者只会使用默认的元数据创建服务。
        在生命周期期间,用户可以向服务中新增,删除服务实例,同时也能够对服务的元数据进行修改。
        当用户主动发起删除服务的请求或⼀定时间内服务下没有实例(无论健康与否)后,服务才结束其生命周期,等待下⼀次的创建。

2)实例的⽣命周期

        实例的生命周期开始于注册实例的请求。但是根据不同的持久化属性,实例后续的生命周期有⼀定的不同。
        持久化的实例,会通过健康检查的状态维护健康状态,但是不会自动的终止该实例的生命周期;在生命周期结束之前,持久化实例均可以被修改数据,甚至主动修改其健康状态。唯⼀终止持久化实例生命周期的方式就是注销实例的请求。
        而非持久化的实例,会根据版本的不同,采用不同的方式维持健康状态:如果是 Nacos1.0 的版本,会通过定时的心跳请求来进行续约,当超过⼀定时间内没有心跳进行续约时,该非持久化实例则终止生命周期;如果是 Nacos2.0 的版本,会通过 gRPC 的长连接来维持状态,当连接发生中断时,该非持久化实例则终止生命周期。当然,非持久化实例也可以通过注销实例的请求,主动终止其生命周期,但是由于长连接和心跳续约的存在,可能导致前⼀个实例数据的生命周期刚被终止移除,立刻又因为心跳和长连接的补偿请求,再次开启实例的生命周期,给人⼀种注销失败的假象。

3)集群的⽣命周期

        集群的生命周期则相对复杂,由于集群作为服务和实例的⼀个中间层,因此集群的生命周期与实例和服务的生命周期均有关。
        集群的生命周期开始与该集群第⼀个实例的生命周期同时开始,因为⼀个实例必定归属于⼀个集群,哪怕是默认的集群,因此当第⼀个实例的生命周期开始时,也就是集群生命周期的开始;
        当⼀个集群下不存在实例时,集群的生命周期也不会立刻结束,而是会等到这个服务的生命周期结束时,才会⼀起结束生命周期。

4)元数据的⽣命周期

        由于元数据的其对应的数据模型是紧密关联的,所以元数据的生命周期基本和对应的数据模型保持 ⼀致。但是也如前文所说,元数据通常为运维人员的主动操作的数据,会被 Nacos 进行⼀段时间内的记忆,因此元数据的生命周期的终止相比对应的数据要滞后;若这滞后期间内,对应的数据又重新开始生命周期,则该元数据的生命周期将被立刻重置,不再终止。

 

 

猜你喜欢

转载自blog.csdn.net/a6470831/article/details/124304352