【分布式—要点】数据分区

采用数据分区的主要目的是提升可扩展性。不一样的分区能够放在一个无共享集群的不一样节点上。这样一个大数据集能够分散在更多的磁盘上,查询负载也随之分布到更多的处理器上。算法

对单个分区进行查询时,每一个节点对本身所在分区能够独立执行查询操做,所以添加更多的节点能够提升查询吞吐量。超大而复杂的查询尽管比较困难,但也可能作到跨节点的并行处理。数据库

分区一般与复制结合使用,即每一个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而一样的内容会保存在不一样的节点上以提升系统的容错性。网络

一个节点上可能存储了多个分区。每一个分区都有本身的主副本,而从副本则分配在其余一些节点。一个节点可能便是某些分区的主副本,同时又是其余分区的从副本。负载均衡

键-值数据的分区

如今假设数据是简单的键-值数据模型,这意味着老是能够经过关键字来访问记录。异步

基于关键字区间分区

一种分区方式是为每一个分区分配一段连续的关键字或者关键字区间范围(以最小值和最大值来指示)。若是知道关键字区间的上下限,就能够轻松肯定哪一个分区包含这些关键字。若是还知道哪一个分区分配在哪一个节点,就能够直接向该节点发出请求。分布式

关键字的区间段不必定非要均匀分布,这主要是由于数据自己可能就不均匀。分区边界能够由管理员手动肯定,或者由数据库自动选择。每一个分区内能够按照关键字排序保存,这样能够轻松支持区间查询,即将关键字做为一个拼接起来的索引项从而一次查询获得多个相关记录。例如,对于一个保存网络传感器数据的应用系统,选择测量的时间戳(年-月-日-时-分-秒)做为关键字,此时区间查询会很是有用,它能够快速得到某个月分内的全部数据。函数

然而,基于关键字的区间分区的缺点是某些访问模式会致使热点。若是关键字是时间戳,则分区对应于一个时间范围,例如天天一个分区。然而,当测量数据从传感器写入数据库时,全部的写入操做都集中在同一个分区(即当天的分区),这会致使该分区在写入时负载太高,而其余分区始终处于空闲状态。性能

为了不上述问题,须要使用时间戳之外的其余内容做为关键字的第一项。例如,能够在时间戳前面加上传感器名称做为前缀,这样首先由传感器名称,而后按时间进行分区。假设同时有许多传感器处于活动状态,则写入负载最终会比较均匀地分布在多个节点上。接下来,当须要获取一个时间范围内、多个传感器的数据时,能够根据传感器名称,各自执行区间查询。大数据

基于关键字哈希值分区

对于上述数据倾斜与热点问题,许多分布式系统采用了基于关键字哈希函数的方式来分区。网站

一个好的哈希函数能够处理数据倾斜并使其均匀分布。一旦找到合适的关键字哈希函数,就能够为每一个分区分配一个哈希范围(而不是直接做用于关键字范围),关键字根据其哈希值的范围划分到不一样的分区中。

这种方法能够很好地将关键字均匀地分配到多个分区中。分区边界能够是均匀间隔,也能够是伪随机选择(在这种状况下,该技术有时被称为一致性哈希)。

然而,经过关键字哈希进行分区,咱们丧失了良好的区间查询特性。即便关键字相邻,但通过哈希以后会分散在不一样的分区中,区间查询就失去了原有的有序相邻的特性

负载倾斜与热点

如前所述,基于哈希的分区方法能够减轻热点,但没法作到彻底避免。一个极端状况是,全部的读/写操做都是针对同一个关键字,则最终全部请求都将被路由到同一个分区。

这种负载或许并不广泛,但也并不是不可能:例如,社交媒体网站上,一些名人用户有数百万的粉丝,当其发布一些热点事件时可能会引起一场访问风暴,出现大量的对相同关键字的写操做(其中关键字多是名人的用户ID,或者人们正在评论的事件ID)。此时,哈希起不到任何帮助做用,由于两个相同ID的哈希值仍然相同。

