前言


Dubbo 早期开源时默认的注册中心 ZooKeeper 最早进入人们的视线,并且在很长一段时间里,人们将注册中心和 ZooKeeper 划上了等号,可能 ZooKeeper 的设计者都没有想到这款产品对微服务领域造成了如此深厚的影响,直到 Spring Cloud 开始流行,其自带的 Eureka 进入了人们的视野,人们这才意识到原来注册中心还可以有其他的选择。再到后来,热衷于开源的阿里把目光也聚焦在了注册中心这个领域, Nacos 横空出世。

应用构建设计模型的问题


ZK: 采用CP模型

我们先来看一下网络分区(Network Partition)情况下注册中心不可用对服务调用产生的影响,即 CAP 中的A不满足时带来的影响。

注: 后文将 service 简写为 svc, 且svcB为服务提供方, svcA为服务消费方

考虑一个典型的ZooKeeper三机房容灾5节点部署结构 (即2-2-1结构),如下图:
ZooKeeper和Nacos于作为注册中心的区别 - 图1
当机房3出现网络分区(Network Partitioned)的时候,即机房3在网络上成了孤岛,我们知道虽然整体 ZooKeeper 服务是可用的,但是节点ZK5是不可写的,因为联系不上 Leader。
也就是说,这时候机房3的应用服务 svcB 是不可以新部署,重新启动,扩容或者缩容的,但是站在网络和服务调用的角度看,机房3的 svcA 虽然无法调用机房1和机房2的 svcB,但是与机房3的svcB之间的网络明明是 OK 的啊,为什么不让我调用本机房的服务?
现在因为注册中心自身为了保脑裂(P)下的数据一致性(C)而放弃了可用性,导致了同机房的服务之间出现了无法调用,这是绝对不允许的!可以说在实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性,这是注册中心设计应该遵循的铁律!
通过以上我们的阐述可以看到,在 CAP 的权衡中,注册中心的可用性比数据强一致性更宝贵,所以整体设计更应该偏向 AP,而非 CP,数据不一致在可接受范围: 只要注册中心在SLA承诺的时间内(例如1s内)将数据收敛到一致状态(即满足最终一致),流量将很快趋于统计学意义上的一致,所以注册中心以最终一致的模型设计在生产实践中完全可以接受。而P下舍弃A却完全违反了注册中心不能因为自身的任何原因破坏服务本身的可连通性的原则。

注: SLA:一般指服务级别协议。 服务级别协议是指提供服务的企业与客户之间就服务的品质、水准、性能等方面所达成的双方共同认可的协议或契约。

Nacos: 支持CP(针对临时实例) 、AP(针对持久化实例)

其实 Nacos 并不是支持两种一致性模型,也并不是支持两种模型的切换,而是共存和兼容. Nacos 中的有两个概念:临时服务和持久化服务。

  • 临时服务(Ephemeral):临时服务健康检查失败后会从列表中删除,常用于服务注册发现场景。
  • 持久化服务(Persistent):持久化服务健康检查失败后会被标记成不健康,常用于 DNS 场景。

临时服务使用的是 Nacos 为服务注册发现场景定制化的私有协议 distro,其一致性模型是 AP;而持久化服务使用的是 raft 协议,其一致性模型是 CP。所以是受服务节点状态或者使用场景的约束, Nacos通过这两种手段,既可以选择保证可用性,又可以选择保证数据的最终一致性.
具体流程可以参阅:https://mp.weixin.qq.com/s/vr1DC_1eWOAbbpJlWWJgiw

  1. #摘自ServiceManager--632
  2. public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
  3. throws NacosException {
  4. String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
  5. Service service = getService(namespaceId, serviceName);
  6. synchronized (service) {
  7. List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
  8. Instances instances = new Instances();
  9. instances.setInstanceList(instanceList);
  10. //调用 DelegateConsistencyServiceImpl 类的 put 方法。
  11. //在这个地方有一个 AP/CP 模式的我们可以选择
  12. consistencyService.put(key, instances);
  13. }
  14. }
  1. @DependsOn("ProtocolManager")
  2. @Service("consistencyDelegate")
  3. public class DelegateConsistencyServiceImpl implements ConsistencyService {
  4. //raft算法的实现服务
  5. private final PersistentConsistencyServiceDelegateImpl persistentConsistencyService;
  6. //Distro算法的实现服务
  7. private final EphemeralConsistencyService ephemeralConsistencyService;
  8. ....
  9. @Override
  10. public void put(String key, Record value) throws NacosException {
  11. mapConsistencyService(key).put(key, value);
  12. }
  13. //选择对应的模型
  14. private ConsistencyService mapConsistencyService(String key) {
  15. return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
  16. }
  17. }

