目录
一、⼀致性协议下沉
既然 Nacos 已经做到了将 AP、CP 协议下沉到了内核模块,而且尽可能的保持了⼀样的使用体验。那么这个⼀致性协议下沉,Nacos 是如何做到的呢?
1、⼀致性协议抽象
其实,⼀致性协议,就是用来保证数据⼀致的,而数据的产生,必然有⼀个写入的动作;同时还要能够读数据,并且保证读数据的动作以及得到的数据结果,并且能够得到⼀致性协议的保障。因此,⼀致性协议最最基础的两个方法,就是写动作和读动作
public interface ConsistencyProtocol<T extends Config, P extends RequestProcessor> exten
ds CommandOperations {
...
/**
* Obtain data according to the request. *
* @param request request
* @return data {@link Response}
* @throws Exception {@link Exception}
*/
Response getData(ReadRequest request) throws Exception;
/**
* Data operation, returning submission results synchronously. *
* @param request {@link com.alibaba.nacos.consistency.entity.WriteRequest}
* @return submit operation result {@link Response}
* @throws Exception {@link Exception}
*/
Response write(WriteRequest request) throws Exception;
...
}
任何使用⼀致性协议的,都只需要使用 getData 以及 write 方法即可。同时,⼀致性协议已经被抽象在了 consistency 的包中,Nacos 对于 AP、CP 的⼀致性协议接口使用抽象都在里面,并且在实现具体的⼀致性协议时,采用了插件可插拔的形式,进⼀步将⼀致性协议具体实现逻辑和服务注册发现、配置管理两个模块达到解耦的目的。
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 这里又做了另⼀份工作——数据存储抽象。
2、数据存储抽象
正如前面所说,⼀致性协议,就是用来保证数据⼀致的,如果利用⼀致性协议实现⼀个存储,那么服务模块以及配置模块,就由原来的依赖⼀致性协议接口转变为了依赖存储接口,而存储接口后面的具体实现,就比⼀致性协议要丰富得多了,并且服务模块以及配置模块也无需为直接依赖⼀致性协议而承担多余的编码工作(快照、状态机实现、数据同步)。使得这两个模块可以更加的专注自己的核心逻辑。对于数据抽象,这里仅以服务注册发现模块为例
public interface KvStorage {
enum KvType {
/**
* Local file storage.
*/
File,
/**
* Local memory storage.
*/
Memory,
/**
* LSMTree storage.
*/
LSMTree, AP, CP,
}
// 获取⼀个数据
byte[] get(byte[] key) throws KvStorageException;
// 存入⼀个数据
void put(byte[] key, byte[] value) throws KvStorageException;
// 删除⼀个数据
void delete(byte[] key) throws KvStorageException;
...
}
由于 Nacos 的服务模块存储,更多的都是根据单个或者多个唯⼀ key 去执行点查的操作,因此Key-Value 类型的存储接口最适合不过。而 Key-Value 的存储接口定义好之后,其实就是这个KVStore 的具体实现了。可以直接将 KVStore 的实现对接 Redis,也可以直接对接 DB ,或者直接根据 Nacos 内核模块的⼀致性协议,在此基础之上,实现⼀个内存或者持久化的分布式强(弱)⼀致性 KV。通过功能边界将 Nacos 进程进⼀步分离为计算逻辑层和存储逻辑层,计算层和存储层之间的交互仅通过⼀层薄薄的数据操作胶水代码,这样就在单个 Nacos 进程里面实现了计算和存储二者逻辑的彻底分离。
同时,针对存储层,进⼀步实现插件化的设计,对于中小公司且有运维成本要求的话,可以直接使用 Nacos 自带的内嵌分布式存储组件来部署⼀套 Nacos 集群,而如果服务实例数据以及配置数据的量级很大的话,并且本身有⼀套比较好的 Paas 层服务,那么完全可以复用已有的存储组件,实现 Nacos 的计算层与存储层彻底分离。
二、Nacos 自研 Distro 协议
1、背景
Distro 协议是 Nacos 社区自研的⼀种 AP 分布式协议,是面向临时实例设计的⼀种分布式协议,其保证了在某些 Nacos 节点宕机后,整个临时实例处理系统依旧可以正常工作。作为⼀种有状态的中间件应用的内嵌协议,Distro 保证了各个 Nacos 节点对于海量注册请求的统⼀协调和存储。
2、设计思想
Distro 协议的主要设计思想如下:
- Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。
- 每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据⼀致性。
- 每个节点独立处理读请求,及时从本地发出响应。
下面几节将分为几个场景进行 Distro 协议工作原理的介绍。
2.1、数据初始化
新加入的 Distro 节点会进行全量数据拉取。具体操作是轮询所有的 Distro 节点,通过向其他的机器发送请求拉取全量数据。
在全量拉取操作完成之后,Nacos 的每台机器上都维护了当前的所有注册上来的非持久化实例数据。
2.2、数据校验
在 Distro 集群启动之后,各台机器之间会定期的发送心跳。心跳信息主要为各个机器上的所有数据的元信息(之所以使用元信息,是因为需要保证网络中数据传输的量级维持在⼀个较低水平)。这种数据校验会以心跳的形式进行,即每台机器在固定时间间隔会向其他机器发起⼀次数据校验请求。
⼀旦在数据校验过程中,某台机器发现其他机器上的数据与本地数据不⼀致,则会发起⼀次全量拉取请求,将数据补齐。
2.3、写操作
对于⼀个已经启动完成的 Distro 集群,在⼀次客户端发起写操作的流程中,当注册非持久化的实例的写请求打到某台 Nacos 服务器时,Distro 集群处理的流程图如下。
整个步骤包括几个部分(图中从上到下顺序):
- 前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。
- 责任节点上的 Controller 将写请求进行解析。
- Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。
2.4、读操作
由于每台机器上都存放了全量数据,因此在每⼀次读操作中,Distro 机器会直接从本地拉取数据。快速响应。
这种机制保证了 Distro 协议可以作为⼀种 AP 协议,对于读操作都进行及时的响应。在网络分区的情况下,对于所有的读操作也能够正常返回;当网络恢复时,各个 Distro 节点会把各数据分片的数据进行合并恢复。
3、小结
Distro 协议是 Nacos 对于临时实例数据开发的⼀致性协议。其数据存储在缓存中,并且会在启动时进行全量数据同步,并定期进行数据校验。
在 Distro 协议的设计思想下,每个 Distro 节点都可以接收到读写请求。所有的 Distro 协议的请求场景主要分为三种情况:
- 当该节点接收到属于该节点负责的实例的写请求时,直接写入。
- 当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。
- 当该节点接收到任何读请求时,都直接在本机查询并返回(因为所有实例都被同步到了每台机器上)。
Distro 协议作为 Nacos 的内嵌临时实例⼀致性协议,保证了在分布式环境下每个节点上面的服务信息的状态都能够及时地通知其他节点,可以维持数十万量级服务实例的存储和⼀致性。