是一个冗余过程。冗余提高了可用性,可以有效地平衡读取负载。数据的分区是一个将整体转化为部分的过程。这种拆装就像你有很多书,但是你的书架放不下,那么多加几个书架来收纳也是有道理的。拆分整体并将其本地存储在多个较小的空间中。这个思路映射到电脑上也是一样的。当数据量过大,单个存储节点不足以存储数据时(没有更大容量的磁盘或者太贵),人们想要继续存储和规律化就需要对数据集进行拆解。这就是数据分区的意思,它是为了提高数据系统的可扩展性而引入的一种技术方法。如何分区?分区的关键是采用统一的规则,在读取的时候可以计算出数据放到哪个节点,读取数据到哪个节点。为了实现这几点,目前有三种分区方式:根据key的范围分区在存储数据的时候,我们将数据中的某个字段作为分区key,按照这个字段的范围进行分区,比如自增id值,0-10000存储在A节点,10001-20000存储在B节点。基于这个规则,我们可以高效的访问分区中的数据,自然支持按区间查找(key存储为ordered),只要范围的范围只在一个分区中,范围搜索只会访问一个分区,除非搜索范围跨越多个分区。但是问题在于,当某段时间数据写入出现热点时,比如大量写入0-100000的key,而10001-20000的key很少时,会导致dataskew(datapartition的大小不大Balanced)根据key的hash进行分区对于dataskew,自然而然的想到了一个高效的hashfunction将数据存储在不同的partitions中。只要哈希函数一致,同一个key就会映射到同一个partition。因此,它也可以解决分区的关键问题,但是由于散列的问题,很难自然地搜索范围范围。有些数据库会将范围搜索的请求发送到所有分区,并行处理后聚合返回结果。但毫无疑问,会频繁产生大量的请求。虽然可以有效解决数据倾斜问题,但是没有办法完全避免这种热点数据。比如一个大V的用户,总是有很多粉丝,每天都会产生很多数据,经过key散列后的数据仍然会存放在同一个分区中,这样不仅会造成数据倾斜,还会造成热点数据的频繁访问,读写负载会分布不均。以上两种分区在range+hash模式下的优缺点恰好是互补的,可以考虑将两者结合起来。比如用数据记录的两个关键字段id和timestamp作为key,先用idhash存储在不同分区,再用timestamp按范围分区,这样就弥补了优势和两者的劣势在一定程度上。但是,热点数据的问题仍然没有完全解决。这时候可以考虑其他方面来解决热点数据的问题,比如为热点数据建立缓存结构。分区方式看似完美地扩展了数据的存储空间,但同时也引入了另一个复杂性,即在查询数据时,如果恰好数据不在同一个分区中,则需要访问多个不同的分区。会增加请求的延迟,或者当我们需要对关系模型中的数据进行join时,因为数据在不同分区的不同表中,join会非常困难,增加了多表查询的复杂度某种程度上来说,一个折衷的方案是,从业务的角度,将同时加入或读取的数据尽量放在同一个分区上,以减少跨分区的性能损失调用,这相当于减少了磁盘寻道。寻址次数也是如此,就是减少最耗时操作的出现次数。如何设计二级索引的分区?以上三种分区方案只对主键进行分区,即一条记录的唯一标识。但是从数据库功能的角度来说,我们还需要能够对字段进行索引,以便灵活高效的查询。此类索引称为二级索引。那么二级索引在分区数据库的设计中应该如何实现呢?通常有两种设计,局部索引和全局索引。当本地索引在这个分区上写入和读取二级索引时,我们说这样的二级索引是一个本地索引,也就是说每个分区上的二级索引文件只存储了这个分区上的数据。索引数据。这样做的好处是写数据的时候更新一条记录的二级索引非常方便,因为关于这条记录的二级索引都在这个节点上。但是在用二级索引读取一条记录的时候,我们没有办法知道这条记录在哪个分区,所以需要进行并行查询,然后合并查询结果,这无疑会放大读取的延迟。全局索引与全局索引相反,即对于一个二级索引,它的所有字段都在同一个分区(不同的全局索引在不同的分区),当我们查询一个二级索引时,我们只能去只有一个节点读取数据,不需要并行查询,所以读取的效率会很高,但是在写入数据的时候,因为一条记录涉及的二级索引可能在多个,所以需要操作多个分区,这涉及到分布式事务一致性等问题,大大增加了复杂度,影响性能。全局索引读取数据时,如何找到索引所在的分区呢?答案是我们可以对全局二级索引使用相同的分区策略,比如范围分区、散列分区或者散列范围分区。不同的分区策略也会影响其对于范围查询的效率。分区重新平衡多个节点上有多个分区。随着数据负载的增加,每个分区的大小会不断增加,从而造成隐患。一旦某个节点发生故障,其上的所有分区都会发生故障,占很大一部分数据将失效。比如现在在集群中增加或者移除一个新的节点,需要把数据均匀的转移到新的节点上(新的节点不转移数据,但是接受新的写入。请求是否可行?这样做会使写入请求不均衡,一段时间内请求不同节点,大量请求新节点会造成负载不均衡)。以上问题归纳为引入分区重平衡特性的原因,即为了可用性和可扩展性,分区重平衡是必不可少的特性。固定分区数分区数应与节点数不同,以方便其扩展。原因是:假设分区数与节点数相同,那么通过对节点数取模来确定数据分配到哪个分区,这种取模会带来隐患。当我们添加或删除节点时,模数发生变化,之前的数据无法路由到正确的分区,因此必须进行rebalancing,所有数据重新分区(类似于hashmaprehashing),这会导致所有数据被处于迁移状态,整个集群将不可用。因此,我们必须将节点数和分区数解耦,在一个节点上分配固定数量的分区。比如在初始化集群的时候,指定了一个分区有1024个分区。现在有3个节点,那么每个节点应该有341个分区。分区。最后一个节点可能有342个分区。这时,添加一个节点。当集群有4个节点后,我们需要对其进行分区和重新平衡。我们只需要在原来的三个节点上取一半的分区。这样,只有一半的数据在迁移过程中(这个比例可以通过复杂的算法动态调整),可以降低分区再平衡过程的复杂度。删除节点也是如此。节点上的分区可以均匀分布在其他节点上。固定数量的分区方案解决了节点数和分区数之间的耦合。我们可以对分区数取模,快速确定数据所在的分区,迁移前后保持相对分区不变。redis的集群模式就是采用这种方式重新平衡分区。动态分区数和固定分区数不能很好地处理热点问题。当一个分区存储的数据量远大于其他节点时,这是不合理的。由于节点数量是固定的,这些数据无法迁移。因此引入了类似B+树节点分裂合并的概念。我们还可以根据数据量拆分和合并分区。当一个分区的负载高于指定的阈值时,我们会将其拆分为两个分区,这样分区的数量就会发生变化。这种方案不能使用分区号取模的方法进行数据哈希,只能根据key范围或者Hash分区。不过值得,他可以有效的平衡各个分区的数据负载。按节点比例分区和动态分区也有一个缺点,即分区数与数据量成正比。数据量的增加会不断增加新的分区,分区数量的增加会成为新的性能瓶颈。因此,引入了一种新的方案,将上述两种方案结合起来。当节点数固定时,分区数也固定,每个节点上的分区数永远是一个固定数。当数据负载增加时,其分区的大小将不断增加。当添加或删除新节点时,将随机选择某些节点上的某些分区(使用一些复杂的策略)进行分裂。分为两个分区,一半移动到新节点,另一半留在原地。这样做的好处是分区数量受节点数量限制,无限增长不会成为瓶颈。手动和自动rebalancing数据系统是否应该自动完成分区rebalancing?这无疑是方便的,很多数据库都在使用它,但是它的复杂度确实很高。例如,当节点分区平衡发生时,当系统检测到节点不可用时,会导致级联崩溃,触发移除节点的逻辑,并触发新的分区平衡导致整个集群崩溃。基于简单的原则,由管理员手动分区重新平衡是一种折衷。请求路由引入了复杂的分区方案后,客户端如何知道请求的数据在哪个分区上?一般有三种方式:随机请求一个节点,分区会判断数据是否在自己上,如果是直接返回结果,否则将请求转发给拥有数据的节点,并返回结果。所有请求都访问路由层,该层具有足够的元数据来做出决定并将请求转发到适当的节点。客户端本身可以感知分区节点的分配关系,直接请求对应的分区。无论哪种方式,这只是由谁来完成路由决策逻辑的问题。对于客户端请求路由,客户端需要感知分区与节点映射关系的变化,这通常是基于分布式共识组件完成的,如zk、etcd等。分区节点启动时,注册自己的元数据与zk,然后zk将信息传播给订阅了这个变化的客户端,客户端会更新分区和节点的映射关系,有请求时直接访问对应的分区。.优点是网络调用次数最少,效率最高,但依赖第三方共识组件。另一种不同的做法是让客户端请求任意一个节点,分区节点根据自身持有的元数据信息判断请求的数据是否在自己的分区中。如果是,则直接处理并返回。如果不是,它会将请求转发到所属分区。请求被处理并返回给客户端。这样做的好处是它不依赖共识组件,但在最坏的情况下,对网络的调用次数会翻倍,从而影响性能。并行查询处理当多个字段需要联合查询时,分区数据库应该怎么做?我们一直在讨论如何通过单独的key进行查询,但是对于针对数据仓库的大量查询,更复杂的join和更多的Table操作。分区数据库性能好吗?这就涉及到分布式数据库分区并行查询的问题。委托给相应的partition,对结果做最后的merge。这个过程中有很多细节,我会在后面的文章中详细介绍。
