DDIA - 第6章 数据分区

信息是激发创新的力量

        本章目标: 数据分区。

第6章 数据分区

        将数据拆分成为分区,也称为分片
        实际上,每个分区都可以视为一个完整的小型数据库,虽然数据库可能存在一些跨分区的操作。采用数据分区的主要目的是提高扩展性。 不同的分区可以放在一个无共享集群的不同节点上。这样一个大数据集可以分散在更多的磁盘上,查询负载也随之分布到更多的处理器上。对单个分区进行查询时,每个节点对自己所在分区可以独立执行查询操作,因此添加更多的节点可以提高查询吞吐量。超大而复杂的查询尽管比较困难,但也可能做到跨节点的并行处理。

1 数据分区与数据复制

        分区通常与复制结合使用,即每个分区在多个节点都存有副本。一个节点上可能存储了多个分区,一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。

2 键-值数据的分区

        分区的主要目标是将数据和查询负载均匀分布在所有节点上。 而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或者查询负载,称之为倾斜。 这种负载严重不成比例的分区即成为系统热点。
        避免热点最简单的方法是将记录随机分配给所有节点上。这种方法可以比较均匀地分布数据,但是有一个很大的缺点:当试图读取特定的数据时,没有办法知道数据保存在哪个节点上,所以不得不并行查询所有节点。

2.1 基于关键字区间分区

        一种分区方式是为每个分区分配一段连续的关键字或者关键字区间范围(以最小值和最大值来指示)
        关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。为了更均匀地分布数据,分区边界理应适配数据本身的分布特征。
        每个分区内可以按照关键字排序保存。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。
        基于关键字嗯嗯区间分区的缺点是某些访问模式会导致热点。

2.2 基于关键字哈希值分区

        对于上述数据倾斜与热点问题,许多分布式系统采用了基于关键字哈希函数的方式来分区。一个好的哈希函数可以处理数据倾斜并使其均匀分布,许多编程语言内置的简单哈希函数可能并不适合分区。
        一旦找到合适的关键字哈希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键字范围),关键字根据其哈希值的范围划分到不同的分区中。这种方法可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择(在这种情况下,该技术有时被称为一致性哈希)。
        然而,通过关键字哈希进行分区,我们丧失了良好的区间查询特性。可以在这两种分区策略的折中,复合主键只有第一部分可用于哈希分区,而其他列则用作组合索引来对数据进行排序。

一致性哈希

        描述数据动态平衡的一种方法,一种平均分配负载的方法,最初用于内容分发网络(CDN)等互联网缓存系统。它采用随机选择的分区边界来规避中央控制或分布式共识。
        正如后面“分区再平衡”一节将要介绍的,这种特殊的分区方法对于数据库实际效果并不是很好,所以目前很少使用(虽然某些数据库的文档仍采用一致性哈希的术语,但其实并不准确)

2.3 负载倾斜与热点

        基于哈希的分区方法可以减轻热点,但无法做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。
        大多数的系统今天仍然无法自动消除这种高度倾斜的负载,而只能通过应用层来减轻倾斜程度。 例如,如果某个关键字被确认为热点,一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到100个不同的关键字上,从而分配到不同的分区上。
        但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有100个关键字中读取数据然后进行合并。因此通常只对少量的热点关键字附加随机数才有意义;而对于写入吞吐量低的绝大多数关键字,这些都意味着不必要的开销。此外,还需要额外的元数据来标记哪些关键字进行了特殊处理。

3 分区与二级索引

        我们之前讨论的键-值模型相对简单,但是如果涉及二级索引,情况会变得复杂。二级索引通常不能唯一标识一条记录,而是用来加速特定值的查询。
        二级索引是关系数据库的必备特性,在文档数据库中应用也非常普遍。但考虑到其复杂性,许多键-值存储(如HBase和Voldemort)并不支持二级索引;但其他一些如Riak则开始增加对二级索引的支持。此外,二级索引技术也是Solr和Elasticsearch等全文索引服务器存在之根本。
        二级索引带来的主要挑战是它们不能规整地映射到分区。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。

