在分布式系统里的注册中心。原理是将部署服务的机器地址记录到注册中心,服务消费者在有需求的时候,只需要查询注册中心,输入提供的服务名,就可以得到地址,从而发起调用。

 

注册中心原理

 

在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图,我来简单解释一下。

RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。

RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。

当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。

RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。

 

注册中心实现方式


1. 注册中心 API

根据注册中心原理的描述,注册中心必须提供以下最基本的 API,例如:

服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。

 

服务注册流程主要有下面几个步骤:

首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步。

其次要查看注册的 Cluster(服务的接口名)是否存在?如果不存在就抛出异常,存在的话继续下一步。

然后要检查 Service(服务的分组)是否存在?如果不存在则抛出异常,存在的话继续下一步。

最后将节点信息添加到对应的 Service 和 Cluster 下面的存储中。

服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。

节点反注册流程主要包含下面几个步骤:

查看 Service(服务的分组)是否存在,不存在就抛出异常,存在就继续下一步。

查看 Cluster(服务的接口名)是否存在,不存在就抛出异常,存在就继续下一步。

删除存储中 Service 和 Cluster 下对应的节点信息。

更新 Cluster 的 sign 值。

心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。

服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。

服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。

除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如:

服务查询接口:查询注册中心当前注册了哪些服务信息。

服务消费者查询节点信息主要分为下面几个步骤:

首先从 localcache(本机内存)中查找,如果没有就继续下一步。这里为什么服务消费者要把服务信息存在本机内存呢?主要是因为服务节点信息并不总是时刻变化的,并不需要每一次服务调用都要调用注册中心获取最新的节点信息,只需要在本机内存中保留最新的服务提供者的节点列表就可以。

接着从 snapshot(本地快照)中查找,如果没有就继续下一步。这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢?这是因为服务消费者同注册中心之间的网络不一定总是可靠的,服务消费者重启时,本机内存中还不存在服务提供者的节点信息,如果此时调用注册中心失败,那么服务消费者就拿不到服务节点信息了,也就没法调用了。本地快照就是为了防止这种情况的发生,即使服务消费者重启后请求注册中心失败,依然可以读取本地快照,获取到服务节点信息。

服务修改接口:修改注册中心中某一服务的信息。

服务消费者如何订阅服务提供者的变更主要分为下面几个步骤:

服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留 Cluster 的 sign 值。

服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的 sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot。

 

2. 集群部署

注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。

以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的,这是为什么呢?这就要从 ZooKeeper 的工作原理说起:

每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。

ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。

Leader 负责处理数据更新等操作(ZAB 协议)。

一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。

通过上面这种方式,ZooKeeper 保证了高可用性以及数据一致性。

 

3. 目录存储

还是以 ZooKeeper 为例,注册中心存储服务信息一般采用层次化的目录结构:

每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识。

znode 可以包含数据和子 znode。

znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。

 

4. 服务健康状态检测


注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。

还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。

在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。

5. 服务状态变更通知

一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。

继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。

6. 白名单机制


在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPC Server 节点,可能会造成意想不到的后果。

为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。

 

注册中心来实现服务发现与传统的 DNS 实现服务发现有什么不同


1.注册中心的服务注册和发现都是基于API的,可以自动化注册与发现,dns则是人工注册。这样也导致前者实时性、容

错性好于后者。

2.注册中心可以注册http、rpc等各种服务,dns只能注册http服务

3.注册中心对已注册的服务会有主动的、自动化的健康检查机制,dns没有。

4.注册中心一般是一级分布式的,dns则是多级架构,例如根域名服务器、权威域名服务器等

5.注册中心的安全机制相对dns弱一些,毕竟是内部使用。

 

服务发布和引用的实践


XML 配置方式的服务发布和引用流程
1. 服务提供者定义接口

服务提供者发布服务之前首先要定义接口,声明接口名、传递参数以及返回值类型,然后把接口打包成 JAR 包发布出去。

package com.weibo.api.common.status.service;

public interface UserLastStatusService {
     /**

     * @param uids

     * @return

     */

    public long getLastStatusId(long uid);

 

    /**

     * @param uids

     * @return

     */

    public Map<Long, Long> getLastStatusIds(long[] uids);

}

2. 服务提供者发布接口

服务提供者发布的接口是通过在服务发布配置文件中定义接口来实现的。

下面以一个具体的motan服务发布配置文件 user-last-status.xml 来给你讲解,它定义了要发布的接口 userLastStatusLocalService,对外暴露的协议是 Motan 协议,端口是 8882。并且针对两个方法 getLastStatusId 和 getLastStatusIds,通过 requestTimeout="300" 单独定义了超时时间是 300ms,通过 retries="0" 单独定义了调用失败后重试次数为 0,也就是不重试。然后服务发布者在进程启动的时候,会加载配置文件 user-last-status.xml,把接口对外暴露出去。

另外,在一个服务被多个服务消费者引用的情况下,由于业务经验的参差不齐,可能不同的服务消费者对服务的认知水平不一,比如某个服务可能调用超时了,最好可以重试来提供调用成功率。最好是可以在服务发布的配置文件中预定义好类似超时重试次数,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。

user-last-status.xml :

