Design

整体架构

集群架构

arch

整个集群由placedriver + 数据节点datanode + etcd + rsync组成. 各个节点的角色如下:

PD node: 负责数据分布和数据均衡, 协调集群里面所有的zankv node节点, 将元数据写入etcd

datanode: 负责存储具体的数据

etcd: 负责存储元数据, 数据分布情况以及其他用于协调的元数据

rsync: 用于传输snapshot备份文件

数据节点架构

datanode

数据节点datanode有多个不同的分区组成, 每个分区是一个raftgroup. 每个分区由协议层redis, 日志同步层raftlog, 数据映射层和数据存储层构成.

redis协议层: 负责读取和解析客户端的redis命令, 并提交到raft层同步

raft日志同步层: 负责raft副本同步, 保证数据一致性

数据映射层: 负责将redis数据结构编码映射成db层的kv形式, 保证符合redis的数据结构语意

数据存储层: 负责封装rocksdb的kv操作.

数据节点会往etcd注册节点信息, 并且定时更新, 使得placedriver可以通过etcd感知自己.

placedriver节点

placedriver负责指定的数据分区的节点分布, 还会在某个数据节点异常时, 自动重新分配数据分布. 总体上负责调度整个集群的数据节点的元数据. placedriver本身并不存储任何数据, 关于数据分布的元数据都是存储在etcd集群, 可以有多个placedriver作为读负载均衡, 多个placedriver通过etcd选举来产生一个master进行数据节点的分配和迁移任务. placedriver节点会watch集群的节点变化来感知整个集群的数据节点变化.

placedriver提供了数据分布查询接口, 供客户端sdk查询对应的数据分区所在节点.

数据分区的分布

目前数据分区算法是通过hash分片实现的, 分区算法是namespace级别的, 因此不同namespace可以使用不同的分区算法. 某条数据请求读写hash分区的过程如下:

hash partition

其中, 分区到数据节点的映射表, 会根据算法生成后写入etcd, 生成算法需要保证如下几点

为了满足以上目标, 在计算映射表时通过以下方式:

数据平衡

在数据节点发生变化时, 需要动态的修改分区到数据节点的映射表, 动态调整映射表的过程就是数据平衡的过程. 数据节点变化时会触发etcd的watch事件, placedriver会实时监测数据节点变化, 来判断是否需要做数据平衡. 为了避免影响线上服务, 可以设置数据平衡的允许时间区间.

为了避免频繁发生数据迁移, 节点发生变化后, 会根据紧急情况, 判断数据平衡的必要性. 特别是在数据节点升级过程中, 可以避免不必要的数据迁移. 考虑以下几种情况:

稳定集群节点数默认只会增加, 每次发现新的数据节点, 就自动增加, 节点异常不会自动减少. 如果稳定集群节点数需要减少, 则需要调用API设置:

POST /stable/nodenum?number=xx

维护稳定集群节点总数, 是为了避免集群网络分区时不必要的数据迁移. 当集群正常节点数小于等于稳定节点数一半时, 自动数据迁移将停止.

HA流程

服务端

go-sdk处理

动态namespace

分布式索引

由于分布式系统中, 一个索引会有多个分片, 为了保证多个分片之间的数据一致性, 需要协调多个分片的索引创建流程. 基本流程如下:

index-create

跨机房集群

zankv目前支持两种跨机房部署模式, 分别适用于不同的场景

单个跨多机房集群模式

此模式, 部署一个大集群, 并且都是同城机房, 延迟较小, 一般是3机房模式. 部署此模式, 需要保证每个副本都在不同机房均匀分布, 从而可以容忍单机房宕机后, 不影响数据的读写服务, 并且保证数据的一致性.

部署时, 需要在配置文件中指定当前机房的信息, 用于数据分布时感知机房信息.增加如下配置项:

"tags": {"dc_info":"dc1"}

不同机房的数据节点, 使用不同的dc_info配置, placedriver进行副本配置时, 会保证每个分区的几个副本都均匀分布在不同的dc中.

跨机房的集群, 通过raft来完成各个机房副本的同步, 发生单机房故障时, 由于另外2个机房拥有超过一半的副本, 因此raft的读写操作可以不受影响, 且数据保证一致. 等待故障机房恢复后, raft自动完成故障期间的数据同步, 使得故障机房数据在恢复后能保持同步.此模式在故障发生和恢复时都无需任何人工介入, 保证单机房故障的可用性的同时, 数据一致性也得到保证.

