背景
阿里云 InfluxDB®是阿里云基于开源版 InfluxDB 打造的一款时序数据库产品,提供更稳定的持续运行状态、更丰富强大的时序数据计算能力。在现有的单节点版本之外,阿里云 InfluxDB®团队还将推出多节点的高可用版本。
我们知道现有的开源版 InfluxDB 只提供单节点的能力,早期开源的集群版本功能不完善、且社区不再提供更新与支持。经过对官网商业版 InfluxDB 现有文档的研究,我们猜测在商业版 InfluxDB 集群方案中,meta 信息集群是基于一致性协议 Raft 做同步的,而数据是异步复制的。这种分离的方式虽然有优点,但也引起了一系列的一致性问题,在一些公开的文档中,官方也承认这种数据复制方案并不令人满意。
因此,团队在参考多项技术选型后,决定采用最为广泛使用并有较长历史积累的 ETCD/Raft 作为核心组件实现阿里云 InfluxDB®的 Raft 内核,对用户所有的写入或一致性读请求直接进行 Raft 同步(不做 meta 信息同步与数据写入在一致性过程中的拆分),保证多节点高可用版本拥有满足强一致性要求的能力。
有幸笔者参与到多节点的高可用版本的开发中,期间遇到非常多的挑战与困难。其中一项挑战是 ETCD 的 Raft 框架移植过程中,在移除了 ETCD 自身较为复杂、对时序数据库没有太多作用的 Raft 日志模块后,所带来的一系列问题。本文就业界 Raft 日志的几种不同实现方案做讨论,并提出一种自研的 Raft HybridStorage 方案。
业内方案
ETCD
由于我们采用了 ETCD/Raft 的方案,绕不开讨论一下 ETCD 本家的 Raft 日志实现方式。
官网对 Raft 的基本处理流程总结参考下图所示,协议细节本文不做扩展:
对于 ETCD 的 Raft 日志,主要包含两个主要部分:文件部分(WAL)、内存存储部分(MemoryStorage)。
文件部分(WAL),是 ETCD Raft 过程所用的日志文件。Raft 过程中收到的日志条目,都会记录在 WAL 日志文件中。该文件只会追加,不会重写和覆盖。
内存存储部分(MemoryStorage),主要用于存储 Raft 过程用到的日志条目一段较新的日志,可能包含一部分已共识的日志和一些尚未共识的日志条目。由于是内存维护,可以灵活的重写替换。MemoryStorage 有两种方式清理释放内存:第一种是 compact 操作,对 appliedId 之前的日志进行清理,释放内存;第二种是周期 snapshot 操作,该操作会创建 snapshot 那一时刻的 ETCD 全局数据状态并持久化,同时清理内存中的日志。
在最新的 ETCD 3.3 代码仓库中,ETCD 已经将 Raft 日志文件部分(WAL)和 Raft 日志内存存储部分(MemoryStorage)都抽象提升到了与 Raft 节点(Node)、Raft 节点 id 以及 Raft 集群其他节点信息(*membership.RaftCluster)平级的 Server 层级,这与老版本的 ETCD 代码架构有较大区别,在老版本中 Raft WAL 与 MemoryStorage 都仅仅只是 Raft 节点(Node)的成员变量。
一般情况下,一条 Raft 日志的文件部分与内存存储部分配合产生作用,写入时先写进 WAL,保证持久化;随之马上追加到 MemoryStorage 中,保证热数据的高效读取。
无论是文件部分还是内存存储部分,其存储的主要数据结构一致,都是 raftpb.Entry。一条 log Entry 主要包含以下几个信息:
参数 | 描述 |
---|---|
Term | leader 的任期号 |
Index | 当前日志索引 |
Type | 日志类型 |
Data | 日志内容 |
此外,ETCD Raft 日志的文件部分(WAL)还会存储针对 ETCD 设计的一些额外信息,比如日志类型、checksum 等等。
CockroachDB
CockroachDB 是一款开源的分布式数据库,具有 NoSQL 对海量数据的存储管理能力,又保持了传统数据库支持的 ACID 和 SQL 等,还支持跨地域、去中 心、高并发、多副本强一致和高可用等特性。
CockroachDB 的一致性机制也是基于 Raft 协议:单个 Range 的多个副本通过 Raft 协议进行数据同步。Raft 协议将所有的请求以 Raft Log 的形式串行化并由 Leader 同步给 Follower,当绝大多数副本写 Raft Log 成功后,该 Raft Log 会标记为 Committed 状态,并 Apply 到状态机。
我们来分析一下 CockroachDB Raft 机制的关键代码,可以很明显的观察到也是从鼻祖 ETCD 的 Raft 框架移植而来。但是 CockroachDB 删除了 ETCD Raft 日志的文件存储部分,将 Raft 日志全部写入 RocksDB,同时自研一套热数据缓存(raftentry.Cache),利用 raftentry.Cache 与 RocksDB 自身的读写能力(包括 RocksDB 的读缓存)来保证对日志的读写性能。
此外,Raft 流程中的创建 snapshot 操作也是直接保存到 RocksDB。这样实现的原因,个人推测是可能由于 CockroachDB 底层数据存储使用的就是 RocksDB,直接使用 RocksDB 的能力读写 WAL 或者存取 snapshot 相对简单,不需要再额外开发适用于 CockroachDB 特性的 Raft 日志模块了。
自研 HybridStorage
移除 snapshot
在阿里云 InfluxDB 多节点高可用方案实现过程中,我们采用了 ETCD/Raft 作为核心组件,根据移植过程中的探索与 InfluxDB 实际需要,移除了原生的 snapshot 过程。同时放弃原生的日志文件部分 WAL,而改用自研方案。
为什么移除 snapshot 呢?原来在 Raft 的流程中,为了防止 Raft 日志的无限增加,会每隔一段时间做 snapshot,早于 snapshot index 的 Raft 日志请求,将直接用 snapshot 回应。然而我们的单 Raft 环架构如果要做 snapshot,就是对整个 InfluxDB 做,将非常消耗资源和影响性能,而且过程中要锁死整个 InfluxDB,这都是不能让人接受的。所以我们暂时不启用 snapshot 功能,而是存储固定数量的、较多的 Raft 日志文件备用。
自研的 Raft 日志文件模块会周期清理最早的日志防止磁盘开销过大,当某个节点下线的时间并不过长时,其他正常节点上存储的日志文件如果充足,则足够满足它追取落后的数据。但如果真的发生单节点宕机太长,正常节点的日志文件已出现被清理而不足故障节点追取数据时,我们将利用 InfluxDB 的 backup 和 restore 工具,将落后节点还原至被 Raft 日志涵盖的较新的状态,然后再做追取。
在我们的场景下,ETCD 自身的 WAL 模块并不适用于 InfluxDB。ETCD 的 WAL 是纯追加模式的,当故障恢复时,正常节点要相应落后节点的日志请求时,就有必要分析并提取出相同 index 且不同 term 中那条最新的日志,同时 InfluxDB 的一条 entry 可能包含超过 20M 的时序数据,这对于非 kv 模式的时序数据库而言是非常大的磁盘开销。
HybridStorage 设计
我们自研的 Raft 日志模块命名为 HybridStorage,即意为内存与文件混合存取,内存保留最新热数据,文件保证全部日志落盘,内存、文件追加操作高度一致。
HybridStorage 的设计思路是这样的:
(1)保留 MemoryStorage:为了保持热数据的读取效率,内存中的 MemoryStorage 会保留作为热数据 cache 提升性能,但是周期清理其中最早的数据,防止内存消耗过大。
(2)重新设计 WAL:WAL 不再是像 ETCD 那样的纯追加模式、也不需要引入类似 RocksDB 这样重的读写引擎。新增的日志在 MemoryStorage 与 WAL 都会保存,WAL 文件中最新内容始终与 MemoryStorage 保持完全一致。
一般情况下,HybridStorage 新增不同 index 的日志条目时,需要在写内存日志时同时操作文件执行类似的增减。正常写入流程如下图所示:
当出现了同 index 不同 term 的日志条目的情况,此时执行 truncate 操作,截断对应文件位置之后一直到文件尾部的全部日志,然后重新用 append 方式写入最新 term 编号的日志,操作逻辑上十分清晰,不存在 Update 文件中间的某个位置的操作。
例如在一组 Raft 日志执行 append 操作时,出现了如下图所示的同 index(37、38、39)不同 term 的日志条目的情况。在 MemoryStorage 的处理方式是:找到对应 index 位置的内存位置(内存位置 37),并抛弃从位置 A 以后的全部旧日志占用的内存数据(因为在 Raft 机制中,这种情况下内存位置 37 以后的那些旧日志都是无效的,无需保留),然后拼接上本次 append 操作的全部新日志。在自研 WAL 也需要执行类似的操作,找到 WAL 文件中对应 index 的位置(文件位置 37),删除从文件位置 37 之后的所有文件内容,并写入最新的日志。如下图分析:
方案对比
ETCD 的方案,Raft 日志有 2 个部分,文件与内存,文件部分因为只有追加模式,因此并不是每一条日志都是有效的,当出现同 index 不同 term 的日志条目时,只有最新的 term 之后的日志是生效的。配合 snapshot 机制,非常适合 ETCD 这样的 kv 存储系统。但对于 InfluxDB 高可用版本而言,snapshot 将非常消耗资源和影响性能,而且过程中要锁死整个 InfluxDB。同时,一次 Raft 流程的一条 entry 可能包含超过 20M 的时序数据。所以这种方案不适合。
CockroachDB 的方案,看似偷懒使用了 RocksDB 的能力,但因其底层存储引擎也是 RocksDB,所以无何厚非。但对于我们这样需要 Raft 一致性协议的时序数据库而言,引入 RocksDB 未免过重了。
自研的 Raft HybridStorage 是比较符合阿里云 InfluxDB®的场景的,本身模块设计轻便简介,内存保留了热数据缓存,文件使用接近 ETCD append only 的方式,遇到同 index 不同 term 的日志条目时执行 truncate 操作,删除冗余与无效数据,降低了磁盘压力。
总结
本文对比了业内常见的两种 Raft 日志的实现方案,也展示了阿里云 InfluxDB®团队自研的 HybridStorage 方案。在后续开发过程中,团队内还会对自研 Raft HybridStorage 进行多项优化,例如异步写、日志文件索引、读取逻辑优化等等。也欢迎读者提出自己的解决方案。相信阿里云 InfluxDB®团队在技术积累与沉淀方面会越做越好,成为时序数据库技术领导者。
本文作者:德施
阅读原文
本文为云栖社区原创内容,未经允许不得转载。