<motan:service ref="userLastStatusLocalService"

            requestTimeout="50" retries="2"    interface="com.weibo.api.common.status.service.UserLastStatusService"

            basicService="serviceBasicConfig" export="motan:8882">

   <motan:method name="getLastStatusId" requestTimeout="300"

              retries="0" />

   <motan:method name="getLastStatusIds" requestTimeout="300"

              retries="0" />

</motan:service>

当服务需要配置升级,若引用服务的服务消费者众多,并且涉及多个部门,升级步骤就显得异常重要。

各个服务消费者在服务引用配置文件中添加服务详细信息。

服务提供者升级两台服务器,在服务发布配置文件中删除服务详细信息,并观察是否所有的服务消费者引用时都包含服务详细信息。

如果都包含,说明所有服务消费者均完成升级,那么服务提供者就可以删除服务发布配置中的服务详细信息。

如果有不包含服务详细信息的服务消费者,排查出相应的业务方进行升级,直至所有业务方完成升级。

 

3. 服务消费者引用接口

服务消费者引用接口是通过在服务引用配置文件中定义要引用的接口,并把包含接口定义的 JAR 包引入到代码依赖中。

下面以一个具体的服务引用配置文件 user-last-status-client.xml 来给你讲解,它定义服务消费者引用了接口commonUserLastStatusService,接口通信协议是 Motan。

user-last-status-client.xml :

服务消费者在进程启动时,会加载配置文件 user-last-status-client.xml 来完成服务引用。

<motan:protocol name="motan" default="true" loadbalance="${service.loadbalance.name}" />

<motan:basicReferer id="userLastStatusServiceClientBasicConfig"

               protocol="motan"  />

<!-- 导出接口 -->

<motan:referer id="commonUserLastStatusService" interface="com.weibo.api.common.status.service.UserLastStatusService"

            basicReferer="userLastStatusServiceClientBasicConfig" />

</beans>

 

注册中心选型


主流的服务注册与发现的解决方案
当下主流的服务注册与发现的解决方案,主要有两种:

应用内注册与发现:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。

应用外注册与发现:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。

1. 应用内

采用应用内注册与发现的方式,最典型的案例要属 Netflix 开源的 Eureka,

 Eureka 的架构,它主要由三个重要的组件组成:

Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。

服务端的 Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。

客户端的 Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。

2. 应用外

采用应用外方式实现服务注册和发现,最典型的案例是开源注册中心 Consul,

使用 Consul 实现应用外服务注册和发现主要依靠三个重要的组件:

Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。

Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。

Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。

对比分析
对比小结一下,Eureka和 Consul这两种解决方案的不同之处在于应用场景,应用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系;应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景,比如服务提供者提供的是 C++ 服务,而服务消费者是一个 Java 应用,这时候采用应用外的解决方案就不依赖于具体一个技术体系。同时,对于容器化后的云应用来说,一般不适合采用应用内 SDK 的解决方案,因为这样会侵入业务,而应用外的解决方案正好能够解决这个问题。

 

注册中心的高可用性和数据一致性


1. 高可用性
注册中心作为服务提供者和服务消费者之间沟通的纽带,它的高可用性十分重要。试想,如果注册中心不可用了,那么服务提供者就无法对外暴露自己的服务,而服务消费者也无法知道自己想要调用的服务的具体地址,后果将不堪设想。

实现高可用性的方法主要有两种:

集群部署,顾名思义就是通过部署多个实例组成集群来保证高可用性,这样的话即使有部分机器宕机,将访问迁移到正常的机器上就可以保证服务的正常访问。

多 IDC 部署,就是部署在不止一个机房,这样能保证即使一个机房因为断电或者光缆被挖断等不可抗力因素不可用时,仍然可以通过把请求迁移到其他机房来保证服务的正常访问。

2. 数据一致性
为了保证注册中心的高可用性,注册中心的部署往往都采用集群部署,并且还通常部署在不止一个数据中心,这样的话就会引出另一个问题,多个数据中心之间如何保证数据一致?如何确保访问数据中心中任何一台机器都能得到正确的数据?

这里就涉及分布式系统中著名的 CAP 理论,即同时满足一致性、可用性、分区容错性这三者是不可能的,其中 C(Consistency)代表一致性,A(Availability)代表可用性,P(Partition Tolerance)代表分区容错性。

为什么说 CAP 三者不能被同时满足的呢?

你可以想象在一个分布式系统里面,包含了多个节点,节点之间通过网络连通在一起。正常情况下,通过网络,从一个节点可以访问任何别的节点上的数据。

但是有可能出现网络故障,导致整个网络被分成了互不连通的区域,这就叫作分区。一旦出现分区,那么一个区域内的节点就没法访问其他节点上的数据了,最好的办法是把数据复制到其他区域内的节点,这样即使出现分区,也能访问任意区域内节点上的数据,这就是分区容错性。

但是把数据复制到多个节点就可能出现数据不一致的情况,这就是一致性。要保证一致,就必须等待所有节点上的数据都更新成功才可用,这就是可用性。

总的来说,就是数据节点越多,分区容错性越高,但数据一致性越难保证。为了保证数据一致性,又会带来可用性的问题。

而注册中心一般采用分布式集群部署,也面临着 CAP 的问题,根据 CAP 不能同时满足,所以不同的注册中心解决方案选择的方向也就不同,大致可分为两种。

CP 型注册中心,牺牲可用性来保证数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过 Paxos 算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。

AP 型注册中心,牺牲一致性来保证可用性,最典型的例子就是 Eureka 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。

而对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点,也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