服务规模、容量、服务联通性


服务发现和健康监测场景下,随着服务规模的增大,无论是应用频繁发布时的服务注册带来的写请求,还是刷毫秒级的服务健康状态带来的写请求,都无疑是个挑战.

ZK: Leader中心化、长链接的压力

ZooKeeper 集群中的所有机器通过一个 Leader 选举过程 来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,FollowerObserver都只能提供读服务。所以ZooKeeper 的写并不是可扩展的,不可以通过加节点解决水平扩展性问题。其次,对于 Session 的健康监测上,或者说绑定在TCP长链接活性探测上, 整个数据中心的机器或者容器皆与注册中心有长连接带来的连接压力.
要想在 ZooKeeper 基础上硬着头皮解决服务规模的增长问题,一个实践中可以考虑的方法是想办法梳理业务,垂直划分业务域,将其划分到多个 ZooKeeper 注册中心,但是作为提供通用服务的平台机构组,因自己提供的服务能力不足要业务按照技术的指挥棒配合划分治理业务,真的可行么?
而且这又违反了因为注册中心自身的原因(能力不足)破坏了服务的可连通性,举个简单的例子,1个搜索业务,1个地图业务,1个大文娱业务,1个游戏业务,他们之间的服务就应该老死不相往来么?也许今天是肯定的,那么明天呢,1年后呢,10年后呢?谁知道未来会要打通几个业务域去做什么奇葩的业务创新?注册中心作为基础服务,无法预料未来的时候当然不能妨碍业务服务对未来固有联通性的需求

Nacos: 去中心化、推送模型

首先采用去中心化的思想,每个节点是平等的都可以处理写入请求,同时把新数据同步到其他节点.
其次Nacos通过ServiceChangeEvent事件触发我们的推送的模式,对于Zookeeper那种通过tcp长连接来说会节约很多资源,就算大量的节点更新也不会让Nacos出现太多的性能瓶颈,在Nacos中客户端如果接受到了udp消息会返回一个ACK,如果一定时间Nacos-Server没有收到ACK,那么还会进行重发,当超过一定重发时间之后,就不在重发了,虽然通过udp并不能保证能真正的送到订阅者,但是Nacos还有定时轮训作为兜底,不需要担心数据不会更新的情况。

健康检测(Service Health Check)


健康检测的一大基本设计原则就是尽可能真实的反馈服务本身的真实健康状态,否则一个不敢被服务调用者相信的健康状态判定结果还不如没有健康检测。

ZK: 对Session、TCP的活性检测

使用 ZooKeeper 作为服务注册中心时,服务的健康检测常利用 ZooKeeper 的 Session 活性 Track机制 以及结合 Ephemeral ZNode的机制,简单而言,就是将服务的健康监测绑定在了 ZooKeeper 对于 Session 的健康监测上,或者说绑定在TCP长链接活性探测上了。
这在很多时候也会造成致命的问题,ZK 与服务提供者机器之间的TCP长链接活性探测正常的时候,该服务就是健康的么?答案当然是否定的!注册中心应该提供更丰富的健康监测方案,服务的健康与否的逻辑应该开放给服务提供方自己定义(Eureka的做法),而不是一刀切搞成了 TCP 活性检测!

Nacos: 临时节点续约、持久节点心跳检测、持久节点探活

临时节点续约

临时实例向Nacos注册,Nacos不会对其进行持久化存储,只能通过心跳方式保活。默认模式是:客户端心跳上报Nacos实例健康状态,默认间隔5秒,Nacos在15秒内未收到该实例的心跳,则会设置为不健康状态,超过30秒则将实例删除。但阅读源码发现2.0版本已经把这种做法取消掉

  1. public class ClientBeatCheckTask implements BeatCheckTask {
  2. @Override
  3. public void run() {
  4. try {
  5. // If upgrade to 2.0.X stop health check with v1
  6. if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
  7. return;
  8. }
  9. .....
  10. }
  11. ......
  12. }

