文章目录
前言
什么情况下需要对数据进行分区?
海量数据或者IO压力非常大的时候。
分区也叫分片。在不同系统有不同的称呼:MongoDB,Elasticsearch,SolrCloud-shard;HBase-region;Bigtable-tablet;Cassandra-vnode;Couchbase-vBucket。
分区的定义通常是指每一条数据只属于某个特定的分区。分区的主要目的是提高可扩展性。
数据分区与数据复制
数据分区与数据复制通常结合使用,即每个分区在多个节点都存有副本。一个节点可能存在多个分区。
键-值数据的分区
面对海量数据,如何决定哪些记录放在哪个节点上?
分区的主要目标是将数据和查询负载均匀分布在所有节点上。所以理论上10个节点应该能够处理单节点的10倍数据量。如果分区不均匀,就会发生倾斜
,产生系统热点
。
基于关键字区间分区
方法:为每个分区分配一段连续的关键字或关键字区间范围。百科全书/字典 按照关键字区间分区。每个分区区间长度不是均匀的(区间密度不同),分区边界理应适配数据本身的分布特征。分区边界可以手动确定,或由数据库自动选择(采用这个策略:Bigtable,Bigtable开源版HBase,RethinkDB)。
优点:每个分区内可以按照关键字排序,这样能轻易支持区间查询。
缺点:某些访问模式会导致热点。例如用时间戳分区,每天的写入会集中在一个分区。一种解决方法是使用时间戳意外的其他内容作为关键字第一项,时间戳作为第二项。
基于关键字哈希值分区
对于倾斜和热点问题,许多分布式系统采用了基于关键字哈希函数的方式分区。
好的哈希函数可以处理数据倾斜并使其均匀分布,即使两条输入字符串非常相似。对于同一个键要返回同样的值。Cassandra和MongoDb使用MD5,Voldemort使用Fowler-Noll-Vo函数。注意Java内置的Object.hashCode()函数适用于哈希表,同一个键在不同进程中会返回不同哈希值,这是不能当作数据库分区的哈希函数的。
将哈希函数的值域均分分配给每个分区。分区边界可以是均匀间隔,也可以伪随机选择(这种情况下,该技术有时被称为一致性哈希
)。
缺点:丧失了良好的区间查询特性。在MongoDB中,如果启用基于哈希的分片模式,区间查询会发送到所有分区上。而Riak/Couchbase/Voldemort不支持关键字上的区间查询。Cassandra做了一个折中,它可以声明复合主键,复合主键只有第一部分用于哈希分区。这样只要指定了第一列的值,它就可以支持相对高效的区间查询。
负载倾斜与热点
基于哈希的分区可以减轻热点,但无法完全避免,一个极端情况是大部分操作都针对同一个关键字,如社交媒体(微博等)的内容评论,cxk某条微博评论转发上亿。
至今系统仍然无法自动消除这种高度倾斜的负载,而只能通过应用层来减轻:如在关键字被确认为热点后,在关键字开头或结尾处添加一个随机数,这样就可以分配到不同的分区。但随之而来的问题是读取时需要并行查询所有的分区。
分区与二级索引
“键-值数据的分区”模型简单,查询时使用关键字确定分区,但如果查询涉及二级索引就无能为力了。而二级索引在关系型和文档型数据库都非常常见。许多键值存储(HBase,Voldemort)并不支持二级索引。二级索引是Slor,Elasticsearch等全文索引服务器的存在根本。
二级索引带来的主要挑战是它们不能规整地映射到分区中。有两种主要方法来支持对二级索引分区:基于文档,基于词条。
基于文档分区的二级索引
在这种索引方法中,每个分区维护自己的二级索引,索引只关心自己分区内的文档。文档一词提现了索引的粒度与范围。例如一个日期的二级索引,它会建立2019-01-01的条目,将分区内这个日期的数据ID添加到本条目内。
现在考虑写入,更新,删除过程。基于文档的二级索引进行包含目标文档ID的更新操作(一般不太可能进行二级索引上的更新,因为数量大,很危险)时,只需要处理本分区内的数据。但在利用二级索引读取时,客户端需要将查询请求发送到所有分区。这种查询方法也叫分散/聚集(scatter/gather)。这种二级索引查询代价很高,但还是广泛用于实践。如果查询时有2个二级索引,有一个索引将起不到查询优化的作用。
基于词条的二级索引分区
在这种索引方案中,对所有数据构建全局索引,而不是每个分区维护自己本地的索引。全局索引本身也是分区的,分区策略可以与数据关键字采取不同的分区策略。
基于词条的二级索引分区相比于基于文档分区的主要优点是:读取更高效,它不需要采用scatter/gather对所有分区执行查询,而是只需要向包含词条的那一个分区发出读请求即可(?获取到词条id后还是要执行一个查询请求?)。
基于词条的分区缺点:在写入时很复杂,单个文档更新时可能涉及多个二级索引,此时就涉及到节点之间的数据交互。在实践中,一般使用异步更新二级索引策略。
分区再平衡
问题来源:当数据库环境发生变化(业务负载增高,节点故障等),要将数据和请求进行迁移,这个迁移负载的成功称为再平衡(或动态平衡)。分区再平衡的要求:平衡之后,负载应该更均匀分布;再平衡执行过程中,数据库应保持可用;避免不必要的负载迁移。
动态再平衡策略
多种策略,逐一介绍:
为什么不用取模
取模即mod运算。当有3个节点时,关键字6处于节点0。当节点数N发生变化时,如N=5时,关键字6又属于节点1。这样当节点数N发生变化,所有分区数据几乎都要进行迁移,大大增加了再平衡的成本。
除非节点数不发生变化,否则不要使用取模。
固定数量分区
有一种简单的解决方案:创建远超实际节点数的分区数,然后为每个节点分配多个分区。例如对于10个节点的集群,数据库一开始就逻辑分为1000个分区。这样大约每个节点承担100个分区。现在要增加一个节点,N=11,新节点可以从每个现有的节点上匀走几个分区,直到再次达到全局平衡。这样一来,分区的总数始终没有变化,分区内的数据也不需要单独迁移(整体迁移即可)。
动态分区
对于采用关键字区间分区的数据库,如果边界设置又问题,最终可能会出现热点分区,形成热点后再手动去重新配置分区边界很繁琐(随着业务的演变配置可能又要失效)。因此一些数据库(HBase,RethinkDB)采用了动态创建分区。动态分区就是根据实际的数据量、吞吐量等系统参数动态配置分区。预先设定分区配置可认为是静态分区。
动态分区需要预先设定分区的最大容量,节点包的分区数量。分区容量大,发生分裂。分区容量小,合并分区。HBase里,分区文件的传输需要借助HDFS(底层分布式文件系统)。
动态分区的优点是分区数量自动适配数据总量。
动态分区存在一个起始问题。对于一个新建的数据库,若从一个分区开始,则直到该分区达到阈值前,读写操作都是由单节点处理。为了缓解该问题,HBase/MongoDB允许在一个空的数据库上配置一组初始分区。
动态分区适用:关键字分区,关键字哈希分区
按节点比例分区
动态分区中,分裂/合并使得分区数量和数据集的大小成正比关系;固定数量分区中,分区的大小与数据集的大小成正比。这两种模式,分区的数量都与节点数无关。
节点的操作粒度比分区大,因此按节点比例分区通常用于非常大的数据集。
Cassandra/Ketama采用第三种方式,使分区数与集群节点数成正比,即每个节点具有固定数量的分区。当节点数不变时,分区的容量与数据集大小保持正比的增长关系。这种方法会使每个分区的容量保持相对稳定。
当新增一个节点时,它从集群中随机选择固定数量的现有分区进行分裂。随机选择可能会带来不公平,但平均分区数较大时,则影响较小(Cassandra默认情况下,每个节点256个分区)。显然随机选择就需要分区容量具有一定的均匀性,所以它的前提要求是集群是基于关键字哈希分区的。
自动与手动再平衡操作
动态的平衡的一个重要问题:自动配置还是手动配置?
全自动式再平衡:系统自动决定何时将分区从一个节点迁移到另一个节点,不需要管理员任何介入。
纯手动:分区到节点的映射由管理员来显示配置。
混合:自动生成一个分区分配建议方案,由管理员确认。
再平衡的是一个非常昂贵的过程,同时伴随着不可预知的风险。所以通常选择手动或者混合方式。
请求路由
现在已经将数据集分布到多个节点上,那么当客户发送请求时如何知道应该连接哪个节点?如果发生了分区再平衡,分区与节点的对应关系还会变化。这是典型的服务发现
问题。
服务发现问题通常的解决方案:
- 允许客户端连接任意节点(如采用循环式的负载均衡器)。如果节点有数据,则处理请求;否则将请求转发到下一个合适的节点,接收答复并将答复返回给客户端.(想象一下数据本身就不存在,会如何?)
- 将所有客户端请求发送至一个路由层。路由层充当一个分区感知的负载均衡器。
- 客户端感知分区和节点的分配关系。此时客户端直接连接目标节点,不需要中介。
不管是以上哪种方法,核心问题都是:作出路由决策的组建(某个节点,路由层或客户端)如何知道分区与节点的对应关系及变化。
这一问题要求所有参与者都达成共识,否则请求被发送到错误的节点。许多分布式数据系统依靠独立的协调服务(如ZooKeeper)跟踪范围内的元数据。每个节点都在ZooKeeper注册自己,ZooKeeper维护了分区到节点的最终映射关系。其他参与者(路由层或分区感知的客户端)向ZooKeeper订阅此消息。一旦发生分区/节点改变,ZooKeeper会主动通知路由层,使之保持最新状态。
LinkedIn的Espresso使用Helix(底层ZooKeeper)进行集群管理,实现了路由层。HBase,SolrCloud,Kafka使用ZooKeeper跟踪分区分配情况。MongoDB依赖自己的配置服务器和Mongos守护进程充当路由层。
Cassandra/Riak采用不同的方法,它们在节点之间使用gossip协议来同步集群状态的变化。请求可以发送给任何节点,这种方式增加了节点复杂性,但它避免了对外部协调服务的依赖。
并行执行查询
读取/写入单个关键字这样的简单查询是大多数NoSQL分布式数据存储所支持的访问类型,而对于大规模并行处理(massively parallel processing,MPP)这一类主要用于数据分析的关系数据库,查询很复杂。MPP查询优化器会将复杂的查询分解成许多执行阶段和分区,以便在集群的不同节点上并行执行。例如全表扫描,并行执行获益颇多。