多个机房内集群同步模式

如果是异地机房, 使用跨机房单集群部署方式, 可能会带来较高的同步延迟, 使得读写的延迟都大大增加. 为了优化延迟问题, 可以使用异地机房同步模式. 由于异地机房是后台异步同步的, 异地机房不影响本地机房的延迟, 但同时引入了数据同步滞后的问题, 在故障时可能会发生数据不一致的情况.

此模式的部署方式稍微复杂一些, 具体的部署和故障处理可以参考运维指南.基本原理是通过在异地机房增加一个raft learner节点, 通过raft异步的拉取log然后重放到异地机房集群. 由于每个分区都是一个独立的raft group, 因此分区内是串行回放, 各个分区间是并行回放raft log. 异地同步机房默认是只读的, 如果主机房发生故障需要切换时, 可能发生部分数据未同步, 需要在故障恢复后根据raft log进行人工修复.

数据过期功能设计

为了满足各种场景下的业务需求,ZanKV设计了多种不同的数据过期策略。在不同的数据过期策略下,ZanKV对数据过期有不同的支持和表现。 目前,ZanKV支持的数据过期策略有: - 一致性同步过期 - 非一致性本地删除

与redis不同的是,ZanKV只支持秒级的数据过期,不支持pexpire指令(毫秒级别的数据过期)。

一致性同步过期

当ZanKV集群的数据过期策略配置为:一致性同步过期时。所有的数据过期操作都由leader发起,通过raft协议进行删除操作。

在实际操作中,过期数据处理逻辑层每间隔1s会调用存储层接口,获取当前已经过期的数据key和类型。为减少集群node间网络交互的次数,逻辑层会对相同类型的key通过一条指令、一次raft交互进行批量删除。

使用该策略时,数据过期删除需要集群节点之间频繁的网络交互,在大量数据过期的情况下,会大大增加网络的负担和leader节点的压力。该策略适用于:过期数据量不大,业务方依赖于数据的TTL进行业务逻辑处理。

但是,在一般情况下,业务方并不会依赖于数据的TTL进行业务逻辑处理。相反的,业务方关注的是,数据需要保持一定的时间(半年、三个月、一周等),过了保存期,这些数据可以自动失效、删除,即在业务上数据已经没有用了,业务方不会再次访问或者关注这些数据,存储集群可以自行删除数据。在这种情况下,采用非一致性本地删除策略更加的合适。

非一致性本地删除

在非一致性本地删除的策略下,数据删除操作由各个集群节点自己进行,删除操作不通过raft协议进行。

具体的:数据删除操作由存储层自动进行,数据处理逻辑层不需要参与。ZanKV集群中的每个节点每间隔5min会扫描一次过期数据,并对扫描出的待删除数据进行本地直接删除。

相比于一致性同步过期策略,该策略具有以下特性:

更大量的数据过期支持。过期数据删除不需要集群节点之间的通信,可以避免在大量数据过期情况下造成的网络和性能处理瓶颈,进而支撑更大量的数据过期服务。 具有更高的数据吞吐量。该策略考虑到用户不关注于具体的TTL时间,优化了存储数据的编码格式,减少了数据存储时的写放大,提供更好的性能支持。 数据过期扫描间隔为5min。保证数据一定不会被提前删除,但是不保证数据删除的实时性,业务方不应该依赖于数据是否还存在进行业务逻辑处理。 用户无法通过TTL指令获取数据生存时间,也不支持Persist指令对过期数据进行持久化。这是为了提高吞吐量,提供更大量的数据过期支持所做的trade off。

前缀清理删除

非一致性删除虽然提供了更好的性能, 但是面对非常海量的过期数据时, 依然会产生大量的本地删除操作, 增加底层rocksdb的压力, 为了进一步减少删除过期的影响, 对于具有时效性特点的数据, ZanKV支持按照前缀清理. 这种模式下, 业务方的数据都是时效性的, 比如监控数据或者日志数据, 写入时业务会在key前面加上时间戳前缀. 使用删除API的时候, 指定业务前缀, 可以删除特定时间内的所有数据, 大大提高删除的效率. 前缀删除的API底层使用了rocksdb的DelRange方法, 此方法相比大量Delete操作, 对rocksdb的压力大大减少.