-
什么是分区?
**分区(partition)/分片(sharding)**是一种将大型数据库分解成小型数据库的方
💡 **网络分区(network partitons,netsplits)**是节点之间网络故障的一种。 -
分区的目的
- 可伸缩性。将数据和查询负载可以分布在多个节点上。
分区与复制
分区通常与复制结合使用,这样每个分区的副本存储在多个节点上。即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
图 6-1 组合使用复制和分区:每个节点充当某些分区的主库,其他分区充当从库。
键值数据的分区
分区公平:节点公平分享数据和负载
分区不公平:
- 偏斜(skew) : 一些分区比其他分区有更多的数据或查询
- 热点(hot spot) :由不均衡导致的高负载的分区被称为热点
如何避免热点?
-
将记录随机分配给节点
- 缺点:取特定值的时候,不知道它在哪个节点上,只能并行地查询所有节点
根据键的范围分区
每个分区指定一块连续的键范围(从最小值到最大值),键在哪个范围内就属于哪个分区。
优点:
- 范围扫描简单。每个分区中,可以按照一定的顺序保存键。
缺点:
- 键的范围不一定均匀分布,因为数据也可能不均匀分布。如果要均匀分配数据,分区边界需要依据数据进行调整。
根据键的散列分区
使用散列函数来确定给定键的分区。一个好的散列函数可以将偏斜的数据均匀分布。
优点:
- 可以在各个分区之间公平地分配键
缺点:
- 失去了键范围分区的范围查询的能力
负载偏斜和热点消除
哈希分区可以帮助减少热点,但是,无法完全避免热点。
💡 场景:在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴。这个事件可能导致同一个键的大量写入(键可能是名人的用户 ID,或者人们正在评论的动作的 ID)。哈希策略不起作用,因为两个相同 ID 的哈希值仍然是相同的。如今,大多数系统都无法自动补偿这种高度偏斜的负载,需要应用程序来减少偏斜。例如,如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为 100 种不同的主键,从而存储在不同的分区中。
然而,将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有 100 个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来说是不必要的开销。因此,你还需要一些方法来跟踪哪些键需要被分割。
分区与次级索引
键值分区方案依赖于键值数据模型。如果只通过主键访问记录,可以从该键确定分区;如果设计次级索引(次级索引并不能唯一的标识记录),该如何进行分区?基于文档的分区(document-based) 和 基于关键词(term-based)的分区。
基于文档的次级索引进行分区
图 6-4 基于文档的次级索引进行分区
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖分区中的文档。文档分区索引也被称为本地索引。
在搜索时,需要查询所有分区,并合并所有返回的结果。这种查询分区数据库的方法有时称为分散/聚集(scatter/gather) ,并且可能会使次级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾步延迟放大。
基于关键词的次级索引进行分区
思想:构建一个覆盖所有分区数据的全局索引;而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
图 6-5 基于关键词对次级索引进行分区
分区再平衡
背景:随着时间的推移,数据库会有各种变化:
- 查询吞吐量增加,需要增加CPU
- 数据集大小增加,需要增加磁盘和内存
- 机器出现故障,需要添加机器
所有这些改变都需要数据和请求从一个节点移动到另一个节点。
再平衡(rebalancing) :将负载从集群中的一个节点向另一个节点移动的过程称为再平衡。
再平衡的最低要求:
- 公平。 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
- 可用性。再平衡发生时,数据库应该继续接受读取和写入。
- 效率。 节点之间只移动必须的数据,以便快速再平衡,并减少网络和磁盘 I/O 负载。
再平衡策略
反面教材:hash mod N
将散列分成不同的范围,并将每个范围分配给一个分区。
缺点:
- 若节点数量变化,大多数键需要从一个节点移动到另一个节点。
固定数量的分区
创建比节点更多的分区,并为每个节点分配多个分区。
优点:
- 若节点数量变化,只有分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。
图 6-6 将新节点添加到每个节点具有多个分区的数据库集群。
💡 在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是你可以拥有的最大节点数量,所以你需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。动态分区
分区边界是动态的。当分区增长超过配置的大小时,会被分成两个区;反之,当大量数据被删除并且分区缩小到某个阈值以下时,则可以将其与相邻的分区合并。
优点:
- 分区数量适应总数据量。
预分割(pre-spliting) :在一个空间的数据库上配置一组初始分区。
动态分区适用于:数据的范围分区、散列分区
按节点比例分区
动态分区:分区的数量与数据集的大小成正比; 固定数量的分区:每个分区的大小与数据集的大小成正比。 这两种分区中,分区的数量都与节点的数量无关。
每个节点具有固定数量的分区。这种情况下,每个分区的大小与数据集大小成正比,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。
请求路由
**服务发现(service discovery)**问题:进行分区后,客户端发请求时,如何知道连接哪个节点?分区再平衡之后,分区对节点的分配也发生了变化,客户端如何知道该变化?
服务发现的方案:
- 允许客户端联系任何节点(例如:循环策略的负载均衡,即 Round-Robin Load Balancer)。如果该节点恰好有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
- 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡。
- 要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
以上所有情况中的关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区 - 节点之间的分配关系变化?
图 6-7 将请求路由到正确节点的三种不同方式。
关键是:所有参与者需要达成共识——否则请求将被路由到错误的节点,得不到正确的处理。
许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 图 6-8 所示。 每个节点在 ZooKeeper 中注册自己,ZooKeeper 维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。 只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper 就会通知路由层使路由信息保持最新状态。
图 6-8 使用 ZooKeeper 跟踪分区分配给节点。
执行并行查询
通常用于分析的 **大规模并行处理(MPP, Massively parallel processing)**关系型数据库产品可以支持复杂的查询类型。MPP 查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。