1 Nacos ⼀致性协议

1.1 为什么 Nacos 需要⼀致性协议

Nacos尽可能减少用户部署以及运维成本,做到用户只需要⼀个程序包,就快速单机模式启动 Nacos 或集群模式启动 Nacos。而 Nacos 是⼀个需要存储数据的组件,为实现目标,就要在 Nacos 内部实现数据存储。单机问题不大,内嵌关系型数据库即可;但集群模式就要考虑保障各节点间的数据⼀致性及数据同步,就得引入共识算法,通过算法保障各节点间的数据⼀致性。

1.2 为什么 Nacos 选择了 Raft 以及 Distro

Nacos 在单个集群中同时运行 CP 协议及 AP 协议?要从 Nacos 场景出发:Nacos 集服务注册发现及配置管理于⼀体,集群下,各节点间的数据⼀致性保障问题,需拆分成两方面:

① 从服务注册发现来看

服务之间感知对方服务的当前可正常提供服务的实例信息,须从服务发现注册中心获取,因此对服务注册发现中心组件的可用性提出高要求,需在任何场景尽最大可能保证服务注册发现能力可以对外提供服务;同时 Nacos 的服务注册发现设计,采取了心跳可自动完成服务数据补偿的机制。如果数据丢失的话,是可以通过该机制快速弥补数据丢失。

为满足服务发现注册中心的可用性,强⼀致性共识算法不太合适,因为强⼀致性共识算法能否对外提供服务有要求,如当前集群可用节点数没过半,整个算法直接“罢工”,而最终⼀致共识算法更多保障服务可用性,并能保证在⼀定时间内各节点之间数据能达成⼀致。上述都是针对 Nacos 服务发现注册中的非持久化服务(即需客户端上报心跳进行服务实例续约)。

而对 Nacos 服务发现注册中的持久化服务,因为所有数据都是直接调用 Nacos服务端直接创建,因此需由 Nacos 保障数据在各节点间的强⼀致性,因此针对此类型的服务数据,选择强⼀致性共识算法来保障数据⼀致性。

② 配置管理

配置数据直接在 Nacos 服务端进行创建并管理,须保证大部分的节点都保存此配置数据,才能认为配置被成功保存,否则就会丢失配置的变更,问题很严重,如发布重要配置变更出现丢失变更,多半引起严重现网故障,因此对配置数据的管理,须集群中大部分节点强⼀致,这里只能使用强⼀致性共识算法。

③ 为啥是 Raft 和 Distro

强⼀致性共识算法,当前最多使用 Raft 协议,易让人理解,并有很多成熟工业算法实现,如蚂蚁金服 JRaft、Zookeeper ZAB、Consul Raft、百度 braft、Apache Ratis;因为 Nacos 是 Java 技术栈,因此只能在 JRaft、ZAB、ApacheRatis 中选择,但ZAB 和 Zookeeper 强绑定,再加上希望可以和 Raft 算法库支持团队随时沟通交流,因此选择 JRaft,也因为 JRaft 支持多 RaftGroup,为 Nacos 后面的多数据分片带来可能。

而 Distro 协议是阿里巴巴自研的⼀个最终⼀致性协议,最终⼀致性协议很多如 Gossip、Eureka 内的数据同步算法。Distro 算法集 Gossip 及 Eureka 协议优点并优化而出,原生 Gossip由于随机选取发送消息的节点,不可避免存在消息重复发给同⼀节点情况,增加网络传输的压力,也给消息节点带来额外的处理负载,而 Distro 算法引入权威 Server 概念,每个节点负责⼀部分数据及将自己的数据同步给其他节点,有效降低消息冗余问题。

2 早期的 Nacos ⼀致性协议

2.1 早期 Naocs 架构

特点

服务注册和配置管理⼀致性协议 分开,没下沉到 Nacos内核模块作为通用能力

  • 服务发现模块⼀致性协议的实现和服务注册发现模块的逻辑强耦合
  • 充斥服务注册发现的概念

缺点

  • Nacos 服务注册发现模块逻辑复杂难维护,耦合了⼀致性协议层的数据状态
  • 计算存储彻底难以分离
  • 对计算层的无限水平扩容能力也有影响

因此必然要对 Nacos⼀致性协议做抽象及下沉,使其成为 Core 模块能力,彻底让服务注册发现模块只充当计算能力,同时为配置模块去外部数据库存储打下架构基础。

3 当前 Nacos 的⼀致性协议层

当前 Nacos 内核中,已将⼀致性协议的能力完全下沉到内核模块作为 Nacos 核心能力,很好的服务于服务注册发现模块及配置管理模块。

3.1 当前 Nacos 的架构

特点

新 Nacos 架构将⼀致性协议从原先服务注册发现模块下沉到内核模块,并尽可能提供统⼀抽象接口,使上层的服务注册发现模块及配置管理模块,不再耦合任何⼀致性语义。

