2
0
Fork 0
mirror of https://github.com/Vonng/ddia.git synced 2026-06-21 00:47:05 +08:00
ddia/ddia/ch6.md
2018-02-08 18:29:00 +08:00

41 KiB
Raw Blame History

6. 分片

我们必须跳出电脑指令序列的窠臼。 叙述定义、描述元数据、梳理关系,而不是编写过程。

—— Grace Murray Hopper未来的计算机及其管理1962


[TOC]

在第5章中我们讨论了复制 - 即在不同节点上有相同数据的多个副本。对于非常大的数据集或非常高的查询吞吐量这是不够的我们需要将数据拆分成分区也称为sharding1

术语澄清

我们在这里称之为分区partition的东西在MongoDBElasticsearch和Solr Cloud中被称为分片shard在HBase中称之为区域RegionBigtable中的 表块tabletCassandra和Riak中虚节点vnode以及Couchbase中的虚桶vBucket。但是**分区partition**是最重要的术语,所以这里坚持使用它。

通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)只属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。

要分区数据的主要原因是可扩展性。不同的分区可以放在不共享的集群中的不同节点上(请参阅第二部分关于无共享架构的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。

对于在单个分区上运行的查询,每个节点可以独立执行对其自己的分区的查询,因此可以通过添加更多的节点来缩放查询吞吐量。大型,复杂的查询可能会跨越多个节点进行并行处理,尽管这会变得非常困难。

分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近又被NoSQL数据库和基于Hadoop的数据仓库重新发明。有些系统是为事务性工作负载设计的其他系统则用于分析请参阅第90页上的“事务处理或分析这种差异会影响系统的调整方式但是分区的基本原理适用于这两种工作负载。

在本章中,我们将首先介绍分割大型数据集的不同方法,并观察数据索引如何与分区交互。然后,我们将讨论重新平衡,如果您想要添加或删除群集中的节点,则必须进行重新平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。

分片与复制

图6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。

键值数据的分片

假设你有大量的数据,你想分割它。你如何决定在哪些节点上存储哪些记录?

我们的分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点都公平分享那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量目前忽略复制

如果分区是不公平的那么一些分区比其他分区有更多的数据或查询我们称之为偏斜。歪斜的存在使分区效率下降得多。在极端的情况下所有的负载都可能在一个分区上所以10个节点中有9个是空闲的你的瓶颈就是单个的繁忙节点。一个负载不均衡的分区被称为热点。

避免热点的最简单方法是将记录随机分配给节点。这将在整个节点上平均分配数据,但是它有一个很大的缺点:当你试图读取一个特定的项目时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。

我们可以做得更好。现在让我们假设您有一个简单的键值数据模型,其中您总是通过其主键访问记录。例如,在一篇老式的纸质百科全书中,你可以通过标题来查找一个条目;由于所有条目按字母顺序排序,因此您可以快速找到您要查找的条目。

根据键的范围分片

分区的一种方法是为每个分区分配一个连续的键范围从最小值到最大值如纸百科全书的卷图6-2。如果知道范围之间的界限则可以轻松确定哪个分区包含给定的键。如果您还知道哪个分区分配给哪个节点那么您可以直接向相应的节点发出请求或者在百科全书的情况下从书架上选取正确的书籍

图6-2 印刷版百科全书按照关键字范围进行分区

键的范围不一定均匀分布因为您的数据可能不均匀分布。例如在图6-2中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据分区边界需要适应数据。

分区边界可以由管理员手动选择也可以由数据库自动选择我们将在第209页的“重新平衡分区”中更详细地讨论分区边界的选择。 Bigtable使用了这种分区策略其开源的HBase [23]RethinkDB和2.4版本之前的MongoDB [4][4]。

在每个分区中我们可以按照排序的顺序保存键请参见第70页上的“SSTables和LSM-树”。这具有范围扫描非常简单的优点您可以将键作为连接索引来处理以便在一个查询中获取多个相关记录请参阅第79页的“多列索引”。例如考虑存储来自传感器网络的数据的应用程序其中关键是测量的时间戳年 - 月 - 日 - 时 - 分 - 秒)。范围扫描在这种情况下非常有用,因为它们让您轻松获取某个月份的所有读数。