3.1 基于文档分区的二级索引

        在需要检索的字段上设定二级索引(在文档数据库中这些都是字段;在关系数据库中则是列)。声明这些索引之后,数据库会自动创建索引。
        在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中的数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档ID的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引
        查询过程是将查询发送到所有的分区,然后合并所有返回的结果。这种查询分区数据库的方法有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。大多数数据库供应商都建议用户自己来构建合适的分区方案,尽量由单个分区满足二级索引查询,但现实往往难以如愿,尤其是当查询中可能引用多个二级索引时(例如同时指定多个条件)

3.2 基于词条的二级索引分区

        另一种方法,我们可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。而且,为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。
        我们将这种索引方案称为词条分区,它以待查找的关键字本身作为索引。例如颜色color:red。名字词条源于全文检索(一种特定类型的二级索引),term指的是文档中出现的所有单词的集合。
        和前面讨论的方法一样,可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询(例如,查询汽车报价在某个值以上),而采用哈希的方式则可以更均匀的划分分区。
        这种全局的词条分区相比于文档分区索引的主要优点是,他的读取更为高效,即它不需要采用scatter/gather对所有的分区都执行一遍查询,相反,客户端只需要向包含词条的那一个分区发出读请求。然而全局索引的不利之处在于,写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引入显著的写放大。

4 分区再平衡

        随着时间的推移,数据库可能总会出现某些变化:

  • 查询压力增加,因此需要更多的CPU来处理负载
  • 数据规模增加,因此需要更多的磁盘和内存来存储数据
  • 节点可能出现故障,因此需要其他机器来接管失效的节点

        所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再平衡(或者动态平衡)。无论对于哪种分区方案,分区再平衡通常至少要满足:

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

4.1 动态再平衡的策略

        将分区对应到节点上存在多种不同的分配策略

4.1.1 为什么不用取模?

        前面提到将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。例如,区间[0,b0)对应于分区0,[b0,b1)对应分区1等。
        至于为什么不直接使用mod(许多编程语言里的取模运算符%)。对节点数取模方法的问题是,如果节点数N发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。(123456%N)这种频繁的迁移操作大大增加了再平衡的成本。
        因此我们需要一种减少迁移数据的方法

4.1.2 固定数量的分区

        首先,创建远超实际节点数的分区数,然后为每个节点分配多个分区。例如,对于一个10节点的集群,数据库可以从一开始就逻辑划分为1000个分区,这样大约每个节点承担100个分区。接下来,如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。如果从集群中删除节点,则采取相反的均衡措施。
        选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变,也不会改变关键字到分区的映射关系。这里唯一要调整的是分区与节点的对应关系。考虑到节点间通过网络传输数据总是需要时间,这样调整可以逐步完成,在此期间,旧的分区仍然可以接收读写请求。
        使用该策略时,分区的数量往往在数据库创建时就确定好,之后不会改变。原则上也可以拆分和合并分区,但固定数量的分区使得相关操作非常简单,因此许多采用固定分区策略的数据库决定不支持分区拆分功能。所以,在初始化时,已经充分考虑将来扩容增长的需求(未来可能拥有的最大节点数),设置一个足够大的分区数。而每个分区也有些额外的管理开销,选择过高的数字可能会有副作用。
        如果数据集的总规模高度不确定或可变(例如,开始非常小,但随着时间的推移可能会变得异常庞大),此时如何选择合适的分区数就有些困难。每个分区包含的数据量的上限是固定的,实际大小应该与集群中的数据总量成正比。如果分区里的数据量非常大,则每次再平衡和节点故障恢复的代价就很大;但是如果一个分区太小,就会产生太多的开销。分区大小应该“恰到好处”,不要太大,但不能过小,如果分区数量固定了但总数据量却高度不确定,就难以达到一个最佳取舍点。