大多数的系统今天仍然没法自动消除这种高度倾斜的负载,而只能经过应用层来减轻倾斜程度。例如,若是某个关键字被确认为热点,一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就能够将关键字的写操做分布到100个不一样的关键字上,从而分配到不一样的分区上。

可是,随之而来的问题是,以后的任何读取都须要些额外的工做,必须从全部100个关键字中读取数据而后进行合并。所以一般只对少许的热点关键字附加随机数才有意义;而对于写入吞吐量低的绝大多数关键字,这些都意味着没必要要的开销。此外,还须要额外的元数据来标记哪些关键字进行了特殊处理。

分区与二级索引

在分区方案设计中,若是涉及二级索引,状况会变得复杂。二级索引一般不能惟一标识一条记录,而是用来加速特定值的查询。二级索引是关系数据库的必备特性,在文档数据库中应用也很是广泛。

二级索引带来的主要挑战是它们不能规整的地映射到分区中。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。

基于文档的二级索引

在这种索引方法中,每一个分区彻底独立,各自维护本身的二级索引,且只负责本身分区内的文档而不关心其余分区中数据。每当须要写数据库时,包括添加,删除或更新文档等,只须要处理包含目标文档ID的那一个分区。所以文档分区索引也被称为本地索引,而不是全局索引。

但读取时须要注意:除非对文档ID作了特别的处理,不然不太可能全部特定查询条件的数据都放在一个分区中。所以须要将查询发送到全部的分区,而后合并全部返回的结果。

这种查询分区数据库的方法有时也称为分散/汇集,显然这种二级索引的查询代价高昂。即便采用了并行查询,也容易致使读延迟显著放大。

基于词条的二级索引

另外一种方法,咱们能够对全部的数据构建全局索引,而不是每一个分区维护本身的本地索引。并且,为避免成为瓶颈,不能将全局索引存储在一个节点上,不然就破坏了设计分区均衡的目标。因此,全局索引也必须进行分区,且能够与数据关键字采用不一样的分区策略。

和前面讨论的方法同样,能够直接经过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是能够支持高效的区间查询;而采用哈希的方式则能够更均匀的划分分区。

这种全局的词条分区相比于文档分区索引的主要优势是,它的读取更为高效,即它不须要采用scatter/gather对全部的分区都执行一遍查询,相反,客户端只须要向包含词条的那一个分区发出读请求。然而全局索引的不利之处在于,写入速度较慢且很是复杂,主要由于单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能彻底不一样甚至在不一样的节点上,由此势必引入显著的写放大

实践中,对全局二级索引的更新每每都是异步的。

分区再平衡

迁移负载的过程称为再平衡(或者动态平衡)。不管对于哪一种分区方案,分区再平衡一般至少要知足:

  • 平衡以后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。
  • 再平衡执行过程当中,数据库应该能够继续正常提供读写服务。
  • 避免没必要要的负载迁移,以加快动态再平衡,并尽可能减小网络和磁盘I/O影响。

动态再平衡的策略

为何不用取模?

对节点数取模方法的问题是,若是节点数N发生了变化,会致使不少关键字须要从现有的节点迁移到另外一个节点。这种频繁的迁移操做大大增长了再平衡的成本。

固定数量的分区

有一个至关简单的解决方案:首先,建立远超实际节点数的分区数,而后为每一个节点分配多个分区。例如,对于一个10节点的集群,数据库能够从一开始就逻辑划分为1000个分区,这样大约每一个节点承担100个分区。

接下来,若是集群中添加了一个新节点,该新节点能够从每一个现有的节点上匀走几个分区,直到分区再次达到全局平衡。若是从集群中删除节点,则采起相反的均衡措施。

选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变,也不会改变关键字到分区的映射关系。这里惟一要调整的是分区与节点的对应关系。考虑到节点间经过网络传输数据老是须要些时间,这样调整能够逐步完成,在此期间,旧的分区仍然能够接收读写请求。

原则上,也能够将集群中的不一样的硬件配置因素考虑进来,即性能更强大的节点将分配更多的分区,从而分担更多的负载。