持久节点心跳检测
主要实现类为ClientBeatCheckTaskV2:

  1. #摘自ClientBeatCheckTaskV2#doHealthCheck--65
  2. @Override
  3. public void doHealthCheck() {
  4. try {
  5. // 获取所有的Service
  6. Collection<Service> services = client.getAllPublishedService();
  7. for (Service each : services) {
  8. // 获取Service对应的InstancePublishInfo
  9. HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client
  10. .getInstancePublishInfo(each);
  11. // 创建一个InstanceBeatCheckTask,并交由拦截器链处理
  12. interceptorChain.doInterceptor(new InstanceBeatCheckTask(client, each, instance));
  13. }
  14. } catch (Exception e) {
  15. Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
  16. }
  17. }

具体查看InstanceBeatCheckTask

  1. public class InstanceBeatCheckTask implements Interceptable {
  2. //加载检查器
  3. static {
  4. //倘若没有设置preserved.heart.beat.timeout,默认是超过15s就认为不健康的检查逻辑
  5. CHECKERS.add(new UnhealthyInstanceChecker());
  6. //在 30 秒没收到心跳时将这个临时实例摘除的检查逻辑
  7. CHECKERS.add(new ExpiredInstanceChecker());
  8. CHECKERS.addAll(NacosServiceLoader.load(InstanceBeatChecker.class));
  9. }
  10. .....
  11. @Override
  12. public void passIntercept() {
  13. for (InstanceBeatChecker each : CHECKERS) {
  14. //执行检查逻辑
  15. each.doCheck(client, service, instancePublishInfo);
  16. }
  17. }
  18. }

持久节点探活:

