最近几年来,越来越多的文章介绍了 Raft 或者 Paxos 这样的分布式一致性算法,且主要集中在算法细节和日志同步方面的应用。但是呢,这些算法的潜力并不仅限于此,基于这样的分布式一致性算法构建一个完整的可弹性伸缩的高可用的大规模存储系统,是一个很新的课题,我结合我们这一年多以来在 TiKV 这样一个大规模分布式数据库上的实践,谈谈其中的一些设计和挑战。
本次分享的主要内容是如何使用 Raft 来构建一个可以「弹性伸缩」存储。其实最近这两年也有很多的文章开始关注类似 Paxos 或者 Raft 这类的分布式一致性算法,但是主要内容还是在介绍算法本身和日志复制,但是对于如何基于这样的分布式一致性算法构建一个大规模的存储系统介绍得并不多,我们目前在以 Raft 为基础去构建一个大规模的分布式数据库 TiKV ,在这方面积累了一些第一手的经验,今天和大家聊聊类似系统的设计,本次分享的内容不会涉及很多 Raft 算法的细节,大家有个 Paxos 或者 Raft 的概念,知道它们是干什么的就好。
先聊聊 Scale
其实一个分布式存储的核心无非两点,一个是 Sharding 策略,一个是元信息存储,如何在 Sharding 的过程中保持业务的透明及一致性是一个拥有「弹性伸缩」能力的存储系统的关键。如果一个存储系统,只有静态的数据 Sharding 策略是很难进行业务透明的弹性扩展的,比如各种 MySQL 的静态路由中间件(如 Cobar)或者 Twemproxy 这样的 Redis 中间件等,这些系统都很难无缝地进行 Scale。
Sharding 的几种策略
在集群中的每一个物理节点都存储若干个 Sharding 单元,数据移动和均衡的单位都是 Sharding 单元。策略主要分两种,一种是 Range 另外一种是 Hash。针对不同类型的系统可以选择不同的策略,比如 HDFS 的Datanode 的数据分布就是一个很典型的例子:
▌首先是 Range
Range 的想法比较简单粗暴,首先假设整个数据库系统的 key 都是可排序的,这点其实还是蛮普遍的,比如 HBase 中 key 是按照字节序排序,MySQL 可以按照自增 ID 排序,其实对于一些存储引擎来说,排序其实是天然的,比如 LSM-Tree 或者 BTree 都是天然有序的。Range 的策略就是一段连续的 key 作为一个 Sharding 单元:
例如上图中,整个 key 的空间被划分成 (minKey, maxKey),每一个 Sharding 单元(Chunk)是一段连续的 key。按照 Range 的 Sharding 策略的好处是临近的数据大概率在一起(例如共同前缀),可以很好的支持 range scan 这样的操作,比如 HBase 的 Region 就是典型的 Range 策略。
但是这种策略对于压力比较大的顺序写是不太友好的,比如日志类型的写入 load,写入热点永远在于最后一个 Region,因为一般来说日志的 key 基本都和时间戳有关,而时间显然是单调递增的。但是对于关系型数据库来说,经常性的需要表扫描(或者索引扫描),基本上都会选用 Range 的 Sharding 策略。
▌另外一种策略是 Hash
与 Range 相对的,Sharding 的策略是将 key 经过一个 Hash 函数,用得到的值来决定 Sharding ID,这样的好处是,每一个 key 的分布几乎是随机的,所以分布是均匀的分布,所以对于写压力比较大、同时读基本上是随机读的系统来说更加友好,因为写的压力可以均匀的分散到集群中,但是显然的,对于 range scan 这样的操作几乎没法做。
比较典型的 Hash Sharding 策略的系统如:Cassandra 的一致性 Hash,Redis Cluster 和 Codis 的 Pre-sharding 策略,Twemproxy 有采用一致性 Hash 的配置。
当然这两种策略并不是孤立的,可以灵活组合,比如可以建立多级的 Sharding 策略,最上层用 Hash ,每一个 Hash Sharding 中,数据有序的存储。
在做动态扩展的时候,对于 Range 模型的系统会稍微好做一些,简单来说是采用分裂,比如原本我有一个 [1, 100) 的 Range Region,现在我要分裂,逻辑上我只需要简单的将这个 region 选取某个分裂点,如分裂成 [1,50), [50, 100) 即可,然后将这两个 Region 移动到不同的机器上,负载就可以均摊开。
但是对于 Hash 的方案来说,做一次 re-hash 的代价是挺高的,原因也是显而易见,比如现在的系统有三个节点,现在我添加一个新的物理节点,此时我的 hash 模的 n 就会从 3 变成 4,对于已有系统的抖动是很大,尽管可以通过 ketama hash 这样的一致性 hash 算法尽量的降低对已有系统的抖动,但是很难彻底的避免。
Sharding 与高可用方案结合
选择好了 sharding 的策略,那剩下的就是和高可用方案结合,不同的复制方案达到的可用性及一致性级别是不同的。很多中间件只是简单的做了 sharding 的策略,但是并没有规定每个分片上的数据的复制方案,比如 redis 中间件 twemproxy 和 codis,MySQL 中间件 cobar 等,只是在中间层进行路由,并未假设底层各个存储节点上的复制方案。但是,在一个大规模存储系统上,这是一个很重要的事情,由于支持弹性伸缩的系统一般来说整个系统的分片数量,数据分片的具体分布都是不固定的,系统会根据负载和容量进行自动均衡和扩展,人工手动维护主从关系,数据故障恢复等操作在数据量及分片数量巨大的情况下几乎是不可能完成的任务。选择一个高度自动化的高可用方案是非常重要的。
在 TiKV 中,我们选择了按 range 的 sharding 策略,每一个 range 分片我们称之为 region,因为我们需要对 scan 的支持,而且存储的数据基本是有关系表结构的,我们希望同一个表的数据尽量的在一起。另外在 TiKV 中每一个 region 采用 Raft 算法在多个物理节点上保证数据的一致性和高可用。
从社区的多个 Raft 实现来看,比如 Etcd / LogCabin / Consul 基本都是单一 raft group 的实现,并不能用于存储海量的数据,所以他们主要的应用场景是配置管理,很难直接用来存储大量的数据,毕竟单个 raft group 的参与节点越多,性能越差,但是如果不能横向的添加物理节点的话,整个系统没有办法 scale。
scale 的办法说来也很简单,采用多 raft group,这就很自然的和上面所说的 sharding 策略结合起来了,也就是每一个分片作为一个 raft group,这是 TiKV 能够存储海量数据的基础。但是管理动态分裂的多 raft group 的复杂程度比单 group 要复杂得多,目前 TiKV 是我已知的开源项目中实现 multiple raft group 的仅有的两个项目之一。
正如之前提到过的我们采用的是按照 key range 划分的 region,当某一个 region 变得过大的时候(目前是 64M),这个 region 就会分裂成两个新的 region,这里的分裂会发生在这个 region 所处的所有物理节点上,新产生的 region 会组成新的 raft group。
总结
构建一个健壮的分布式系统是一个很复杂的工程,上面提到了在 TiKV 在实践中的一些关键的设计和思想,希望能抛砖引玉。因为 TiKV 也是一个开源的实现,作为 TiDB 的核心存储组件,最近也刚发布了 Beta 版本,代码面前没有秘密,有兴趣深入了解的同学也可以直接阅读源码和我们的文档,谢谢大家。
参考:https://mp.weixin.qq.com/s?__biz=MzI3NDIxNTQyOQ==&mid=2247483881&idx=1&sn=f42cf3e23cd229a7b12cbf5d2116ecf8&scene=21#wechat_redirect
网友评论