This commit is contained in:
Vonng 2018-03-26 00:18:28 +08:00
commit cede21522d

141
ch6.md
View File

@ -53,7 +53,6 @@
我们可以做得更好。现在假设您有一个简单的键值数据模型,其中您总是通过其主键访问记录。例如,在一本老式的纸质百科全书中,你可以通过标题来查找一个条目;由于所有条目按字母顺序排序,因此您可以快速找到您要查找的条目。
### 根据键的范围分区
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
@ -94,7 +93,7 @@
>
> 正如我们将在“[重新平衡分区](#重新平衡分区)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。
@ -112,22 +111,21 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。
-------------------------
(以上完成)
## 分片与次级索引
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构]()”。辅助索引通常并不能唯一地标识记录而是一种搜索记录中出现特定值的方式查找用户123的所有操作查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
次级索引是关系型数据库的吃饭家伙在文档数据库中也是很普遍的。许多键值存储如HBase和Volde-mort由于实现的复杂性而避免次级索引但是一些如Riak已经开始添加它们因为它们对于数据建模实在是太有用了。最后次级索引是Solr和Elasticsearch等搜索服务器的存在意义
次级索引是关系型数据库的基础并且在文档数据库中也很普遍。许多键值存储如HBase和Volde-mort为了减少实现的复杂度而放弃了次级索引但是一些如Riak已经开始添加它们因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石
次级索引的问题是它们不能整齐地映射到分区。有两种主要的方法可以用二级索引分区数据库**基于文档的分区document-based**和**基于关键词term-based的分区**。
次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法**基于文档的分区document-based**和**基于关键词term-based的分区**。
### 按文档的二级索引
例如,假设您正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`colorred`的文档ID列表中。
@ -137,57 +135,56 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**图6-4 按文档分区二级索引**
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的二级索引,仅覆盖该分区中的文档。它不关心哪些数据存储在其他分区中。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因**文档分区索引**也被称为**本地索引local index**(而不是将在下一节中描述的**全局索引global index**)。
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的二级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因**文档分区索引**也被称为**本地索引local index**(而不是将在下一节中描述的**全局索引global index**)。
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”。然而它被广泛使用MonDBDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个二级索引时例如同时需要按颜色和制造商查询
### 根据关键词(Term)的二级索引
### 根据Term的二级索引
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是每个分区都有自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但索引可以采用与主键不同的分区方式。
[图6-5](img/fig6-5.png)说明了这可能是什么情况:来自所有分区的红色汽车在索引中显示为红色:索引中的红色,但索引是分区的,以便从字母`a`到`r`开始的颜色出现在分区0中颜色以`s`开始`z`出现在第1部分。汽车制造商的指数也是相似的分区边界在`f`和`h`之间)。
[图6-5](img/fig6-5.png)阐述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中`s`到`z`的在分区1。汽车制造商的索引也与之类似分区边界在`f`和`h`之间)。
![](img/fig6-5.png)
**图6-5 按术语对二级索引进行分区**
**图6-5 按关键词对二级索引进行分区**
我们将这种索引称为**关键词分term-partitioned**,因为我们寻找的关键词决定了索引的分片。在这里,例如,一个关键词可能是:`颜色:红色`。**关键词Term**这个术语来源于来自全文搜索索引(一种特定的次级索引),其中术语是文档中出现的所有单词。
我们将这种索引称为**关键词分term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`颜色:红色`。**关键词(Term)** 来源于来自全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
以前一样,我们可以通过**关键词**本身来将索引分片,或者使用关键词的散列。对关键词本身进行划分,对于范围扫描是有用的(例如,数字特性,例如汽车的要价),而对术语的哈希进行划分给出了负载的更均匀的分布
之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据它本身分区对于范围扫描非常有用(例如对于数字,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力
全局(关键词分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求它想要的但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
在理想的世界里,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在分区索引中,这需要跨库分布式事务,跨越所有被写入影响的分片,这在所有数据库中都不受支持(请参阅[第7章](ch7.md)和[第9章](ch9.md))。
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第7章](ch7.md)和[第9章](ch9.md))。
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB指出,在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会经历更长的传播延迟【20】。
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB声称在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会延迟【20】。
全局术语分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地索引和全局索引之间进行选择【22】。我们将回到[第12章](ch12.md)中回到实现关键字二级索引的主题。
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中涉及实现关键字二级索引的话题。
## 分区再平衡
## 重新平衡
在数据库中,随着时间的推移,事情也在起变化。
随着时间的推移,数据库会有各种变化。
* 查询吞吐量增加所以您想要添加更多的CPU来处理负载。
* 数据集大小增加所以您想添加更多的磁盘和RAM来存储它。
* 机器出现故障,其他机器需要接管故障机器的责任。
所有这些更改都要数据和请求从一个节点移动到另一个节点。 从集群中的一个节点向另一个节点移动负载的过程称为**重新平衡reblancing**。
所有这些更改都要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为**再平衡reblancing**。
无论使用哪种分区方案,重新平衡通常都会满足一些最低要求:
无论使用哪种分区方案,再平衡通常都要满足一些最低要求:
* 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
* 再平衡发生时,数据库应该继续接受读取和写入。
* 节点之间只移动必须的数据以便快速再平衡并减少网络和磁盘I/O负载。
* 重新平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
* 重新平衡正在发生时,数据库应该继续接受读取和写入。
* 节点之间不应移动超过所需的数据以便快速重新平衡并尽量减少网络和磁盘I/O负载。
### 平衡策略
有几种不同的分区分配方式【23】。让我们依次简要讨论一下。
有几种不同的分区分配方法【23】,让我们依次简要讨论一下。
#### 反面教材hash mod N
@ -197,77 +194,75 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
模$N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
我们需要一种不需要移动数据的方法。
我们需要一种只移动必需数据的方法。
#### 固定数量的分区
幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。
现在,如果一个节点被添加到集群中,新节点可以从每个现有节点中**窃取**几个分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中**窃取**一些分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
只有整个分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所指派的节点。这种指派变更并不是即时的——在网络上传输大量的数据需要一些时间——所以在传输过程中,旧的分区会接受传输过程中发生的读写操作。
只有分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 — 所以在传输过程中,原有分区仍然会接受读写操作。
![](img/fig6-6.png)
**图6-6 将新节点添加到每个节点具有多个分区的数据库群集。**
原则上,您甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点分担更多的负载。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了这种重新平衡的方法。
原则上,您甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了这种平衡的方法。
在这种配置中,分区的数量通常在数据库第一次建立时是固定的,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够高的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太高的数字是适得其反的
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反
如果数据集的总大小是高度可变的(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含总数据的固定部分,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,重新平衡和从节点故障恢复变得昂贵。但是,如果份额太小,则会产生太多的开销。当分区大小“恰到好处”,既不会太大,也不会太小,如果分区数量固定,但数据集大小变化不定,则难以达到最佳性能。
如果数据集的总大小难以预估(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
#### 动态分区
对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致所有一个分区中的数据和所有其他分区中的数据为空。手动重新配置分区边界将非常繁琐。
对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB它被分成两个分区大约每个分区各占一半的数据【26】。相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
每个分区指派给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase的情况下分区文件的传输通过HDFS底层分布式文件系统来实现【3】。
每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase分区文件的传输通过HDFS底层分布式文件系统来实现【3】。
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据每个分区的大小被限制在一个可配置的最大值【23】。
但是,需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。虽然数据集很小,直到达到第一个分区的分割点所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**)。在键范围分区的情况下,预分割要求已经知道键分布的样子【4,26】。
需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小直到达到第一个分区的分割点所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**)。在键范围分区的情况中,预分割需要提前知道键是如何进行分配的【4,26】。
动态分区不仅适用于关键的范围分区数据而且也适用于散列分区数据。从版本2.4开始MongoDB同时支持键范围和哈希分区并且在任何情况下动态分割分区。
动态分区不仅适用于数据的范围分区而且也适用于散列分区。从版本2.4开始MongoDB同时支持范围和哈希分区并且都是进行动态分割分区。
#### 按节点比例分区
通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成比——换句话说每个节点具有固定数量的分区【23, 27, 28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小相当稳定。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成比——换句话说每个节点具有固定数量的分区【23,27,28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分裂,但是当在更大数量的分区上进行平均在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种可重用的算法来避免不公平的分裂【29】。
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再分配的算法来避免不公平的分割【29】。
随机选择分区边界要求使用基于散列的分区(所以可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性散列的原始定义【7】参阅“[一致性散列](#一致性散列)”)。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果【8】。
随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】参阅“[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
### 运维:手动还是自动平衡
关于我们已经掩盖的重新平衡问题有一个重要问题:重新平衡是自动还是手动进行?
关于再平衡有一个重要问题:自动还是手动进行?
在全自动重新平衡(系统自动决定何时将分区从一个节点移动到另一个节点,而没有任何管理员交互)和完全手动(分区指派给节点由管理员明确配置)之间有一个梯度仅在管理员明确重新配置时才会更改。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配但需要管理员在生效之前提交它
在全自动重新平衡(系统自动决定何时将分区从一个节点移动到另一个节点,无须人工干预)和完全手动(分区指派给节点由管理员明确配置,仅在管理员明确重新配置时才会更改)之间有一个权衡。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配但需要管理员提交才能生效
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,并在重新平衡过程中损害其他请求的性能。
这种自动化与自动故障检测相结合可能是危险的。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对超载节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。
出于这个原因,再平衡的循环中有一个人参与是一件好事。这比完全自动的过程慢,但可以帮助防止意外操作。
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
这种自动化与自动故障检测相结合可能十分危险。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对已经超负荷的节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。
出于这个原因,再平衡的过程中有人参与是一件好事。这比完全自动的过程慢,但可以帮助防止运维意外。
## 请求路由
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,有人需要停留在这些变化之上如果我想读或写键“foo”需要连接哪个IP地址和端口号
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,需要有人知晓这些变化如果我想读或写键“foo”需要连接哪个IP地址和端口号
是一个称为**服务发现service discovery**的更普遍问题的实例,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是实现高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。
个问题可以概括为 **服务发现(service discovery)** 它不仅限于数据库。任何可通过网络访问的软件都有这个问题特别是如果它的目标是高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。
在很高的层面上这个问题有几种不同的方法如图6-7所示
概括来说这个问题有几种不同的方案如图6-7所示:
1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡Round-Robin Load Balancer**)。如果该节点巧合地拥有请求所适用的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收复并传递给客户端。
2. 首先将所有来自客户端的请求发送到路由选择层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅充当分区感知负载平衡器
3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡Round-Robin Load Balancer**)。如果该节点巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收复并传递给客户端。
2. 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡
3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
在所有情况下,关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化?
以上所有情况中的关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化?
![](img/fig6-7.png)
@ -275,39 +270,37 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的权威映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区改变了所有权或者添加或删除了一个节点ZooKeeper就会通知路由层,以使路由信息保持最新状态。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的可靠映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变,或者集群中添加或删除了一个节点ZooKeeper就会通知路由层使路由信息保持最新状态。
![](img/fig6-8.png)
**图6-8 使用ZooKeeper跟踪分区分配给节点。**
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了一个路由层,如[图6-8](img/fig6-8.png)所示。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server**实现和mongos守护进程作为路由层。
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
Cassandra和Riak采取不同的方法他们在节点之间使用**八卦协议gossip protocal**来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Cassandra和Riak采取不同的方法他们在节点之间使用**流言协议gossip protocol** 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由选择它会从集群节点了解路由变化【32】。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由层它会从集群节点了解路由变化【32】。
当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些分区并不像分配给节点那么快,所以为此使用DNS通常就足够了。
当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些地址并不像分区的节点分布变化的那么快,所以使用DNS通常就足够了。
### 执行并行查询
到目前为止,我们只关注读取或写入单个键的非常简单的查询(对于文档分区的二级索引,另外还有分散/聚集查询。这与大多数NoSQL分布式数据存储所支持的访问级别有关。
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing**关系数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大部分数据集的查询尤其受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很强的商业重要性,它收到了很多商业利益。我们将在[第10章](ch10.md)讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题由于分析有很重要的商业意义可以带来很多利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
## 本章小结
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。如果您有太多的数据,在单台机器上存储和处理不再可行,则分区是必要的。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行再分区。
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。数据量非常大的时候,在单台机器上存储和处理不再可行,则分区十分必要。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行再分区。
我们讨论了两种主要的分区方法:
***键范围分区***
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常按照排序顺序访问密切相关的键,则存在热点的风险。
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常访问相邻的主键,则存在热点的风险。
在这种方法中,当分区变得太大时,通常将分区分成两个子分区,动态地再平衡分区。
@ -317,14 +310,16 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。也可以使用动态分区。
混合方法也是可行的,例如使用复合主键:使用键的一部分来标识分区,而使用另一部分作为排序顺序。还讨论了分区和二级索引之间的相互作用。次级索引也需要分片,有两种方法:
两种方法搭配使用也是可行的,例如使用复合主键:使用键的一部分来标识分区,而使用另一部分作为排序顺序。
* 按文档分区(本地索引),其中辅助索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取辅助索引需要在所有分区之间进行分散/收集。
* 按关键词分区(全局索引),其中二级索引是分开分开的。辅助索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新二级索引的多个分区;但是,可以从单个分区提供读取。
我们还讨论了分区和二级索引之间的相互作用。次级索引也需要分区,有两种方法:
最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区感知负载平衡到复杂的并行查询执行引擎。
* 按文档分区(本地索引),其中二级索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取二级索引需要在所有分区之间进行分散/收集。
* 按关键词分区(全局索引),其中二级索引存在不同的分区的。辅助索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新多个分区中的二级索引;但是可以从单个分区中进行读取。
按照设计,每个分区大部分是独立运行的——这就是允许分区数据库扩展到多台机器的原因。但是,需要写入多个分区的操作难以推理:例如,如果写入一个分区成功,但另一个分区失败,会发生什么情况?我们将在下面的章节中讨论这个问题。
最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区负载平衡到复杂的并行查询执行引擎。
按照设计,多数情况下每个分区是独立运行的 — 这就是分区数据库可以扩展到多台机器的原因。但是,需要写入多个分区的操作结果可能难以预料:例如,如果写入一个分区成功,但另一个分区失败,会发生什么情况?我们将在下面的章节中讨论这个问题。