主要实现类为HealthCheckTaskV2:

  1. #摘自HealthCheckTaskV2#doHealthCheck--93
  2. @Override
  3. public void doHealthCheck() {
  4. try {
  5. //获取Client的service列表
  6. for (Service each : client.getAllPublishedService()) {
  7. //是否开启健康检查
  8. if (switchDomain.isHealthCheckEnabled(each.getGroupedServiceName())) {
  9. //注册节点信息
  10. InstancePublishInfo instancePublishInfo = client.getInstancePublishInfo(each);
  11. ClusterMetadata metadata = getClusterMetadata(each, instancePublishInfo);
  12. //容器中获取该代表类
  13. ApplicationUtils.getBean(HealthCheckProcessorV2Delegate.class).process(this, each, metadata);
  14. if (Loggers.EVT_LOG.isDebugEnabled()) {
  15. Loggers.EVT_LOG.debug("[HEALTH-CHECK-V2] schedule health check task: {}", client.getClientId());
  16. }
  17. }
  18. }
  19. } catch (Throwable e) {
  20. Loggers.SRV_LOG.error("[HEALTH-CHECK-V2] error while process health check for {}", client.getClientId(), e);
  21. }
  1. #摘自HealthCheckProcessorV2#process--53
  2. @Override
  3. public void process(HealthCheckTaskV2 task, Service service, ClusterMetadata metadata) {
  4. String type = metadata.getHealthyCheckType();
  5. //找出对应的检查处理器
  6. HealthCheckProcessorV2 processor = healthCheckProcessorMap.get(type);
  7. if (processor == null) {
  8. //如果没有设定则为None,啥也不执行
  9. processor = healthCheckProcessorMap.get(NoneHealthCheckProcessor.TYPE);
  10. }
  11. processor.process(task, service, metadata);
  12. }

具体的其他实现类如下,有Http、Tcp和Mysql的实现, 也可以根据该接口自定义健康检查实现.
image.png

注册中心的容灾和可用性的考虑


ZK: 弱依赖注册中心

服务调用(请求响应流)链路应该是弱依赖注册中心,必须仅在服务发布,机器上下线,服务扩缩容等必要时才依赖注册中心。
这需要注册中心仔细的设计自己提供的客户端,客户端中应该有针对注册中心服务完全不可用时做容灾的手段,例如设计客户端缓存数据机制(我们称之为 client snapshot)就是行之有效的手段。另外,注册中心的 health check 机制也要仔细设计以便在这种情况不会出现诸如推空等情况的出现。
ZooKeeper的原生客户端并没有这种能力,所以利用 ZooKeeper 实现注册中心的时候我们一定要问自己,如果把 ZooKeeper 所有节点全干掉,你生产上的所有服务调用链路能不受任何影响么?而且应该定期就这一点做故障演练。

Nacos: 本地缓存文件 Failover 机制、心跳同步服务


本地缓存文件 Failover 机制

我们思考一个问题当 Dubbo 应用运行时,Nacos 注册中心宕机,会不会影响 RPC 调用。这个题目大多数应该都能回答出来,因为 Dubbo 内存里面是存了一份地址的,一方面这样的设计是为了性能,因为不可能每次 RPC 调用时都读取一次注册中心,另一面,注册中心宕机后内存会有一份数据,这也起到了可用性的保障(尽管可能 Dubbo 设计者并没有考虑这个因素)。
那如果,我在此基础上再抛出一个问题:Nacos 注册中心宕机,Dubbo 应用发生重启,会不会影响 RPC 调用。如果了解了 Nacos 的 Failover 机制,应当得到和上一题同样的回答:不会。
Nacos 存在本地文件缓存机制,nacos-client 在接收到 nacos-server 的服务推送之后,会在内存中保存一份,随后会落盘存储一份快照。snapshot 默认的存储路径为:{USER_HOME}/nacos/naming/ 中:
image.png
{USER_HOME}/nacos/naming/{namespace} 下除了缓存文件之外还有一个 failover 文件夹,里面存放着和 snapshot 一致的文件夹。这是 Nacos 的另一个 failover 机制,snapshot 是按照某个历史时刻的服务快照恢复恢复,而 failover 中的服务可以人为修改,以应对一些极端场景。

心跳同步服务

心跳机制一般广泛存在于分布式通信领域,用于确认存活状态。一般心跳请求和普通请求的设计是有差异的,心跳请求一般被设计的足够精简,这样在定时探测时可以尽可能避免性能下降。而在 Nacos 中,出于可用性的考虑,一个心跳报文包含了全部的服务信息,这样相比仅仅发送探测信息降低了吞吐量,而提升了可用性,怎么理解呢?考虑以下的两种场景:

  • nacos-server 节点全部宕机,服务数据全部丢失。nacos-server 即使恢复运作,也无法恢复出服务,而心跳包含全部内容可以在心跳期间就恢复出服务,保证可用性。
  • nacos-server 出现网络分区。由于心跳可以创建服务,从而在极端网络故障下,依旧保证基础的可用性

    一致性协议 distro

    distro 协议与高可用有什么关系呢? nacos-server 节点宕机后,客户端会重试,但少了一个前提,即 nacos-server 少了一个节点后依旧可以正常工作。Nacos 这种有状态的应用和一般无状态的 Web 应用不同,并不是说只要存活一个节点就可以对外提供服务的,需要分 case 讨论,这与其一致性协议的设计有关。distro 协议的工作流程如下:

  • Nacos 启动时首先从其他远程节点同步全部数据。

  • Nacos 每个节点是平等的都可以处理写入请求,同时把新数据同步到其他节点。
  • 每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。

image.pngZooKeeper和Nacos于作为注册中心的区别 - 图5
如上图所示,每个节点负责一部分服务的写入,但每个节点都可以接收到写入请求,这时就存在两种情况:

  • 当该节点接收到属于该节点负责的服务时,直接写入。
  • 当该节点接收到不属于该节点负责的服务时,将在集群内部路由,转发给对应的节点,从而完成写入。

读取操作则不需要路由,因为集群中的各个节点会同步服务状态,每个节点都会有一份最新的服务数据。
而当节点发生宕机后,原本该节点负责的一部分服务的写入任务会转移到其他节点,从而保证 Nacos 集群整体的可用性。
ZooKeeper和Nacos于作为注册中心的区别 - 图6
一个比较复杂的情况是,节点没有宕机,但是出现了网络分区,即下图所示:
ZooKeeper和Nacos于作为注册中心的区别 - 图7image.png
这个情况会损害可用性,客户端会表现为有时候服务存在有时候服务不存在。
综上,Nacos 的 distro 一致性协议可以保证在大多数情况下,集群中的机器宕机后依旧不损害整体的可用性。该可用性保证存在于 nacos-server 端。

Nacos支持动态修改配置


通过动态配置服务,我们可以在所有环境中以集中和动态的方式管理所有应用程序或服务的配置信息。动态配置中心可以实现配置更新时无需重新部署应用程序和服务即可使相应的配置信息生效,这极大了增加了系统的运维能力。

动态配置服务

动态配置服务让您能够以中心化、外部化和动态化的方式管理所有环境的配置。动态配置消除了配置变更时重新部署应用和服务的需要。配置中心化管理让实现无状态服务更简单,也让按需弹性扩展服务更容易。

服务发现及管理

动态服务发现对以服务为中心的(例如微服务和云原生)应用架构方式非常关键。Nacos支持DNS-Based和RPC-Based(Dubbo、gRPC)模式的服务发现。Nacos也提供实时健康检查,以防止将请求发往不健康的主机或服务实例。借助Nacos,您可以更容易地为您的服务实现断路器。

动态DNS服务

通过支持权重路由,动态DNS服务能让您轻松实现中间层负载均衡、更灵活的路由策略、流量控制以及简单数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以DNS协议为基础的服务发现,以消除耦合到厂商私有服务发现API上的风险


主要原理

ClientWorker里面执行一个定时任务的线程池, 分别做两部分

  • 本地检查: 主要是做一个故障容错,当服务端挂掉后,Nacos 客户端可以从本地的文件系统中获取相关的配置信息,主要在~/nacos/config/fixed-{address}8848nacos/snapshot/DEFAULT_GROUP/{dataId}目录下.
  • 服务端检查: 在addTenantListeners,把Listener都注册到CacheData中,等待后续的触发回调,客户端将最新的数据获取下来之后,保存在了 CacheData 中,将最新的数据通知给 Listener 的持有者。
  1. #摘自ClientWorker--162
  2. public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
  3. throws NacosException {
  4. group = blank2defaultGroup(group);
  5. String tenant = agent.getTenant();
  6. CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
  7. synchronized (cache) {
  8. for (Listener listener : listeners) {
  9. cache.addListener(listener);
  10. }
  11. cache.setSyncWithServer(false);
  12. agent.notifyListenConfig();
  13. }
  14. }

再来看CacheData的safeNotifyListener方法:获取最新的配置信息,调用 Listener 的回调方法,将最新的配置信息作为参数传入,这样 Listener 的使用者就能接收到变更后的配置信息了

  1. 摘自CacheData--278
  2. private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
  3. {
  4. ....
  5. listenerWrap.inNotifying = true;
  6. listener.receiveConfigInfo(contentTmp);
  7. // compare lastContent and content
  8. if (listener instanceof AbstractConfigChangeListener) {
  9. Map data = ConfigChangeHandler.getInstance()
  10. .parseChangeData(listenerWrap.lastContent, content, type);
  11. ConfigChangeEvent event = new ConfigChangeEvent(data);
  12. ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
  13. listenerWrap.lastContent = content;
  14. }
  15. listenerWrap.lastCallMd5 = md5;
  16. ....
  17. }

总结


一般聊注册中心时, 都会以 Zookeeper 为引子,这也是很多人最熟悉的注册中心。但如果你真的写过或看过使用 Zookeeper 作为注册中心的适配代码,会发现并不是那么容易,再加上注册中心涉及到的一致性原理,这就导致很多人对注册中心的第一印象是:这个东西好难! 但归根到底是因为 Zookeeper 根本不是专门为注册中心而设计的,其提供的 API 以及内核设计,并没有预留出「服务模型」的概念,这就使得开发者需要自行设计一个模型,去填补 Zookeeper 和服务发现之间的鸿沟。
image.png

而Nacos 服务发现使用的领域模型,如上图所示, 是命名空间-分组-服务-集群-实例这样的多层结构。服务 Service 和实例 Instance 是核心模型,命名空间 Namespace 、分组 Group、集群 Cluster 则是在不同粒度实现了服务的隔离。
在服务注册发现场景,Nacos性能上以及为可用性做了非常多的努力,而这些保障,ZooKeeper 是不一定有的。在做注册中心选型时,Nacos 绝对是优秀的。

参阅与转载


https://nacos.io/zh-cn/docs/what-is-nacos.html
https://developer.aliyun.com/article/780618?spm=a2c6h.24874632.0.0.148d1fb6lhU5LW
https://developer.aliyun.com/article/601745
https://xiaozhuanlan.com/topic/8763590142
https://mp.weixin.qq.com/s/vr1DC_1eWOAbbpJlWWJgiw
https://mp.weixin.qq.com/s/oS4OUD6O6Lo9BQuT6-72yA
https://mp.weixin.qq.com/s/FecdiL1arx4FiBquRibqJw