你真的理解分布式数据的分区吗?

分布式数据存储是指将数据分散存储在多个节点或服务器上的技术。而分区是将数据划分为逻辑上的片段或部分,每个分区可以在分布式系统中的不同节点上存储。分区主要是为了可扩展性。不同的分区可以放在不共享集群中的不同节点上,可以帮助实现负载均衡、高可用性、并行处理、数据本地性优化以及系统的扩展性和灵活性。通过合理的分区策略,可以充分发挥分布式存储系统的优势,提供高效、可靠的数据存储和处理能力。

  1. 数据划分与负载均衡:通过将数据划分为多个分区,可以将数据在分布式系统中均匀地分散存储在不同的节点上。这样可以实现负载均衡,避免某个节点负载过重,提高系统的性能和可扩展性。
  2. 高可用性和容错性:将数据划分为多个分区后,每个分区可以在不同的节点上复制多个副本,以实现数据的冗余和容错性。如果某个节点发生故障或网络中断,其他节点上的副本仍然可用,确保数据的高可用性和可靠性。
  3. 并行处理和查询优化:分区可以使分布式系统能够同时处理多个分区上的数据,实现并行计算和处理。这对于大规模数据处理和复杂查询非常重要,可以提高处理速度和性能。
  4. 数据本地性和局部性优化:通过将数据划分为分区,并将每个分区存储在靠近数据的节点上,可以实现数据本地性和局部性优化。这意味着在执行查询或操作时,系统可以更快地访问和处理本地节点上的数据,减少网络延迟,提高数据访问效率。
  5. 扩展性和灵活性:分区可以使系统具备良好的扩展性和灵活性。当需要增加存储容量或处理能力时,可以通过增加节点和调整分区策略来实现系统的扩展,而不需要对整个系统进行大规模的改变或迁移。

分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。一个节点可能存储多个分区。每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
在这里插入图片描述
那么既然要对数据进行划分,放在不同的分区上,首先我们需要解决的一个重要问题就是如何划分数据?

为什么说这个问题很重要呢?

如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为数据偏斜。数据偏斜的存在使分区效率下降很多。加入有10个分区,在极端的情况下,所有的负载可能压在一个分区上,其余9个节点空闲的,瓶颈落在这一个繁忙的节点上。不均衡导致的高负载的分区 被称为热点。

避免热点最简单的方法是将记录随机分配给节点。但进而导致的一个很大的问题就是无法知道我们想要查询的值实际上是落在那个节点上,需要扫描全部分区才能获取结果,这种方式带来的性能损耗是不能接受的。

所以接下来介绍一下我们常见的集中数据分配策略:

分片策略

范围分区

每个分区指定一块连续的键范围(从最小值到最大值)。一个简单的比喻就是图书馆中某类书根据字母顺序有序排列
在这里插入图片描述
这种分区的好处是进行范围扫描非常简单,但可能会导致热点,比如字母A的书籍特别受欢迎,80%的用户访问都会集中在A类书籍,就会导致对应的机器负载变得很大。

为了解决热点的问题,我们可以在分片键增加干扰项:书籍+作者,但这样也会增加查询复杂度

散列分区

首先,对要存储的数据进行散列计算。通常使用散列函数(如MD5、SHA-1、SHA-256等)将数据转换为固定长度的散列值。散列函数具有将任意大小的输入转换为固定长度输出的特点,且输出值具有高度随机性。散列计算后,根据散列值的范围将数据映射到相应的分区。分区的数量通常是固定的,每个分区可以在分布式系统中的不同节点上存储。映射过程可以使用模运算或范围映射等算法,将散列值映射到特定的分区。将散列后的数据存储到对应的分区中。当需要访问特定数据时,再次对数据进行散列计算得到散列值,并通过映射算法确定存储该数据的分区。然后,通过访问对应的分区,从相应的节点上获取数据。

好的散列函数可以将将偏斜的数据均匀分布,当然代价就是失去高效的范围查询能力:使用了基于散列的分区模式,则任何范围查询都必须发送到所有分区
在这里插入图片描述
进而我们也可以想到,为了减少分区节点的增减对与映射的影响,可以采用一致性hash优化

我们可以思考一下如何负载倾斜与消除热点?

哈希分区可以帮助减少热点。但是,它不能完全避免:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。对于高热点数据,我们可以在主键的开始或结尾添加一个随机数(比如0-100),但查询时必须从100个分布中读取数据并合并,同时还要记录哪些数据是属于热点数据,普通数据就没必要这么操作了,毕竟也是有一定代价的。