然而,关键范围分区的缺点是某些访问模式会导致热点。 如果密钥是时间戳,则分区对应于时间范围,例如,每天一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态[5]。

为了避免传感器数据库中的这个问题,您需要使用除时间戳以外的其他内容作为密钥的第一个元素。 例如,您可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。

根据键的哈希分片

由于这种倾斜和热点的风险,许多分布式数据存储使用散列函数来确定给定密钥的分区。

一个好的散列函数需要偏斜的数据并使其均匀分布。假设你有一个带有字符串的32位散列函数。无论何时给它一个新的字符串它将返回一个0到232-1之间的表面上的随机数。即使输入的字符串非常相似它们的散列也会均匀分布在这个数字范围内。

对于分区目的来说散列函数不需要密码强壮例如Cassandra和MongoDB使用MD5Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数因为它们用于哈希表但是它们可能不适合分区例如在Java的Object.hashCode和Ruby的Object哈希中同一个键可能有不同的过程中不同的哈希值[6]。

一旦你有一个合适的密钥散列函数你可以为每个分区分配一个散列范围而不是一系列的密钥每个散列落在分区范围内的密钥将被存储在该分区中。如图6-3所示。

图6-3 按哈希键分区

这种技术擅长在分区之间分配密钥。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时被称为一致性散列)。

一致性哈希 一致性哈希由Karger等人定义。[7] 是一种平均分配负载的方法通过内容分发网络CDN等互联网系统的缓存。 它使用随机选择的分区边界来避免中央控制或分布式共识的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性请参阅第7章无关而是描述了重新平衡的特定方法。

正如我们将在第209页的“重新平衡分区”中所看到的这种特殊的方法对于数据库实际上并不是很好所以在实际中很少使用某些数据库的文档仍然指的是一致性哈希但是它 往往是不准确的)。 因为这太混乱了,所以最好避免使用一致性哈希这个术语,而只是把它称为散列分区(hash partitioning)。

不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性执行高效范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们的排序顺序就会丢失。在MongoDB中如果您启用了基于散列的分片模式则任何范围查询都必须发送到所有分区[4]。主键上的范围查询不受Riak [9]Couchbase [10]或Voldemort的支持。

Cassandra在两个分区策略之间达成了一个折衷[11,12,13]。 Cassandra中的表可以使用由多个列组成的复合主键来声明。只有该密钥的第一部分被散列来确定分区而其他列则被用作Cas- sandra的SSTables中排序数据的连接索引。因此查询无法在组合键的第一列中搜索一系列值但如果为第一列指定了固定值则可以对该键的其他列执行有效的范围扫描。

串联索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为user_idupdate_timestamp,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。

负载倾斜与消除热点

如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个密钥的,所有的请求都会被路由到同一个分区。

这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴[14]。这个事件可能导致大量写入同一个密钥密钥可能是名人的用户ID或者人们正在评论的动作的ID。哈希键不起作用因为两个相同ID的哈希值仍然是相同的。

如今大多数数据系统无法自动补偿这种高度偏斜的工作负载因此应用程序有责任减少偏斜。例如如果一个密钥被认为是非常热的一个简单的方法是在密钥的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将写入密钥分散到100个不同的密钥中从而允许这些密钥分配到不同的分区。

然而在不同的密钥之间进行分割任何读取都必须要做额外的工作因为他们必须从所有100个密钥中读取数据并将其合并。此技术还需要额外的簿记只为少量热键附加随机数是有意义的;对于写入吞吐量低的绝大多数密钥,这将是不必要的开销。因此,您还需要一些方法来跟踪哪些键被分割。

也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要考虑自己的应用程序的权衡。

分片与次级索引

到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。

如果涉及二级索引情况会变得更加复杂另见“其他索引结构”在第85页。辅助索引通常不能唯一地标识记录而是搜索特定值的发生的方式查找用户123的所有操作查找包含词语hogwash的所有文章查找所有颜色为红色的车辆等等上。

二级索引是关系数据库的吃饭家伙在文档数据库中也是通用的。许多键值存储如HBase和Volde-mort由于增加了实现的复杂性而避免了二级索引但是一些如Riak已经开始添加它们因为它们对于数据建模非常有用。最后二级索引是Solr和Elasticsearch等搜索服务器的存在理由。