4.1.3 动态分区

        对于采用关键字区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。
        动态分区: 当分区的数据增长超过一个可配的参数阈值,它就拆分为两个分区,每个承担一半的数据量。相反,如果大量数据被删除,并且分区缩小到某个阈值以下,则将其与相邻分区进行合并。该过程类似于B树的分裂操作。
        每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。对于HBase,分区文件的传输需要借助HDFS(底层分布式文件系统)
        动态分区的一个优点是分区数量可以自动适配数据总量。如果只有少量的数据,少量的分区就够了,这样系统开销很小;如果有大量的数据,每个分区的大小则被限制在一个可配的最大值。
        但是,需要注意的是,对于一个空的数据库,因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理,而其他节点则处于空闲状态,为了缓解这个问题,HBase和MongoDB允许在一个空的数据库上配置一组初始分区(这被称为预分裂)。对于关键字区间分区,预分裂要求已经知道一些关键字的分布情况。
        动态分区不仅适用于关键字区间分区,也适用于基于哈希的分区策略。

4.1.4 按节点比例分区

        动态分区策略,分区的数量与数据集的大小成正比关系;固定分区方式,每个分区的大小与数据集的大小成正比。两种情况,分区的数量都与节点数无关。
        Cassandra和Ketama则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时,当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系;当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方法也使每个分区大小保持稳定。
        当一个新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择可能会带来不太公平的分区分裂,但是当平均分区数量较大时,新节点最终会从现有节点中拿走相当数量的负载。Cassandra在3.0时退出了改进算法,可以避免上述不公平的分裂。
        随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)。这种方法也最符合本章开头所定义一致性哈希。一些新设计的哈希函数也可以以较低的元数据开销达到类似的效果。

4.2 自动与手动再平衡操作

        全自动式的再平衡(即由系统自动决定何时将分区从一个节点迁移到另一个节点,不需要任何管理员的介入)与纯手动方式(即分区到节点的映射由管理员来显式配置)之间,可能还会有一个过渡阶段。例如,Couchbase,Riak和Voldemort会自动生成一个分区分配的建议方案,但需要管理员的确认才能生效。
        全自动的方式是存在很大风险的,所以让管理员介入到再平衡可能是个更好的选择。它的确比全自动过程响应慢一些,但它可以有效防止意外发生。

5 请求路由

        服务发现问题的几种不同的处理策略:

  1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,则直接处理该请求;否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端
  2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器
  3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介
            不管哪种方法,核心问题是做出路由决策的组件(可能是某个节点,路由层或客户端)如何知道分区与节点的对应关系以及其变化情况?
            这其实是一个很有挑战性的问题,所有参与者都要达到共识这一点很重要。分布式系统中有专门的共识协议算法,但通常难以正确实现。可以依靠独立的协调服务(如ZooKeeper)跟踪集群范围内的元数据,或者是节点之间适用gossip协议来同步集群状态的变化。
            当使用路由层或随机选择节点发送请求时,客户端仍然需要知道目标节点的IP地址。IP地址的变化往往没有分区-节点变化那么频繁,采用DNS通常就足够了。

5.1 并行查询执行

        对于大规模并行处理(massively parallel processing,MPP)这一类主要用于数据分析的关系型数据库 ,在查询类型方面要复杂得多。典型的数据仓库查询包含多个联合、过滤、分组和聚合操作。MPP查询优化器会将复杂的查询分解成许多执行阶段和分区,以便在集群的不同节点上并行执行。尤其是涉及全表扫描这样的查询操作,可以通过并行执行获益颇多。
        数据仓库中快速并行执行查询可以作为单独的话题。

小结

        分区的目的是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。
        两种主要的分区方法:

  • 基于关键字区间的分区
  • 哈希分区

        混合上述两种基本方法也是可行的,例如使用复合键:键的一部分来标识分区,而另一部分来记录排序后的顺序。

        我们还讨论了分区与二级索引,二级索引也需要进行分区,有两种方法:

  • 基于文档来分区二级索引(本地索引)
            二级索引存储在与关键字相同的分区中,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上执行scatter/gather
  • 基于词条来分区二级索引(全局索引)
            它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。再写入时,不得不更新二级索引的多个分区;但读取时,则可以从单个分区直接快速提取数据

        最后,我们讨论了如何将查询请求路由到正确的分区,包括简单的分区感知负载均衡器,以及复杂的并行查询执行引擎。
        理论上,每个分区基本保持独立运行,这也是为什么我们试图将分区数据库分布、扩展到多台机器上。但是,如果写入需要跨多个分区,情况就会格外复杂,例如,如果其中一个分区写入成功,但另一个发生失败,接下来会发生什么?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值