分片与次级索引

接下来我们应该考虑另外一个问题了,为了提高查询效率,很多数据库都会支持刺激索引来满足不同场景下的不同查询条件,分片时我们应该如何处理次级索引的更新问题呢?

有两种用二级索引对数据库进行分区的方法:基于文档的分区(document-based)和基于关键词(term-based)的分区。

按文档的二级索引

举一个列子,假设有一个存储汽车信息的数据表,每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区(例如,分区0中的ID 0到499,分区1中的 ID 500到999等)。如果用户想按照颜色进行搜索,那么按文档的二级索引表现形式如下:
在这里插入图片描述
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的二级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。文档分区索引也被称为本地索引。如果要按照颜色进行查询,则需要将查询发送到所有分区,并合并所有返回的结果。这种查询分区数据库的方法有时被称为分散/聚集,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大。

根据关键词(Term)的二级索引

它与本地索引的区别如下:
在这里插入图片描述
它构建一个覆盖所有分区数据的全局索引,而不是给每个分区创建自己的次级索引。全局索引也必须进行分区,但可以采用与主键不同的分区方式。

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

理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持。在实践中,对全局二级索引的更新通常是异步的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。

分区再平衡

我们的数据库可能会出现各种情况,比如机器的CPU、磁盘达到限制了,不得不需要扩展更多的磁盘CPU,机器出现故障需要替换等等,所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为再平衡。

再平衡通常都要满足一些最低要求:

  • 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
  • 再平衡发生时,数据库应该继续接受读取和写入。
  • 节点之间只移动必须的数据,以便快速再平衡,并减少网络和磁盘I/O负载。

平衡策略

我们首先需要知道一点,hash mod N,这种策略是绝对不行的,因为扩缩容会导致N数据的变化,N的变化会导致数据路由出错

那么我们就可以想到,是否可以创建固定数量的分区?当然可以,像redis就是这么干的,创建比节点更多的分区,只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 —所以在传输过程中,原有分区仍然会接受读写操作。
在这里插入图片描述
但需要注意的是,要选择一个合适大小的分区数量,如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。

并且这种对与hash分片的数据是比较合适的,对于使用键范围分区的数据库,具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。

对了解决范围分区数据库的问题,我们想到是否可以支持动态分片呢?

当然可以,如HBase,MongoDB,按键的范围进行分区的数据库会动态创建分区。当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似。每个分区分配给一个节点,每个节点可以处理多个分区,就像固定数量的分区一样。大型分区拆分后,可以将其中的一半转移到另一个节点,以平衡负载。在HBase中,分区文件的传输通过HDFS(底层分布式文件系统)来实现

动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值

需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小,直到达到第一个分区的分割点,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。为了解决这个问题,HBase和MongoDB允许在一个空的数据库上配置一组初始分区(这被称为预分割(pre-splitting))

动态分区不仅适用于数据的范围分区,而且也适用于散列分区。从版本2.4开始,MongoDB同时支持范围和哈希分区,并且都是进行动态分割分区。

上面无论是固定数量分片还是动态分片,都与节点数量无关(动态分片是与分区数据量有关,单个分片数据量增长到一定程度,会分裂),那么我们可不可以支持分区数量随节点数量变动呢?

也可以,这种就是按照节点比例进行分区。每个节点具有固定数量的分区。这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。

当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时,新节点最终从现有节点获得公平的负载份额。

随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义

然后我们就需要考虑是手动还是自动平衡

全自动重新平衡更方便

手动平衡更安全。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。特别是自动化与自动故障检测相结合可能十分危险,可能存在误判

请求路由

随着分区重新平衡,分区对节点的分配也发生变化,客户端请求时,怎么知道应该去访问哪个节点的数据?

一般来说,有以下三种形式:

  1. 允许客户联系任何节点(例如,通过循环策略的负载均衡(Round-Robin LoadBalancer))。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。(redis)
  2. 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡。(mongos)
  3. 要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
    在这里插入图片描述
    许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据。HBase和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构,但它依赖于自己的配置服务器(config server) 实现和mongos守护进程作为路由层。

Cassandra和Riak采取不同的方法:他们在节点之间使用流言协议(gossip protocol) 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像ZooKeeper这样的外部协调服务的依赖。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值