二级索引的问题是它们不能整齐地映射到分区。有两种主要的方法可以用二级索引分区数据库:基于文档的分区和基于词的分区。

按文档的二级索引

例如假设您正在经营一个销售二手车的网站如图6-4所示。 每个列表都有一个唯一的ID--称之为文档ID--并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等

你想让用户搜索汽车,允许他们通过颜色和通过过滤,所以你需要一个二级索引的颜色和(在文档数据库中这些将是字段(field),在关系数据库中,他们将是列(column) )。 如果您声明了索引,则数据库可以自动执行索引。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目colorred的文档ID列表中。

图6-4 按文档分区二级索引

在这种索引方法中每个分区是完全独立的每个分区维护自己的二级索引仅覆盖该分区中的文档。它不关心哪些数据存储在其他分区中。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因文档分区索引也被称为本地索引而不是全局索引在下一节中描述

但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。

这种查询分区数据库的方法有时被称为分散/聚集,并且可能会使二级索引的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大请参阅第16页的“实践中的百分比”。然而它被广泛使用MonDBDBRiak [15]Cassandra [16]Elasticsearch [17]SolrCloud [18]和VoltDB [19]都使用文档分区二级索引。大多数数据库供应商建议您构建分区方案,以便可以从单个分区提供二级索引查询,但这并不总是可行,尤其是当您在单个查询中使用多个二级索引时(例如按颜色并通过在同一时间)。

根据Term的二级索引

我们可以构建覆盖所有分区数据的全局索引,而不是每个分区都有自己的二级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但可以与主键索引进行不同的分区。

图6-5 按术语对二级索引进行分区

图6-5说明了这可能是什么情况来自所有分区的红色汽车在索引中显示为红色索引中的红色但索引是分区的以便从字母a到r开始的颜色出现在分区0中颜色以s开始z出现在第1部分。汽车制造商的指数也是相似的分区边界在f和h之间

我们将这种索引术语称为分割术语,因为我们期待的术语决定了索引的分割。在这里,例如,一个术语将是颜色:红色。名称术语来自全文索引(一种特定的二级索引),其中术语是文档中出现的所有单词。

和以前一样,我们可以通过术语本身来划分索引,或者使用术语的散列。通过术语本身进行划分对于范围扫描是有用的(例如,数字特性,例如汽车的要价),而对术语的哈希进行划分给出了负载的更均匀的分布。

