如何理论上从零开始设计一个去中心化的分布式数据库集群

网络上有句流行语 : 集齐七颗龙珠,便可召唤神龙。那么问题来了,如果从零开始设计一套分布式去中心化的数据库集群需要多少颗“龙珠”呢?答案是 6 颗,对你没听错,不是 998 ,也不是 888 ,只需 6 颗龙珠,你也可以从理论上拥有一个私人订制的分布式去中心化的数据库集群系统,还犹豫什么吗,赶快往下看。

       怎么设计?我们需要从一个用户的角度去看问题,什么叫用户的角度,说白了就是一个key-value对象时如何存储到数据库中,然后如何从一个数据库中读取出来,最后这个数据库要实现一定的容灾能力。

整个流程就是按照一个key-value从客户端发送过来。第一,这个key-value对象的逻辑数据结构是怎么设计的,这就用到了龙珠one:key-value的逻辑数据结构设计,个人觉得列族模型相对灵活。第二,该对象是如何找到对应的服务节点的,用到了龙珠two:分发策略设计,主要是一致性哈希。第三,数据到了服务节点后在数据库内部是如何存储的并且当要查询某个key对应的value值时是如何快速实现的,这个用到了龙珠tree:存储和查找key-value设计,哈希函数的应用是关键。第四,这份数据是怎样落地到磁盘的,这就用到了龙珠four:持久化策略设计,绕不开的是重做日志。第五,由于整个集群是去中心化的,保持节点信息和生产数据的一致性就成为个难题,这就用到了龙珠five:节点间一致性设计,核心就是Gossip协议实现最终的一致性。第六,是不管是不是分布式数据库都配置的那就是备份和容灾策略,用到了龙珠six:备份和负载均衡设计,相对于多份副本都在一个集群上,我更倾向于通过主从集群进行备份。

 

龙珠one:key-value的逻辑数据结构设计(列族模型)

    大家都知道分布式数据库的基本的逻辑数据结构都是key-value键值对设计,区别只在于value值的数据结构不同,我们看看三个标本数据库是怎么设计的。

    Cassandra是列族模型,简单来说就是在key下面还存在另一层维度,那就是列,一个key下面有多个列(而且数量可以不固定),每个列对应一个value值,所以你可以吧多个列和这些列对应的value值作为一个整体当做是这个key对应的value值。

    Redis呢?redisvalue值的数据结构是可变的,可以直接是个string,也可以是个容器比如listset等,当value是个容器时,跟cassandra的列族的概念差不多,都是key对应有多条记录,每条记录还有自己的唯一标识。

    Hbasecassandra很类似,都是列族模型。就不再细说了。

    个人觉得从实际情况来看列族模型更灵活。

