diff --git a/ddia/ch6.md b/ddia/ch6.md index 7cc15fbefe34031075b2319b34f1e10771dacf4a..f8eb82b3179f05d146cafecc4747b644c422f8c7 100644 --- a/ddia/ch6.md +++ b/ddia/ch6.md @@ -11,38 +11,43 @@ [TOC] -在第5章中,我们讨论了复制 - 即在不同节点上有相同数据的多个副本。对于非常大的数据集或非常高的查询吞吐量,这是不够的:我们需要将数据拆分成分区,也称为sharding[^i] +在[第5章](ch5.md)中,我们讨论了复制 - 即在不同节点上有相同数据的多个副本。对于非常大的数据集,或非常高的查询吞吐量是不够的:我们需要将数据拆分成**分区(partitions)**,也称为**分片(sharding)**[^i] [^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与网络分区(net splits)无关,这是节点之间网络中的一种故障类型。我们将在第8章讨论这些错误。 > ##### 术语澄清 > -> 我们在这里称之为**分区(partition)**的东西,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**;在HBase中称之为**区域(Region)**,Bigtable中的 **表块(tablet)**,Cassandra和Riak中**虚节点(vnode)**以及Couchbase中的**虚桶(vBucket)**。但是**分区(partition)**是最重要的术语,所以这里坚持使用它。 +> 这里称之为**分区(partition)**的东西,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**;在HBase中称之为**区域(Region)**,Bigtable中的 **表块(tablet)**,Cassandra和Riak中**虚节点(vnode)**以及Couchbase中的**虚桶(vBucket)**。但是**分区(partition)**是最重要的术语,所以这里坚持使用它。 > -通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)只属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。 +通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。 -要分区数据的主要原因是可扩展性。不同的分区可以放在不共享的集群中的不同节点上(请参阅[第二部分](part-ii.md)关于无共享架构的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。 +需要分区数据的主要原因是**可扩展性**。不同的分区可以放在不共享的集群中的不同节点上(请参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。 对于在单个分区上运行的查询,每个节点可以独立执行对其自己的分区的查询,因此可以通过添加更多的节点来缩放查询吞吐量。大型,复杂的查询可能会跨越多个节点进行并行处理,尽管这会变得非常困难。 -分区数据库在20世纪80年代由Teradata和NonStop SQL[【1】][1]等产品率先推出,最近又被NoSQL数据库和基于Hadoop的数据仓库重新发明。有些系统是为事务性工作负载设计的,其他系统则用于分析(请参阅第90页上的“事务处理或分析?”):这种差异会影响系统的调整方式,但是分区的基本原理适用于这两种工作负载。 +分区数据库在20世纪80年代由Teradata和NonStop SQL[【1】][1]等产品率先推出,最近又被NoSQL数据库和基于Hadoop的数据仓库重新发明。有些系统是为事务性工作负载设计的,其他系统则用于分析(请参阅“[事务处理或分析]()?”):这种差异会影响系统的调整方式,但是分区的基本原理适用于这两种工作负载。 -在本章中,我们将首先介绍分割大型数据集的不同方法,并观察数据索引如何与分区交互。然后,我们将讨论重新平衡,如果您想要添加或删除群集中的节点,则必须进行重新平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。 +在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后,我们将讨论[再平衡](),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。 ## 分片与复制 +分区通常与复制结合使用,以便每个分区的副本都存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。 + +一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1]()所示。 每个分区的主被分配给一个节点,从被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。 +我们在[第5章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制复制。 + ![](img/fig6-1.png) **图6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。** ## 键值数据的分片 -假设你有大量的数据,你想分割它。你如何决定在哪些节点上存储哪些记录? +假设你有很多的数据,想要分片?那么,如何决定在哪些节点上存储哪些记录呢? -我们的分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点都公平分享,那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量(目前忽略复制)。 +分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点公平分享数据和负载,那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量(目前忽略复制)。 -如果分区是不公平的,那么一些分区比其他分区有更多的数据或查询,我们称之为偏斜。歪斜的存在使分区效率下降得多。在极端的情况下,所有的负载都可能在一个分区上,所以10个节点中有9个是空闲的,你的瓶颈就是单个的繁忙节点。一个负载不均衡的分区被称为热点。 +如果分区是不公平的,那么一些分区比其他分区有更多的数据或查询,我们称之为**偏斜(skew)**。数据倾斜的存在使分区效率下降很多。在极端的情况下,所有的负载都可能在一个分区上,所以10个节点中有9个是空闲的,你的瓶颈就是单个的繁忙节点。一个负载不均衡的分区被称为**热点(hot spot)**。 避免热点的最简单方法是将记录随机分配给节点。这将在整个节点上平均分配数据,但是它有一个很大的缺点:当你试图读取一个特定的项目时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。 @@ -51,117 +56,119 @@ ### 根据键的范围分片 -分区的一种方法是为每个分区分配一个连续的键范围(从最小值到最大值),如纸百科全书的卷(图6-2)。如果知道范围之间的界限,则可以轻松确定哪个分区包含给定的键。如果您还知道哪个分区分配给哪个节点,那么您可以直接向相应的节点发出请求(或者在百科全书的情况下,从书架上选取正确的书籍)。 +一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的界限,则可以轻松确定哪个分区包含给定的键。如果您还知道哪个分区分配给哪个节点,那么您可以直接向相应的节点发出请求(或者对于百科全书而言,从书架上选取正确的书籍)。 ![](img/fig6-2.png) **图6-2 印刷版百科全书按照关键字范围进行分区** -键的范围不一定均匀分布,因为您的数据可能不均匀分布。例如,在图6-2中,第1卷包含以A和B开头的单词,但第12卷则包含以T,U,V,X,Y和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据,分区边界需要适应数据。 +键的范围不一定均匀分布,因为您的数据可能不均匀分布。例如,在[图6-2]()中,第1卷包含以A和B开头的单词,但第12卷则包含以T,U,V,X,Y和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据,分区边界需要适应数据。 -分区边界可以由管理员手动选择,也可以由数据库自动选择(我们将在第209页的“重新平衡分区”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略,其开源的HBase [2,3],RethinkDB和2.4版本之前的MongoDB [4][4]。 +分区边界可以由管理员手动选择,也可以由数据库自动选择(我们将在第209页的“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略,以及其开源等价物HBase [2,3],RethinkDB和2.4版本之前的MongoDB [4][4]。 -在每个分区中,我们可以按照排序的顺序保存键(请参见第70页上的“SSTables和LSM-树”)。这具有范围扫描非常简单的优点,您可以将键作为连接索引来处理,以便在一个查询中获取多个相关记录(请参阅第79页的“多列索引”)。例如,考虑存储来自传感器网络的数据的应用程序,其中关键是测量的时间戳(年 - 月 - 日 - 时 - 分 - 秒)。范围扫描在这种情况下非常有用,因为它们让您轻松获取某个月份的所有读数。 +在每个分区中,我们可以按照排序的顺序保存键(参见“[SSTables和LSM-树]()”)。这具有范围扫描非常简单的优点,您可以将键作为连接索引来处理,以便在一个查询中获取多个相关记录(请参阅第79页的“[多列索引]()”)。例如,考虑存储来自传感器网络的数据的应用程序,其中关键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为它们让您轻松获取某个月份的所有读数。 -然而,关键范围分区的缺点是某些访问模式会导致热点。 如果密钥是时间戳,则分区对应于时间范围,例如,每天一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态[5]。 +然而,Key Range分区的缺点是某些访问模式会导致热点。 如果Key是时间戳,则分区对应于时间范围,例如,每天一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态[5]。 -为了避免传感器数据库中的这个问题,您需要使用除时间戳以外的其他内容作为密钥的第一个元素。 例如,您可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。 +为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为Key的第一个部分。 例如,可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。 -### 根据键的哈希分片 +### 根据键的散列分片 -由于这种倾斜和热点的风险,许多分布式数据存储使用散列函数来确定给定密钥的分区。 +由于这种倾斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。 -一个好的散列函数需要偏斜的数据并使其均匀分布。假设你有一个带有字符串的32位散列函数。无论何时给它一个新的字符串,它将返回一个0到232-1之间的表面上的随机数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。 +一个好的散列函数可以将接受偏斜的数据并使其均匀分布。假设你有一个带有字符串的32位散列函数。无论何时给它一个新的字符串,它将返回一个0到$2^{32}-1$之间的"随机"数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。 -对于分区目的来说,散列函数不需要密码强壮:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(因为它们用于哈希表),但是它们可能不适合分区:例如,在Java的Object.hashCode()和Ruby的Object#哈希中,同一个键可能有不同的过程中不同的哈希值[6]。 +出于分区的目的,散列函数不需要多么强壮的密码学安全性:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(因为它们用于哈希表),但是它们可能不适合分区:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一个键可能有不同的进程中不同的哈希值[6]。 -一旦你有一个合适的密钥散列函数,你可以为每个分区分配一个散列范围(而不是一系列的密钥),每个散列落在分区范围内的密钥将被存储在该分区中。如图6-3所示。 +一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个散列落在分区范围内的键将被存储在该分区中。如图6-3所示。 ![](img/fig6-3.png) **图6-3 按哈希键分区** -这种技术擅长在分区之间分配密钥。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时被称为一致性散列)。 +这种技术擅长在分区之间分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时被称为**一致性哈希(consistent hashing)**)。 -> 一致性哈希 -> 一致性哈希由Karger等人定义。[7] 是一种平均分配负载的方法,通过内容分发网络(CDN)等互联网系统的缓存。 它使用随机选择的分区边界来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅第5章)或ACID一致性(请参阅第7章)无关,而是描述了重新平衡的特定方法。 +> #### 一致性哈希 +> +> 一致性哈希由Karger等人定义。[7] 用于跨互联网级别的缓存系统,例如CDN中,是一种能均匀分配负载的方法。它使用随机选择的**分区边界(partition boundaries)**来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅第5章)或ACID一致性(请参阅第7章)无关,而是描述了重新平衡的特定方法。 > -> 正如我们将在第209页的“重新平衡分区”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为这太混乱了,所以最好避免使用一致性哈希这个术语,而只是把它称为散列分区(hash partitioning)。 +> 正如我们将在第209页的“重新平衡分区”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为这太混乱了,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区(hash partitioning)**。 -不幸的是,通过使用Key散列进行分区,我们失去了键范围分区的一个很好的属性:执行高效范围查询的能力。曾经相邻的密钥现在分散在所有分区中,所以它们的排序顺序就会丢失。在MongoDB中,如果您启用了基于散列的分片模式,则任何范围查询都必须发送到所有分区[4]。主键上的范围查询不受Riak [9],Couchbase [10]或Voldemort的支持。 +不幸的是,通过使用Key散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中,所以它们之间的顺序就丢失了。在MongoDB中,如果您使用了基于散列的分片模式,则任何范围查询都必须发送到所有分区[4]。主键上的范围查询不受Riak [9],Couchbase [10]或Voldemort的支持。 -Cassandra在两个分区策略之间达成了一个折衷[11,12,13]。 Cassandra中的表可以使用由多个列组成的复合主键来声明。只有该密钥的第一部分被散列来确定分区,而其他列则被用作Cas- sandra的SSTables中排序数据的连接索引。因此,查询无法在组合键的第一列中搜索一系列值,但如果为第一列指定了固定值,则可以对该键的其他列执行有效的范围扫描。 +Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表,但如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。 串联索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id,update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。 ### 负载倾斜与消除热点 -如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个密钥的,所有的请求都会被路由到同一个分区。 +如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。 -这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴[14]。这个事件可能导致大量写入同一个密钥(密钥可能是名人的用户ID,或者人们正在评论的动作的ID)。哈希键不起作用,因为两个相同ID的哈希值仍然是相同的。 +这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴[14]。这个事件可能导致大量写入同一个键(键可能是名人的用户ID,或者人们正在评论的动作的ID)。哈希键不起作用,因为两个相同ID的哈希值仍然是相同的。 如今,大多数数据系统无法自动补偿这种高度偏斜的工作负载,因此应用程序有责任减少偏斜。例如,如果一个密钥被认为是非常热的,一个简单的方法是在密钥的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将写入密钥分散到100个不同的密钥中,从而允许这些密钥分配到不同的分区。 然而,在不同的密钥之间进行分割,任何读取都必须要做额外的工作,因为他们必须从所有100个密钥中读取数据并将其合并。此技术还需要额外的簿记:只为少量热键附加随机数是有意义的;对于写入吞吐量低的绝大多数密钥,这将是不必要的开销。因此,您还需要一些方法来跟踪哪些键被分割。 -也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要考虑自己的应用程序的权衡。 +也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。 + ## 分片与次级索引 到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。 -如果涉及二级索引,情况会变得更加复杂(另见“其他索引结构”在第85页)。辅助索引通常不能唯一地标识记录,而是搜索特定值的发生的方式:查找用户123的所有操作,查找包含词语hogwash的所有文章,查找所有颜色为红色的车辆等等上。 +如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构]()”)。辅助索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户123的所有操作,查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。 -二级索引是关系数据库的吃饭家伙,在文档数据库中也是通用的。许多键值存储(如HBase和Volde-mort)由于增加了实现的复杂性而避免了二级索引,但是一些(如Riak)已经开始添加它们,因为它们对于数据建模非常有用。最后,二级索引是Solr和Elasticsearch等搜索服务器的存在理由。 +次级索引是关系型数据库的吃饭家伙,在文档数据库中也是很普遍的。许多键值存储(如HBase和Volde-mort)由于实现的复杂性而避免次级索引,但是一些(如Riak)已经开始添加它们,因为它们对于数据建模实在是太有用了。最后,次级索引是Solr和Elasticsearch等搜索服务器的存在意义。 -二级索引的问题是它们不能整齐地映射到分区。有两种主要的方法可以用二级索引分区数据库:基于文档的分区和基于词的分区。 +次级索引的问题是它们不能整齐地映射到分区。有两种主要的方法可以用二级索引分区数据库:**基于文档的分区(document-based)**和**基于关键词(term-based)的分区**。 ### 按文档的二级索引 -例如,假设您正在经营一个销售二手车的网站(如图6-4所示)。 每个列表都有一个唯一的ID--称之为文档ID--并且用文档ID对数据库进行分区(例如,分区0中的ID 0到499,分区1中的ID 500到999等)。 +例如,假设您正在经营一个销售二手车的网站(如图6-4所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区(例如,分区0中的ID 0到499,分区1中的ID 500到999等)。 -你想让用户搜索汽车,允许他们通过颜色和通过过滤,所以你需要一个二级索引的颜色和(在文档数据库中这些将是字段(field),在关系数据库中,他们将是列(column) )。 如果您声明了索引,则数据库可以自动执行索引。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。 +你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段(field)**,关系数据库中这些是**列(column)** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。 -[^ii]: 如果您的数据库仅支持键值模型,则可能会尝试通过在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果您沿着这条路线走下去,您需要非常小心,以确保您的索引与基础数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参见第231页上的“多对象事务的需要”。。 +[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 参见“[多对象事务的需求]()”。 ![](img/fig6-4.png) **图6-4 按文档分区二级索引** -在这种索引方法中,每个分区是完全独立的:每个分区维护自己的二级索引,仅覆盖该分区中的文档。它不关心哪些数据存储在其他分区中。无论何时您需要写入数据库(添加,删除或更新文档),只需处理包含您正在编写的文档ID的分区即可。出于这个原因,文档分区索引也被称为本地索引(而不是全局索引,在下一节中描述)。 +在这种索引方法中,每个分区是完全独立的:每个分区维护自己的二级索引,仅覆盖该分区中的文档。它不关心哪些数据存储在其他分区中。无论何时您需要写入数据库(添加,删除或更新文档),只需处理包含您正在编写的文档ID的分区即可。出于这个原因,**文档分区索引**也被称为**本地索引(local index)**(而不是将在下一节中描述的**全局索引(global index)**)。 但是,从文档分区索引中读取需要注意:除非您对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。 -这种查询分区数据库的方法有时被称为分散/聚集,并且可能会使二级索引的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅第16页的“实践中的百分比”)。然而,它被广泛使用:MonDBDB,Riak [15],Cassandra [16],Elasticsearch [17],SolrCloud [18]和VoltDB [19]都使用文档分区二级索引。大多数数据库供应商建议您构建分区方案,以便可以从单个分区提供二级索引查询,但这并不总是可行,尤其是当您在单个查询中使用多个二级索引时(例如按颜色并通过在同一时间)。 +这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅第16页的“实践中的百分比”)。然而,它被广泛使用:MonDBDB,Riak [15],Cassandra [16],Elasticsearch [17],SolrCloud [18]和VoltDB [19]都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。 ### 根据Term的二级索引 -我们可以构建覆盖所有分区数据的全局索引,而不是每个分区都有自己的二级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但可以与主键索引进行不同的分区。 +我们可以构建一个覆盖所有分区数据的**全局索引**,而不是每个分区都有自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但索引可以采用与主键不同的分区方式。 + +图6-5说明了这可能是什么情况:来自所有分区的红色汽车在索引中显示为红色:索引中的红色,但索引是分区的,以便从字母a到r开始的颜色出现在分区0中,颜色以s开始z出现在第1部分。汽车制造商的指数也是相似的(分区边界在f和h之间)。 ![](img/fig6-5.png) **图6-5 按术语对二级索引进行分区** -图6-5说明了这可能是什么情况:来自所有分区的红色汽车在索引中显示为红色:索引中的红色,但索引是分区的,以便从字母a到r开始的颜色出现在分区0中,颜色以s开始z出现在第1部分。汽车制造商的指数也是相似的(分区边界在f和h之间)。 - -我们将这种索引术语称为分割术语,因为我们期待的术语决定了索引的分割。在这里,例如,一个术语将是颜色:红色。名称术语来自全文索引(一种特定的二级索引),其中术语是文档中出现的所有单词。 +我们将这种索引称为**关键词分片(term-partitioned)**,因为我们寻找的关键词决定了索引的分片。在这里,例如,一个关键词可能是:`颜色:红色`。**关键词(Term)**这个术语来源于来自全文搜索索引(一种特定的次级索引),其中术语是文档中出现的所有单词。 -和以前一样,我们可以通过术语本身来划分索引,或者使用术语的散列。通过术语本身进行划分对于范围扫描是有用的(例如,数字特性,例如汽车的要价),而对术语的哈希进行划分给出了负载的更均匀的分布。 +和以前一样,我们可以通过**关键词**本身来将索引分片,或者使用关键词的散列。对关键词本身进行划分,对于范围扫描是有用的(例如,数字特性,例如汽车的要价),而对术语的哈希进行划分给出了负载的更均匀的分布。 -全局(术语分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是分散/收集所有分区,客户端只需要向包含术语的分区发出请求它想要的。但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。 +全局(关键词分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求它想要的。但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。 -在理想的世界里,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在分区索引中,这将需要跨所有受写入影响的分区进行分布式事务,这在所有数据库中都不受支持(请参阅第7章和第9章)。 +在理想的世界里,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在分区索引中,这会需要跨库分布式事务,跨越所有被写入影响的分片,这在所有数据库中都不受支持(请参阅第7章和第9章)。 -在实践中,对全局二级索引的更新通常是异步的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB指出,在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会经历更长的传播延迟[20]。 +在实践中,对全局二级索引的更新通常是**异步**的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB指出,在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会经历更长的传播延迟[20]。 -全局术语分区索引的其他用途包括Riak的搜索功能[21]和Oracle数据仓库,它允许您在本地索引和全局索引之间进行选择[22]。我们将回到第12章中实施分词二级索引的主题。 +全局术语分区索引的其他用途包括Riak的搜索功能[21]和Oracle数据仓库,它允许您在本地索引和全局索引之间进行选择[22]。我们将回到第12章中回到实现关键字二级索引的主题。 -## 平衡分区 +## 再平衡分区 在数据库中,随着时间的推移,事情也在起变化。 @@ -169,62 +176,69 @@ Cassandra在两个分区策略之间达成了一个折衷[11,12,13]。 Cassandra * 数据集大小增加,所以您想添加更多的磁盘和RAM来存储它。 * 机器出现故障,其他机器需要接管故障机器的责任。 -所有这些更改都要求数据和请求从一个节点移动到另一个节点。 从集群中的一个节点向另一个节点移动负载的过程称为重置。 -无论使用哪种分区方案,重新平衡通常都会满足一些最低要求: +所有这些更改都要求数据和请求从一个节点移动到另一个节点。 从集群中的一个节点向另一个节点移动负载的过程称为**再平衡(reblancing)**。 -* 重新平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。 -* 重新平衡正在发生时,数据库应该继续接受读取和写入。 -* 节点之间不应移动超过所需的数据,以便快速重新配置,并尽量减少网络和磁盘I / O负载。 +无论使用哪种分区方案,再平衡通常都会满足一些最低要求: + +* 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。 +* 再平衡正在发生时,数据库应该继续接受读取和写入。 +* 节点之间不应移动超过所需的数据,以便快速再平衡,并尽量减少网络和磁盘I / O负载。 ### 平衡策略 有几种不同的分区分配方式[23]。让我们依次简要讨论一下。 -#### 别这样做:hash mod N +#### 反面教材:hash mod N -我们在前面说过(图6-3),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果0≤散列(键),则将键分配给分区0)如果b0≤散列(关键字)