全局(术语分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是分散/收集所有分区,客户端只需要向包含术语的分区发出请求它想要的。但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。

在理想的世界里索引总是最新的写入数据库的每个文档都会立即反映在索引中。但是在分区索引中这将需要跨所有受写入影响的分区进行分布式事务这在所有数据库中都不受支持请参阅第7章和第9章

在实践中对全局二级索引的更新通常是异步的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB指出在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会经历更长的传播延迟[20]。

全局术语分区索引的其他用途包括Riak的搜索功能[21]和Oracle数据仓库它允许您在本地索引和全局索引之间进行选择[22]。我们将回到第12章中实施分词二级索引的主题。

平衡分区

在数据库中,随着时间的推移,事情也在起变化。

  • 查询吞吐量增加所以您想要添加更多的CPU来处理负载。
  • 数据集大小增加所以您想添加更多的磁盘和RAM来存储它。
  • 机器出现故障,其他机器需要接管故障机器的责任。

所有这些更改都要求数据和请求从一个节点移动到另一个节点。 从集群中的一个节点向另一个节点移动负载的过程称为重置。 无论使用哪种分区方案,重新平衡通常都会满足一些最低要求:

  • 重新平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
  • 重新平衡正在发生时,数据库应该继续接受读取和写入。
  • 节点之间不应移动超过所需的数据以便快速重新配置并尽量减少网络和磁盘I / O负载。

平衡策略

有几种不同的分区分配方式[23]。让我们依次简要讨论一下。

别这样做hash mod N

我们在前面说过图6-3最好将可能的散列分成不同的范围并将每个范围分配给一个分区例如如果0≤散列则将键分配给分区0如果b0≤散列关键字<b1则等于b0等等

也许你想知道为什么我们不使用mod许多编程语言中的运算符。例如hashkeymod 10会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。

mod N方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设hashkey= 123456。如果最初有10个节点那么这个密钥从节点6开始因为123456 mod 10 = 6。当您增长到11个节点时密钥需要移动到节点3123456 mod 11 = 3当您增长到12个节点时需要移动到节点0123456 mod 12 = 0。这种频繁的举动使得再平衡过于昂贵。

我们需要一种不需要移动数据的方法。

固定数量的分区

幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。 现在如果一个节点被添加到集群中新节点可以从每个现有节点中窃取几个分区直到分区再次公平分配。这个过程如图6-6所示。如果从集群中删除一个节点则会发生相反的情况。

只有整个分区在节点之间移动。分区的数量不会改变,也不会将分配的密钥分配给分区。唯一改变的是将分区分配给节点。这种分配的改变并不是即时的 - 在网络上传输大量的数据需要一些时间 - 所以旧的分区分配被用于在传输过程中发生的任何读写操作。

图6-6 将新节点添加到每个节点具有多个分区的数据库群集。

原则上,您甚至可以解释集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点分担更多的负载。

在Riak [15]Elasticsearch [24]Couchbase [10]和Voldemort [25]中使用了这种重新平衡的方法。

在这种配置中,分区的数量通常在数据库第一次建立时是固定的,之后不会改变。虽然原则上可以拆分和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区拆分。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够高的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太高的数字是适得其反的。

如果数据集的总大小是高度可变的(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含总数据的固定部分,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,重新平衡和从节点故障恢复变得昂贵。但是,如果份额太小,则会产生太多的开销。当分区大小“恰到好处”,既不会太大,也不会太小,如果分区数量固定但数据集大小不一,则难以达到最佳性能。

动态分区

对于使用键范围分区的数据库请参阅第202页的“按键范围分区”具有固定边界的固定数量的分区将非常不方便如果出现边界错误则可能会导致所有一个分区中的数据和所有其他分区中的数据为空。手动重新配置分区边界将非常繁琐。 出于这个原因关键的范围分区数据库如HBase和RethinkDB动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB它被分成两个分区因此大约一半的数据在split [26]的每一端结束。相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似请参阅第79页上的“B-树”)。 每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase的情况下分区文件的传输通过HDFS底层分布式文件系统来实现[3]。 动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值[23]。 但是需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。虽然数据集很小直到达到第一个分区的分割点时所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为预分割。在键范围分区的情况下预分割要求您已经知道密钥分配将如何看起来像[4,26]。 动态分区不仅适用于关键的范围分区数据而且也适用于散列分区数据。从版本2.4开始MongoDB同时支持键范围和哈希分区并且在任何情况下动态分割分区。

按比例分配节点

通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。 Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 - 换句话说,每个节点具有固定数量的分区[23,27,28]。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小相当稳定。 当一个新节点加入集群时它随机选择一个固定数量的现有分区进行拆分然后占用这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分裂但是当在更大数量的分区上进行平均时在Cas- sandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种可重用的算法来避免不公平的分裂[29]。 随机选择分区边界要求使用基于散列的分区(所以可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性散列的原始定义[7]请参阅第204页的“一致性散列”。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果[8]。

运维:手动还是自动平衡

关于我们已经掩盖的重新平衡问题有一个重要问题:重新平衡是自动还是手动进行?

在全自动重新平衡系统自动决定何时将分区从一个节点移动到另一个节点而没有任何管理员交互和完全手动分区指派给节点由管理员明确配置之间有一个梯度仅在管理员明确重新配置时才会更改。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配但需要管理员在生效之前提交它。

全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,并在重新平衡过程中损害其他请求的性能。

这种自动化与自动故障检测相结合可能是危险的。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对超载节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。

出于这个原因,让一个人重新平衡循环是一件好事。这比完全自动的过程慢,但它可以帮助防止操作意外。

请求路由

现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题当客户想要提出请求时如何知道要连接哪个节点随着分区重新平衡分区对节点的分配也发生变化。为了回答这个问题有人需要停留在这些变化之上如果我想读或写密钥“foo”需要连接哪个IP地址和端口号

这是一个称为服务发现的更普遍问题的实例,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是实现高可用性(在多台机器上运行冗余配置)。许多公司已经编写了自己的内部服务发现工具,其中许多已经作为开源发布[30]。

在很高的层面上这个问题有几种不同的方法如图6-7所示

  1. 允许客户联系任何节点(例如,通过循环负载均衡器)。如果该节点巧合地拥有请求所适用的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收到答复,并将答复传递给客户端。
  2. 首先将所有来自客户端的请求发送到路由选择层,这决定了应该处理每个请求的节点并相应地转发它。此路由层本身不处理任何请求;它仅充当分区感知负载平衡器。
  3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。

在所有情况下,关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区向节点的分配变化?

图6-7 将请求路由到正确节点的三种不同方式。

这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议但很难正确实施见第9章

许多分布式数据系统都依赖于一个独立的协调服务比如Zoo-Keeper来跟踪这个集群元数据如图6-8所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的权威映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区改变了所有权或者添加或删除了一个节点ZooKeeper就会通知路由层以使路由信息保持最新状态。

图6-8 使用ZooKeeper跟踪分区分配给节点。

例如LinkedIn的Espresso使用Helix [31]进行集群管理依靠ZooKeeper实现了一个路由层如图6-8所示。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的配置服务器实现和mongos守护进程作为路由层。

Cassandra和Riak采取不同的方法他们在节点之间使用八卦协议来传播群集状态的任何变化。可以将请求发送到任何节点并将该节点转发到所请求的分区的适当节点图6-7中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。

Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由选择层它学习了来自集群节点的路由更改[32]。

当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些分区并不像分配给节点那么快所以为此使用DNS通常就足够了。

并行查询执行

到目前为止,我们只关注读取或写入单个关键字的非常简单的查询(对于文档分区的二级索引,另外还有分散/聚集查询。这与大多数NoSQL分布式数据存储所支持的访问级别有关。

然而通常用于分析的大规模并行处理MPP关系数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接过滤分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大部分数据集的查询尤其受益于这种并行执行。

数据仓库查询的快速并行执行是一个专门的话题考虑到分析的业务重要性它收到了很多商业利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献[1,33]。

本章小结

在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。如果您有太多的数据,在单台机器上存储和处理不再可行,则分区是必要的。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时重新分区。 我们讨论了两种主要的分区方法:

  • 键范围分区,其中键排序,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常按照排序顺序访问密切相关的密钥,则存在热点的风险。

在这种方法中,当分区变得太大时,通常将分区分成两个子分区,动态地重新平衡分区。

  • 散列分区,散列函数应用于每个键,分区拥有一定范围的散列。这种方法破坏了键的排序,使得范围查询效率低下,但可以更均匀地分配负载。

通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。动态分区也可以使用。

混合方法也是可能的,例如使用复合键:使用键的一部分来识别分区,而使用另一部分作为排序顺序。 我们还讨论了分区和二级索引之间的交互。第二个索引也需要分割,有两种方法:

  • 文档分区索引(本地索引),其中辅助索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取辅助索引需要在所有分区之间进行分散/收集。
  • 使用索引值的术语分区索引(全局索引),其中二级索引是分开分开的。辅助索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新二级索引的多个分区;但是,可以从单个分区提供读取。

最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区感知负载平衡到复杂的并行查询执行引擎。 按照设计,每个分区大部分是独立运行的 - 这就是允许分区数据库扩展到多台机器的原因。但是,需要写入多个分区的操作难以推理:例如,如果写入一个分区成功,但另一个分区失败,会发生什么情况?我们将在下面的章节中讨论这个问题。

参考文献

  1. David J. DeWitt and Jim N. Gray: “Parallel Database Systems: The Future of High Performance Database Systems,” Communications of the ACM, volume 35, number 6, pages 8598, June 1992. doi:10.1145/129888.129894

  2. Lars George: “HBase vs. BigTable Comparison,” larsgeorge.com, November 2009.

  3. The Apache HBase Reference Guide,” Apache Software Foundation, hbase.apache.org, 2014.

  4. MongoDB, Inc.: “New Hash-Based Sharding Feature in MongoDB 2.4,” blog.mongodb.org, April 10, 2013.

  5. Ikai Lan: “App Engine Datastore Tip: Monotonically Increasing Values Are Bad,” ikaisays.com, January 25, 2011.

  6. Martin Kleppmann: “Java's hashCode Is Not Safe for Distributed Systems,” martin.kleppmann.com, June 18, 2012.

  7. David Karger, Eric Lehman, Tom Leighton, et al.: “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web,” at 29th Annual ACM Symposium on Theory of Computing (STOC), pages 654663, 1997. doi:10.1145/258533.258660

  8. John Lamping and Eric Veach: “A Fast, Minimal Memory, Consistent Hash Algorithm,” arxiv.org, June 2014.

  9. Eric Redmond: “A Little Riak Book,” Version 1.4.0, Basho Technologies, September 2013.

  10. Couchbase 2.5 Administrator Guide,” Couchbase, Inc., 2014.

  11. Avinash Lakshman and Prashant Malik: “Cassandra A Decentralized Structured Storage System,” at 3rd ACM SIGOPS International Workshop on Large Scale Distributed Systems and Middleware (LADIS), October 2009.

  12. Jonathan Ellis: “Facebooks Cassandra Paper, Annotated and Compared to Apache Cassandra 2.0,” datastax.com, September 12, 2013.

  13. Introduction to Cassandra Query Language,” DataStax, Inc., 2014.

  14. Samuel Axon: “3% of Twitter's Servers Dedicated to Justin Bieber,” mashable.com, September 7, 2010.

  15. Riak 1.4.8 Docs,” Basho Technologies, Inc., 2014.

  16. Richard Low: “The Sweet Spot for Cassandra Secondary Indexing,” wentnet.com, October 21, 2013.

  17. Zachary Tong: “Customizing Your Document Routing,” elasticsearch.org, June 3, 2013.

  18. Apache Solr Reference Guide,” Apache Software Foundation, 2014.

  19. Andrew Pavlo: “H-Store Frequently Asked Questions,” hstore.cs.brown.edu, October 2013.

  20. Amazon DynamoDB Developer Guide,” Amazon Web Services, Inc., 2014.

  21. Rusty Klophaus: “Difference Between 2I and Search,” email to riak-users mailing list, lists.basho.com, October 25, 2011.

  22. Donald K. Burleson: “Object Partitioning in Oracle,”dba-oracle.com, November 8, 2000.

  23. Eric Evans: “Rethinking Topology in Cassandra,” at ApacheCon Europe, November 2012.

  24. Rafał Kuć: “Reroute API Explained,” elasticsearchserverbook.com, September 30, 2013.

  25. Project Voldemort Documentation,” project-voldemort.com.

  26. Enis Soztutar: “Apache HBase Region Splitting and Merging,” hortonworks.com, February 1, 2013.

  27. Brandon Williams: “Virtual Nodes in Cassandra 1.2,” datastax.com, December 4, 2012.

  28. Richard Jones: “libketama: Consistent Hashing Library for Memcached Clients,” metabrew.com, April 10, 2007.

  29. Branimir Lambov: “New Token Allocation Algorithm in Cassandra 3.0,” datastax.com, January 28, 2016.

  30. Jason Wilder: “Open-Source Service Discovery,” jasonwilder.com, February 2014.

  31. Kishore Gopalakrishna, Shi Lu, Zhen Zhang, et al.: “Untangling Cluster Management with Helix,” at ACM Symposium on Cloud Computing (SoCC), October 2012. doi:10.1145/2391229.2391248

  32. Moxi 1.8 Manual,” Couchbase, Inc., 2014.

  33. Shivnath Babu and Herodotos Herodotou: “Massively Parallel Databases and MapReduce Systems,” Foundations and Trends in Databases, volume 5, number 1, pages 1104, November 2013.doi:10.1561/1900000036


  1. 正如本章所讨论的分区是一种有意将大型数据库分解成小型数据库的方式。它与网络分区net splits无关这是节点之间网络中的一种故障类型。我们将在第8章讨论这些错误。 ↩︎