又开了一个新的坑,笔者工作之后维护着一个 NoSQL 数据库。而笔者维护的数据库正是基于社区版本的 Aerospike打造而来。所以这个踩坑系列的文章属于工作总结型的内容,会将使用开发 Aerospike 的各种问题进行总结梳理,希望能够给予大家启发和帮助。第一篇开山之文,就先从Aerospike 公司在16年数据库顶会 VLDB的一篇论文 《Aerospike: Architecture of a Real Time Operational DBMS》展开,来高屋建瓴的审视一下 Aeropike 的设计思路,来看看如何Aerospike这款分布式数据库有什么亮点值得我们学习借鉴的,由于论文发布在2016年,笔者完成这篇文章时Aerospike的版本已经发布到4.5了,很多最新的实现与老论文已经有些不同了,这点希望大家理解。准备好,老司机发车了~~
1.AeroSpike 的定位与场景
从论文的题目出发,这篇文章的核心在于实时操作数据库的架构,在论文引言之中对Aerospike的定位是一个高性能分布式数据库,用于处理实时的交互式在线服务。所以说,大多数使用Aerospike的场景是实时决策系统,它们有海量的数据规模,并且有严格的SLA要求,同时是百万级别的 QPS,具有ms的查询时延。显然,这样的场景使用传统的 RDMS 是不现实的,在论文之中,提到 Aerospike 的一个典型的应用场景,广告推荐系统,我们来一起看看它们是如何契合的:
众所周知,广告推荐系统这样的应用场景需要极高的吞吐量、低延迟和稳定的可用性。同时,广告推荐系统具有随时间增加其数据使用量以提高其推荐的质量的趋势,即,在固定时间量中可访问的数据越多,推荐就越精确。下图展示了一个广告推荐系统是如何结合 Aerospike来提供推荐服务的:
显然,这就是笔者之前的文章之中聊到的典型的Lambda架构,笔者当时正是以广告推荐系统进行举例的。所以在这里笔者就不展开再聊Aerospike在其中充当的实时流存储的角色了,感兴趣的朋友可以看这里。
2.Aerospike的总体架构
除了广告推荐系统之外,论文的原文还介绍了许多关于Aerospike的适用场景,有兴趣的可以通过原文深入了解。接下来我们直奔主题,来看看Aerospike的总体架构:
由上图所示,Aerospike核心分为三个层次:
- 客户端层
- 分布式层
- 数据层
所以接下来我们来一一解构,Aerospike的各个层次。
2.1 分布式层
与Cassandra类似的是,Aerospike也采用了P2P的架构,也就是说,集群之中不存在的中心节点,每个节点都是对等的结构。而分布式层聚焦在两点之上:
- 节点分布
- 数据分布
2.1.1 节点分布
节点需要处理节点成员关系,并对Aerospike集群当前成员达成共识。比如:网络故障和节点加入或离开。
节点分布所关心的点在于:
- 集群中的所有节点到达当前集群成员的单一一致视图。
- 自动检测新节点的加入与离开。
- 检测网络故障并且能够容忍网络的不稳定性。
- 尽量缩短集群成员变化的时间。
2.1.1.1 集群视图
每个Aerospike节点都会自动分配一个唯一的节点标识符,它是其MAC地址和监听端口唯一确定的。集群整体视图由一个元组定义:
- cluster_key是随机生成的8字节值标识一个唯一的集群视图
- succession_list 是一个集合,标识了所有属于集群的Aerospike节点
cluster_key标识当前集群成员身份状态,并在每次集群视图更改时更改。 它使得Aerospike节点用于区分两个不同的集群视图。对集群视图的更改都对集群的性能有着有着显著影响,这意味着需要快速检测节点加入/离开,并且随后需要存在有效的一致性机制来处理对集群视图的更改。
2.1.1.2 节点检测
节点的加入或离开是通过不同节点之间定期交换的心跳消息来检测的。集群中的每个节点都维护一个邻接列表,该列表是最近向节点发送心跳消息的节点列表。如果在配置的超时间隔内,由于没有收到对应的心跳消息,从邻近列表中删除对应的节点。
而节点检测机制需要保证:
- 避免由于零星和短暂的网络故障而将节点误删除出集群。
- 防止不稳定节点频繁加入和离开集群。
辅助心跳
在阻塞的网络中,有可能任意丢失某些数据包。因此,除了常规的心跳消息之外,节点还使用了定期交换的其他消息作为备选的辅助心跳机制。例如,副本写可以用作心跳消息的辅助。这确保了,只要节点之间的主要或次要心跳通信是完整的,仅主心跳信息的丢失不会引起集群视图的变更。
健康检测
集群中的每个节点可以通过计算平均消息丢失来评估其每个节点的健康评分,健康评分是通过:每个节点接收的预期消息数量与每个节点接收的实际消息数量的加权平均值计算而成的。
设t为心跳消息的发送间隔,w为心跳信息的发送频率,r为在这个窗口时间中丢失的心跳消息的数量,α是一个比例因子,la(prev)之前的健康因子。la(new)为更新之后的健康因子,所以它的计算方式如下图所示:
健康因子在所有节点标准差两倍的节点是异常值,并且被认为是不健康的。如果不健康的节点是集群的成员,则将其从集群中删除。如果不是成员,则直到其平均消息丢失在可容忍的限度内才能加入集群。在实践中,α被设置为0.95,节点的历史表现比赋予了更多的权重。窗口时间一般设置为1秒。
2.1.1.3 视图更改
对邻近列表的更改就会产生新集群视图,这需要一次Paxos一致性算法。邻接链表之中节点标识符最高的节点充当Paxos提议者,如果建议被接受,节点就开始重新分配数据。
Aerospike实现了最小化集群由于单一故障事件而更改视图的次数。例如,有故障的网络交换机可能使集群成员的子集不可到达。一旦恢复了网络,就需要将这些节点添加到集群中。如果每个丢失或加入的节点都需要触发创建新的集群视图,这种代价是很高的。所以Aerospike仅在固定的集群更改间隔(间隔本身的时间是可配置的)开始时做出集群视图的调整。这里的想法是避免如心跳子系统检测到的那样对节点到达和离开事件反应太快,而是用一个集群视图更改来处理一批节点加入或删除的事件。这避免了由重复的集群视图更改和数据分布导致的大量潜在开销。集群更改间隔等于节点超时值的两倍,确保在单个间隔中明确检测到由于单个网络故障而失败的所有节点。
2.2 数据分布
Aerospike使用RipeMD160算法将record的key散列为160字节的digest,digest被划分为4096个分区。分区是Aerospike中最小的数据分布单元,根据key 的digest为记录分配分区。即使key的分布是倾斜的,在digest空间中分布也是均匀的,它有助于避免在数据访问期间创建热点,这有助于系统的容错。
一个好的数据分布需要满足下列条件:
- 存储负载均匀地分布在集群中,
- 具有较好的扩展性
- 节点出现变化时,数据的重新平衡是非破坏性的
数据分配算法为每个分区生成一个副本列表。副本列表中的第一个节点是该分区的主节点,其余的节点是副本。在默认情况下,所有读/写都通过副本的主节点。Aerospike支持任意数量的副本,(通常设置为两副本,笔者在实际使用中也是两副本)。 Aerospike 采取的是一致性哈希的分片分配的方式,当节点出现失效或宕机的情况时。这个节点可以从副本列表中删除,而后续节点的左移。如下图所示,如果该节点需要承载了数据的副本,则需要将此分区中的记录复制到新节点。一旦原始节点返回并再次成为集群的一部分,它将简单地重新获得其在分区复制列表中的位置。向集群中添加一个全新的节点将具有将此节点插入各个分区副本列表中的某个位置的效果。因此,将导致每个分区的后续节点的右移,而新节点左侧的分配不受影响。
上面的讨论给出了算法就能确保副本的最低迁移成本。但是当一个节点被删除并重新加入集群时,它需要和其他副本进行同步。当一个全新的节点加入一个拥有大量现有数据的集群,所以新的节点需要获得对应分区中所有记录的全新副本,并且还能够处理新的读写操作。接下来我们来看看副本同步的机制:
2.2.1 数据迁移
将record从一个节点移动到另一个节点的过程称为迁移。在每次集群视图改变之后,就需要进行数据迁移。每个分区的主副本为对应的分区分配唯一的分区版本,这个版本号会被复制到各个副本中。在集群视图更改之后,节点之间交换分区的分区版本和数据。
2.2.1.1 增量迁移
Aerospike使用增量迁移的方式优化迁移的速度。如果在能够在分区版本上建立总顺序,那么数据迁移的过程将更加有效。例如,如果节点1上的分区版本的值小于节点2上的相同分区版本的值,则节点1上的分区版本可能被丢弃。但是,通过分区版本号的排序是有问题的,因为网络分区引起的集群分裂会引起分区版本的冲突。
所以当两个版本冲突时,节点需要协商实际记录中的差异,并通过只对应于两个分区版本之间的差异的数据发送。在某些情况下,可以根据分区版本顺序完全避免迁移。在其他情况下,如滚动升级,可以传递增量的数据,而不是迁移整个分区。
- 迁移流程中的读写
如果分区正在进行迁移时,如果此时对应的分区有读写,主副本会读取所有的分区版本,协调出一个最终胜出的版本用于读或写事务。(按照笔者对文章的理解,这个流程会涉及多个副本,是一个耗时的操作) - 没有数据的主副本
新添加到正在运行的集群的空节点成为了主副本,并且没有对应分区的数据,没有任何数据的分区的副本被标记为处于DESYNC状态。Aerospike会指定一个最多记录的分区版本作为这个分区的代理主副本。所有的读操作都会指向代理主副本。(此时写还是在主副本上)如果客户端可以容忍读取旧版本的记录,则可以减少协调胜出版本的损耗。此代理主副本的工作会持续到对应分区的迁移完成。 -
迁移顺序
-
小分区优先
让分区版本中记录最少的分区开始迁移。这种策略可以快速减少特定分区的不同副本的数量。随着迁移的完成,延迟会改善,需要进行协调副本版本会减少对应的节点进行的通信。-
热分区优先
根据分区的 qps 的大小确认分区迁移的顺序。这种策略的目标与小分区优先的逻辑是一致的。
-
热分区优先
-
2.2.2 快速重启
节点重新启动是很常见的场景,比如:服务升级,宕机重启等。Aerospike的索引是内存中的而没有存储在持久设备上。在节点重新启动时,需要通过扫描持久设备上的记录来重新构建索引。(这个过程巨慢无比,笔者目前维护的大集群,单机存储数据量达1T,单次启动需要30分钟之久)
为了避免在每次重新启动时重新构建索引,Aerospike的利用了共享内存来实现快速重启。(目前开源的版本是不支持这个功能的,笔者所在的团队通过二次开发实现了对应的功能。但是机器一旦重启之后,也必须重建索引,所以有机器频繁重启的,可以考虑一些对应索引进行落盘)
2.3 客户端层
2.3.1 服务发现
在Aerospike中,每个节点维护着一个邻接列表标识着全局的节点分布情况。客户端从一个种子节点,发现整个集群的节点。
每个客户端进程都将集群分区映射的信息存储在共享内存之中。为了保持信息最新,客户端进程定期通过AeroSpike节点,来检查集群是否有任何变动。它通过根据服务器的最新版本检查本地存储的版本来实现这一点。对于单机的多个客户端,AeroSpike将数据存储在共享内存之中,并且用跨进程的互斥代码来实现集群信息的共享。
####2.3.2 连接管理
对于每个集群节点,在初始化时,客户端需为节点创建一个内存结构,并存储其分区映射,并且为节点维护连接。一旦出现节点和客户端的网络问题,这种频繁的内存调整容易产生性能问题。所以Aerospike客户端实现以下策略:
####2.3.2.1 健康计数
为了避免由于偶尔的网络故障导致上文的问题。当客户端连接集群节点操作发生问题时,会对集群节点进行故障计数。当故障计数超过特定阈值时,客户端才会删除集群节点。对集群节点的成功操作可以将故障计数重置为0。
####2.3.2.2 节点咨询
网络的故障通常很难复杂。在某些极端情况下,集群节点可以彼此感知,但是客户端不能直接感知到集群节点X。在这些情况下,客户端连接集群之中所有可见节点,并咨询集群之中的所有节点在其邻接列表中是否包含X。如果没有包含,则客户端将等待一个阈值时间,永久移除X节点。
3 跨数据中心同步
3.1.1 失效接管
在正常状态下(即,当没有故障时),每个节点只将节点上主副本的数据传送到远程集群。只在节点出现故障时才使用从副本。如果一个节点出现失效,所有其他节点能够检测到,并代表失效的节点接管工作。
3.1.2 数据传输优化
当发生写操作时,主副本在日志之中记录。进行数据传输时,首先读取一批日志,如果同一个记录有多个更新,选取一批之中最近的更新记录。一旦选取了记录,将其与实际记录比较。如果日志文件上的记录小于实际的记录,则跳过该记录。对于但是跳过记录的次数有一个上限,因为如果记录不断更新,那么可能永远不会推送记录。当系统中存在频繁更新记录的热键时,这些优化提供了巨大的好处。
4 存储落地
4.1 存储管理
Aerospike的存储层是一个混合模型,其中索引存储在内存中(不持久),数据可以选择存储在持久存储(SSD)或内存之中。而随机的读写SSD容易产生写放大。(笔者之前的文章也同样聊过这个问题,可以参考这里)为了避免在SSD的单个块上产生不均匀的磨损,Aerospike采取了批量写的方式。当更新记录时,从SSD读取旧记录,并将更新后的副本写入缓冲区。当缓冲区在充满时刷新到SSD上。
读取单元RBLOCKS的大小是128字节。而WBLOCK的大小,可配置,通常为1MB。这样的写入优化了磁盘寿命。Aerospike通过Hash函数在多个设备上切分数据来操作多个设备。这允许并行访问多个设备,同时避免任何热点。
4.2 Defragmentation垃圾清理
Aerospike通过运行后台碎片整理进程来回收空间。每个设备对应的块都存在填充因子。块的填充因子写入在块中。系统启动时,存储系统载入块中的填充因子,并在每次写入时保持更新。当块的填充因子低于阈值时,块成为碎片整理的候选者,然后排队等待碎片整理。
块进行碎片整理时,将读取有效记录并将其移动到新的写入缓冲区,当写入缓冲区已满时,将其刷新到磁盘。为了避免混合新写和旧写,Aerospike维护两个不同的写缓冲队列,一个用于普通客户端写,另一个用于碎片整理。
设置一个较高的阈值(通常为50%)会导致设备不断的刷写。而较低的设置会降低磁盘的利用率。所以基于可立即被写入可用磁盘空间,调整碎片整理速率以确保有效的空间利用。
4.3 性能与调优
4.3.1 Post Write Queue
Aerospike没有维护LRU缓存,而是维护的post write queue。这是最近写入的数据缓存,这个缓存不需要额外的内存空间。post write queue提高了缓存命中率,并减少了存储设备上的I/O负载。
5.小结
关于论文之中对Aerospike的设计笔者已经夹带私货的阐述清晰了。而关于单机优化和Aerospike性能测试,笔者就不再赘述了,感兴趣的可以回到论文之中继续一探究竟。对于论文之中的细节想要进一步的了解,可以继续关注笔者后续关于Aerospike的拆坑手记~~~