优点

解耦抽象分层后,每个模块能快速演进,并且性能和可用性都大幅提升。

4 Nacos 如何做到⼀致性协议下沉

既然 Nacos 将 AP、CP 协议下沉到内核模块:

且尽可能保持了⼀样的使用体验。那这⼀致性协议下沉如何做到的?

⼀致性协议抽象

其实,⼀致性协议,就是用来保证数据⼀致的,而数据的产生,必然有⼀个写入的动作;同时还要能够读数据,并且保证读数据的动作以及得到的数据结果,并且能够得到⼀致性协议的保障。因此,⼀致性协议最最基础的两个方法,就是写动作和读动作

public interface ConsistencyProtocol extends CommandOperations {/** * Obtain data according to the request. */Response getData(ReadRequest request) throws Exception;/** * 同步数据提交,在 Datum 中已携带相应的数据操作信息 */Response write(WriteRequest request) throws Exception;}

任何使用⼀致性协议的,只需使用 getData 及 write 方法。

⼀致性协议抽象在 consistency 包,Nacos 对 AP、CP ⼀致性协议接口使用抽象都在,且实现具体⼀致性协议时,采用插件可插拔,进⼀步将⼀致性协议具体实现逻辑和服务注册发现、配置管理两个模块解耦。

package com.alibaba.nacos.core.distributed;/** * Conformance protocol management, responsible for managing the lifecycle of conformance protocols in Nacos. */@SuppressWarnings("all")@Component(value = "ProtocolManager")public class ProtocolManager extends MemberChangeListener implements DisposableBean {private void initAPProtocol() {ApplicationUtils.getBeanIfExist(APProtocol.class, protocol -> {Class configType = ClassUtils.resolveGenericType(protocol.getClass());Config config = (Config) ApplicationUtils.getBean(configType);injectMembers4AP(config);protocol.init(config);ProtocolManager.this.apProtocol = protocol;});}private void initCPProtocol() {ApplicationUtils.getBeanIfExist(CPProtocol.class, protocol -> {Class configType = ClassUtils.resolveGenericType(protocol.getClass());Config config = (Config) ApplicationUtils.getBean(configType);injectMembers4CP(config);protocol.init(config);ProtocolManager.this.cpProtocol = protocol;});}}

仅做完⼀致性协议抽象还不够:

  • 服务注册发现及配置管理还是要依赖⼀致性协议接口,在两个计算模块中耦合了带状态的接口
  • 虽然做了比较高度的⼀致性协议抽象,服务模块及配置模块却依然还是要在自己代码模块去显式处理⼀致性协议的读写请求逻辑
  • 需要自己去实现⼀个对接⼀致性协议的存储

服务发现及配置模块,更应专注数据使用及计算,而非数据咋存储、咋保障数据⼀致性,数据存储及多节点⼀致问题应交由存储层保证。

为了:

  • 降低⼀致性协议出现在服务注册发现及配置管理两模块的频次
  • 尽可能让⼀致性协议只在内核模块中感知

Nacos又做数据存储抽象。

5 数据存储抽象

⼀致性协议是为保证数据⼀致,如利用⼀致性协议实现存储,那服务模块以及配置模块,就由原来的依赖⼀致性协议接口转为依赖存储接口,而存储接口后面具体实现就比⼀致性协议丰富,且服务模块、配置模块也无需为直接依赖⼀致性协议而承担多余编码工作(快照、状态机实现、数据同步)。使这两模块可更专注自己核心逻辑。对于数据抽象,这里仅以服务注册发现模块为例:

package com.alibaba.nacos.core.storage.kv;public interface KvStorage {enum KvType {/** * Local file storage. */File,/** * Local memory storage. */Memory,/** * RocksDB storage. */RocksDB,}// ...}

由于 Nacos 的服务模块存储,更多是根据单或多个唯⼀ key 去执行点查,因此KV存储接口最适合。接口定义后,就是这KVStore具体实现。可直接将 KVStore 实现对接 Redis,DB或直接根据 Nacos 内核模块的⼀致性协议,在此基础之上,实现⼀个内存或者持久化的分布式强(弱)⼀致性 KV。

通过功能边界将 Nacos 进程进⼀步分离为计算逻辑层和存储逻辑层,计算层和存储层之间的交互仅通过薄薄数据操作胶水代码,就在单个 Nacos 进程里面实现计算、存储彻底分离。

存储层

进⼀步实现插件化的设计,对于中小公司且有运维成本要求的话,可以直接使用 Nacos 自带的内嵌分布式存储组件来部署⼀套 Nacos 集群。

服务实例数据以及配置数据的量级很大的话,并且本身有⼀套比较好的 Paas 层服务,那么完全可以复用已有的存储组件,实现 Nacos 的计算层与存储层彻底分离。

本文由博客一文多发平台 OpenWrite 发布!