若是数据集的总规模高度不肯定或可变,此时如何选择合适的分区数就有些困难。每一个分区包含的数据量的上限是固定的,实际大小应该与集群中的数据总量成正比。若是分区里的数据量很是大,则每次再平衡和节点故障恢复的代价就很大;可是若是一个分区过小,就会产生太多的开销。分区大小应该“恰到好处”,不要太大,也不能太小,若是分区数量固定了但总数据量却高度不肯定,就难以达到一个最佳取舍点。

动态分区

对于采用关键字区间分区的数据库,若是边界设置有问题,最终可能会出现全部数据都挤在一个分区而其余分区基本为空,那么设定固定边界、固定数量的分区将很是不便,而手动去从新配置分区边界又很是繁琐。

所以,一些数据库如HBase等采用了动态建立分区。当分区的数据增加超过一个可配的参数阈值,它就拆分为两个分区,每一个承担一半的数据量。若是大量数据被删除,而且分区缩小到某个阈值如下,则将其与相邻分区进行合并。该过程相似于B树的分裂操做。

每一个分区老是分配给一个节点,而每一个节点能够承载多个分区,这点与固定数量的分区同样。当一个大的分区发生分裂以后,能够将其中的一半转移到其余某节点以平衡负载。

动态分区的一个优势是分区数量能够自动适配数据总量。若是只有少许的数据,少许的分区就足够了,这样系统开销很小;若是有大量的数据,每一个分区的大小则被限制在一个可配的最大值。

按节点比例分区

采用动态分区策略,拆分和合并操做使每一个分区的大小维持在设定的最小值和最大值之间,所以分区的数量与数据集的大小成正比关系。另外一方面,对于固定数量的分区方式,其每一个分区的大小也与数据集的大小成正比。两种状况,分区的数量都与节点数无关。

Cassandra和Ketama则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每一个节点具备固定数量的分区。此时,当节点数不变时,每一个分区的大小与数据集大小保持正比的增加关系;当节点数增长时,分区则会调整变得更小。较大的数据量一般须要大量的节点来存储,所以这种方法也使每一个分区大小保持稳定。

当一个新节点加入集群时,它随机选择固定数量的现有分区进行分裂,而后拿走这些分区的一半数据量,将另外一半数据留在原节点。随机选择可能会带来不太公平的分区分裂,可是当平均分区数量较大时,新节点最终会从现有节点中拿走至关数量的负载。

随机选择分区边界的前提要求采用基于哈希分区(能够从哈希函数产生的数字范围里设置边界)。这种方法也最符合本章开头所定义一致性哈希。一些新设计的哈希函数也能够以较低的元数据开销达到相似的效果。

请求路由

归纳来说,这个问题有如下几种不一样的处理策略(分别如图所示的三种状况):

  1. 容许客户端链接任意的节点(例如,采用循环式的负载均衡器)。若是某节点刚好拥有所请求的分区,则直接处理该请求;不然,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。
  2. 将全部客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层自己不处理任何请求,它仅充一个分区感知的负载均衡器。
  3. 客户端感知分区和节点分配关系。此时,客户端能够直接链接到目标节点,而不须要任何中介。

image.png

这实际上是一个颇有挑战性的问题,全部参与者都要达成共识这一点很重要。不然请求可能被发送到错误的节点,而没有获得正确处理。分布式系统中有专门的共识协议算法,但一般难以正确实现。

许多分布式数据系统依靠独立的协调服务(如ZooKeeper)跟踪集群范围内的元数据。每一个节点都向ZooKeeper中注册本身,ZooKeeper维护了分区到节点的最终映射关系。其余参与者(如路由层或分区感知的客户端)能够向ZooKeeper订阅此信息。一旦分区发生了改变,或者添加、删除节点,ZooKeeper就会主动通知路由层,这样使路由信息保持最新状态。

Cassandra和Riak则采用了不一样的方法,它们在节点之间使用gossip协议来同步群集状态的变化。请求能够发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增长了数据库节点的复杂性,可是避免了对ZooKeeper之类的外部协调服务的依赖。

当使用路由层或随机选择节点发送请求时,客户端仍然须要知道目标节点的IP地址。IP地址的变化每每没有分区-节点变化那么频繁,采用DNS一般就足够了。

相关文章
相关标签/搜索