龙珠two:分发策略设计(一致性哈希算法或者它的变种)

       所谓分布式,那就是以为着数据要分散存储在不同主机不同节点上,所以任何分布式数据库首先要解决的就是如何把客户端发送过来的key-value对象按照一定的算法存储到相应的节点上。我们先看目前比较流行的3种分布式数据库是怎么做的,没接触过hbase,早上冲忙看了一眼,说的不对的地方请各位见谅。cassandra:一致性哈希。Redis:一致性哈希的变种,hbase:由master节点控制着key的分片机制。注意,hase是分布式数据库但不是去中心化,所以对这篇文章而言也就是个对比例子,没太多借鉴的理论,某种程度而言hbase数据库的理论体系是相对落后的,当然落后不一定不好,落后意味着相对简单,从而相对稳健。从这三种比较来看,一致性哈希是比较好的选择,什么是一致性哈希呢?(具体的请看上篇文章Redis分布式集群机制原理》,那里有详细而通俗的介绍,在这里就简单说一下:简单来说就是定义一个大数组,先把集群节点也映射到对应的数组下标中,等客户端发送过来的key-value对象发过来时,哈希映射到对应的数组下标中,最后根据就近原则,把离某个节点下标近的key-value对象按照顺时针的方向分发给该节点管理。一致性哈希的优点在于当节点进行增删时,数据迁移量会较小,只影响被增删的节点相领的节点,不影响其他节点的数据。而hbase使用的由master节点管理key-value对象的分发路由表,如果某个节点被移除或增加了,这张路由表就要全局更新,至于数据的迁移量有多大要由这个路由表的生成算法决定。但无论如何hbase必须依靠master节点进行分发管理,不符合去中心化的设计。所以龙珠one的选择就是“一致性哈希”。

 

龙珠tree:存储和查找key-value设计(哈希函数)

分布式数据库基本上都以key-value的形式作为逻辑上的数据结构存储,那么如何设计出根据key值快速访问到对应的value值就对数据库的性能就显得格外重要,以三个标本数据库来看看是怎么实现的。

Cassandra是通过Bloom过滤器和索引文件,data文件共同形成快速查找的能力。每个data文件对应着一个Bloom过滤器和一个索引文件,什么是Bloom过滤器呢?又得用到我们的杀手锏了,哈希算法。所谓的Bloom过滤器简单来说就是先定义一个大的数组,然后数组的每个元素都取01两种值,然后定义若干个比如8个哈希函数,然后对要存储的key值做8个哈希计算,最终得到8个结果值(指纹),然后把八个值对应下标的数组都取1,当下次这个key要被访问时,直接用这个key作为输入计算出八个指纹值,然后查看8个下标对应的数组元素是否都为1,如果都为1的话,那么说明可能存在于这个布隆器对应的存储文件中(注意,只是可能不是一定,为什么呢?因为存在哈希冲突),否则说明一定不存在。然后确定了这个存储文件可能存在这个key的数据时,下一步进入这个存储文件的索引文件中,由于data文件中的key-value对象时按照key值顺序存储的,所以可以通过以key为主键建立的索引文件进行二分查找进行快速定位。根据索引文件找到该key对应的value值在data文件 中的偏移量,最后找到相应的value值。

Redis因为是因为全量的生产数据都存放在内存中,所以快速查找设计相对简单,具体详细内容见上篇文章《redis数据结构进一步探析》,简单来说就是用了哈希表来实现,首先创建了一个一定长度的空的数组hash[n](注意是数组,不是链表,因为只有数据能做到随机访问而不需遍历,也就能达到时间要求是n(1)),由于数组是在内存中定长并且连续排列的,所以每个元素的偏移量都是可以根据下标直接计算得来,而不需遍历。然后对要存储的key-value对象的key做哈希计算,根据结果值存放到对应的数组下标中,如果出现哈希冲突的话就在加到同个数组下标的短链表后面,由于一般而言哈希函数选用得当的话出现哈希冲突的概率不是很大,所以可以认为所有的keyvalue们都均匀得放置在数组中,并且每个数组下标只放一个key-value对象。所以当我们根据key=7去访问对应的value值时,实际上的操作步骤1.7%4=3 步骤2:获取到hash[3],所以整个流程的速度非常快,效率几乎达到完美的n(1)

Hbase草草看了下,感觉跟cassandra的设计里面相似。所以对于分布式的物理上存放的数据结构基本上就上面两种分类,但是无论那种,哈希函数的应用都是至关重要的一点。

 

龙珠four:持久化策略设计(重做日志)

    数据库不可避免的问题是如何持久化,不管是所谓的内存数据库还是偏向于磁盘存储的数据库,持久化是个设计要点。看看三个标本数据库是怎样做持久化的。

Cassandra的持久化是通过写缓存和commitlog来实现,什么意思呢?就是当客户端往数据库中写数据时,集群服务端接收到数据后,先写commitlog日志记录下这条数据记录,然后往写缓存中写该条数据,当写缓存满后就冲刷到磁盘上以文件的形式存储。这样即使节点挂了,写缓存中的数据丢失了,也能根据commitlog来恢复,所谓的commitlogoracle的重做日志一样的原理。当写缓存中的数据已经冲刷到磁盘后,就可以清理掉commitlog里面相关的记录。

Redis作为一个内存数据库,是通过全量快照写磁盘和追加操作命令到重做日志的形式实现的。流程是这样的,一条数据过来,redis服务端拿到后先写到重做日志中,然后再修改内存中对应的key-value对象。当需要全量从内存中dump一份全量的数据就执行命令全量备份一份快照。

hbase貌似跟cassandra类似,也是通过写缓存和commitlog来实现。

通过分析三个标本数据库来看,持久化第一步就是得写重做日志,这个步骤是不能省的。然后再根据数据库的特性,设计写缓存以提高数据库的写速度。最后生成数据要全量下载一份到磁盘上保存起来。

就我个人而言,cassandra的持久化设计思想相对要好,虽然也要保存一份重做日志,但是可以根据已经持久化到磁盘上的数据的情况来清理掉没用的重做日志。因为重做日志的作用只是为了防止写缓存丢失的发生。所以如果要设计内存数据库,持久化策略可以参考redis的,如果是非内存数据库的话,持久化策略可以参考cassandra

 

龙珠five:节点间一致性设计(Gossip协议及其变种)

   去中心化的分布式数据库面临的一大挑战是在没有全局管理者也就是没有老大的情况下如何维持各个节点(小弟)的数据信息的最终一致性?并且要尽快短的时间达到一致。没有核心节点就会引发一个非常严重的问题,那就是各个节点之间的信息如何进行同步,最终达到一致性的呢?如果以前有核心,有老大,老大掌握最权威的全局信息数据,然后各个小弟有不明白的就去问老大,获取最权威的最新的数据,所有小弟有新的发现了也会报告给老大,所以老大的数据永远是最新的最权威的。但是有一天老大没了,小弟们该怎么办呢?一个笨办法就是每个小弟一有什么新数据都集体广播给其他小弟,然后其他小弟根据广播的信息对比下自己的数据,一发现有更新的数据了就把自己的旧数据更新成最新的。这个办法很有效,但是太笨了,笨到系统根本撑不住这么大量的网络广播数据量。所以这个办法不可行。

我们先看看三个标本数据库是如何实现的。CassandraGossip协议redisGossip协议hbase:非去中心化,由master节点统一管理节点信息,不存在这个问题。关于Gossip协议的详细具体介绍可见上篇文章《Redis分布式集群机制原理》,在这里简单说一下:Gossip协议哲学思想是世界万物事物之间必然存在某种关联。就如著名的六人定理:在现代社会中,无论身份地位相差多远的两个人总能通过6个人的关联取得联系。也就是你和奥巴马之间的关系很可能是只要你同学的亲戚的男朋友的前女友的邻居的大姨妈肯给你介绍,你就能跟奥巴马取得联系。在一个集群节点中,每个节点都随机地选择和一个(其他节点通信,先实现这两个节点的数据信息的一致性,经过几轮这种随机的通信,最终所有节点的状态都会达成一致。整个流程如下:节点A作为同步发起者准备好一份数据摘要,里面包含了A上数据的指纹。随机选择一个通讯节点B,节点B接收到摘要之后将摘要中的数据与本地数据进行比较,并将A有的但是B没有的数据差异封装成一个数据请求包a,同时把B有但是A没有的数据差异做成另一个数据包b, 最后把这两个数据包都返回给AA接受到数据请求包a和数据包bA根据b数据包中的数据更新A本身的旧数据并且把请求包a要的数据发送给B。B接收到A再次发来的数据后更新自己的旧数据。

    数据库的一致性包括两大块:一是节点信息数据的一致性(很好理解,就是每个节点的状态信息,是宕机了还是在干其他事,这个信息要全部节点都要知道,才能针对性地做动作,比如某个节点A挂了,其他节点都知道了,如果有一个key-value值数据从客户端过来B节点,本来应该是要转发存储到A节点的,但是B节点知道A挂了,于是自己先帮A保存起来,等A节点重启恢复后再转发给它。另一个就是当A节点正常从集群中移除后,其他节点要知道这个信息好重新动态分配一致性哈希的势力范围。)这种节点的信息量一般不大,没有太多复杂的数据结构好说,直接用Gossip协议传输信息包就行。另一大块比较麻烦,那就是生产数据的一致性。先看看三个标本是怎样实现的,这一点redishbase有点像,它们是用主从集群的方式进行实现备份策略的。所以对于它们而言主集群(注意是主集群而不是主节点)各个节点之间是不存在生产数据一致性的问题,因为每个key-value对象只存放在对应的一个节点上,所以是强一致性的。但是cassandra不一样,cassandra没有主从集群的概念,一个key-value对象可能在集群的各个节点间存放N份副本,那么问题来了,由于cassandra的写不是强一致性的,什么意思呢?就是对应cassandra写而言,在往集群中写一个key-value对象时可以指定成功写入N份副本中的某一份就返回写成功,这就出现了N份副本的生产数据版本不一致的情况,怎么办呢?首先协议还是用Gossip协议,但是传输的数据的数据结构要费点心思,因为如果传输的是该节点所有key-value对象最新版本的数据的话,那对网络io的压力就太恐怖了,每当这个时刻,我们的杀手锏哈希算法又出来救场了。Cassandra用了一种Merkle Tree,这是什么鬼呢?看起来很高大上的,其实说白就是一个最低层叶子节点由最新版本的key-value对象经过哈希公式计算得出的值,然后每一层的父节点就由下面的叶子节点的值经过哈希计算得出的数值组成的二叉树。这样就大大减少了网络传输的数据量,然后用这个作为数据包使用Gossip协议进行节点间的通讯,这样每个接收到这个数据包的其他节点,沿着最顶层的父节点逐渐向下面各个树杈进行一一比较,如果某一层的某个树杈的父节点是和自己相同的节点是一致的,那就说明这个父节点下面的子节点都不需再比较了,肯定是一样的,不需要进行一致性修复,如果某一层的某个树杈的父节点是和自己相同的节点是不同的,那就要继续往下面子节点比较下去,最后整个比较下来就能知道哪些key-value对象在这两个节点中版本存在不一致,接着就向数据包的发送包发送该节点找到的不一致的key-value对象的最新版本数据,并且要求发送方也发一份同样的key-value对象的最新版本数据给接收方,然后发送方和接收方都有了对方的不一致key-value对象数据,在根据key-value对象中的时间戳确定哪个版本是最新的,双方都更新到最新版本的数据。当然就是这样经过优化后的数据结构所携带的数据量也是很大的,所以这种一致性修复在cassandra中也是不能频繁进行的。

    个人觉得一般生产环境都数据的一致性要求都比较高,所以cassandra这种需要修复生产数据一致性的系统并不是太好的选择。个人相对觉得redis这种形式的主从集群备份,然后只使用Gossip协议同步节点间的信息而不同步节点间的生产数据这种设计相对比较可控。

 

龙珠six:备份和负载均衡设计(主从集群)

    其实备份策略在上面的龙珠two中已经说了很多,以三个标本数据库对比而言,cassandra是通过在集群中不同节点中存放若干个副本的形式实现,redishbase是通过主从集群的形式进行备份。备份本事其实除了起到容灾的功能外也同时负责了负载均衡的作用,道理很简单,备份数据本身就可以提供查询访问能力,所以就起着分流访问压力的功能。首先说说cassandra这种备份策略,第一眼看上去高大上,不过致命问题就是生产数据一致性同步比较麻烦。对于对数据的一致性要求很高的生产系统不适合使用。比如银行系统,我今天存了1百万,结果我再次查询时只剩下昨天余额的1千块,那客户还不疯了。相对而言redishbase的备份策略比较老土,但是主备数据的一致性收敛速度要快的多。所以我个人偏向redis的主从集群的备份机制。

 

 

    说到这里,好像所谓的去中心化分布式数据库的主要设计点就说完了,说实在的这个标题有点吓人,我个人也觉得诚惶诚恐,其实是标题党,吸引大家来看为的文章,我觉得这篇文章还真不可能指导你去从零开始设计一个去中心化的分布式数据库,虽然对设计一个去中心化的分布式数据库的所有要点都有涉及到,真要实现一个超简单的分布式数据库不是不可能,但是更重要的是从设计的角度去理顺了整个去中心化分布式数据库的难点和要点,希望对于大家更深入地理解去中心化分布式数据库有所帮助。文章主要目的是加深我个人的学习印象,如有偏颇错误的地方请大家见谅。有机会再继续修正。

    题外话,我现在只找到了6颗龙珠,还差一颗,希望大家有什么想法可以文明留言交流。

你可能感兴趣的:(大数据)