第六章:分区
对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行分区(partitions),也称为分片(sharding)。
分区(partition),在MongoDB,Elasticsearch和Solr Cloud中被称为分片(shard),在HBase中称之为区域(Region),Bigtable中则是 表块(tablet),Cassandra和Riak中是虚节点(vnode), Couchbase中叫做虚桶(vBucket).但是分区(partition) 是约定俗成的叫法。
分区主要是为了可扩展性。不同的分区可以放在不共享集群中的不同节点上。
分区与复制
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如下:
图 - 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者
每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
键值数据的分区
分区目标是将数据和查询负载均匀分布在各个节点上。
如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为偏斜(skew)。不均衡导致的高负载的分区被称为热点(hot spot)。
避免热点最简单的方法是将记录随机分配给节点。但是它有一个很大的缺点:当你试图读取一个特定的值时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。
根据键的范围分区
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值)。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求。
键的范围不一定均匀分布,因为数据也很可能不均匀分布,为了均匀分配数据,分区边界需要依据数据调整。
在每个分区中,我们可以按照一定的顺序保存键。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录。
Key Range分区的缺点是某些特定的访问模式会导致热点。
根据键的散列分区
由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
一个好的散列函数可以将将偏斜的数据均匀分布。
出于分区的目的,散列函数不需要多么强壮的加密算法:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。
有一个合适的键散列函数,可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。
图 - 按哈希键分区
分区边界可以是均匀间隔的,也可以是伪随机选择的(该技术有时也被称为一致性哈希(consistent hashing))。
使用Key散列进行分区,就失去了高效执行范围查询的能力。
Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作Casssandra的SSTables中排序数据的连接索引。
负载倾斜与消除热点
在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
大多数数据系统无法自动补偿这种高度偏斜的负载,因此应用程序有责任减少偏斜。如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100种不同的主键,从而存储在不同的分区中。
将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有100个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来是不必要的开销。
分片与次级索引
辅助索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户123的所有操作,查找包含词语hogwash的所有文章,查找所有颜色为红色的车辆等等。
次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。
次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:基于文档的分区(document-based)和基于关键词(term-based)的分区。
按文档的二级索引
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是字段(field),关系数据库中这些是列(column) )。 如果您声明了索引,则数据库可以自动执行索引。
图 - 按文档分区二级索引
每个分区维护自己的二级索引,仅覆盖该分区中的文档。文档分区索引也被称为本地索引(local index)。
根据关键词(Term)的二级索引
可以构建一个覆盖所有分区数据的全局索引,而不是给每个分区创建自己的次级索引(本地索引)。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
图 - 按关键词对二级索引进行分区
我们将这种索引称为关键词分区(term-partitioned),因为我们寻找的关键词决定了索引的分区方式。关键词(Term) 来源于来自全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
分区再平衡
随着时间的推移,数据库会有各种变化。
- 查询吞吐量增加,所以您想要添加更多的CPU来处理负载。
- 数据集大小增加,所以您想添加更多的磁盘和RAM来存储它。
- 机器出现故障,其他机器需要接管故障机器的责任。
所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为再平衡(reblancing)。
无论使用哪种分区方案,再平衡通常都要满足一些最低要求:
- 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
- 再平衡发生时,数据库应该继续接受读取和写入。
- 节点之间只移动必须的数据,以便快速再平衡,并减少网络和磁盘I/O负载。
平衡策略
反面教材:hash mod N
使用mod(许多编程语言中的%运算符)做分配问题:如果节点数量N发生变化,大多数密钥将需要从一个节点移动到另一个节点,这种频繁的举动使得重新平衡过于昂贵。
固定数量的分区
创建比节点更多的分区,并为每个节点分配多个分区。
图 - 将新节点添加到每个节点具有多个分区的数据库群集
如果一个节点被添加到集群中,新节点可以从当前每个节点中窃取一些分区,直到分区再次公平分配。如果从集群中删除一个节点,则会发生相反的情况。
只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。
通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在Riak,Elasticsearch,Couchbase和Voldemort中使用了这种再平衡的方法。
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区,但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。
当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
动态分区
对于使用键范围分区的数据库,具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。
按键的范围进行分区的数据库(如HBase和RethinkDB)会动态创建分区。当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。
动态分区的一个优点是分区数量适应总数据量。
动态分区不仅适用于数据的范围分区,而且也适用于散列分区。
按节点比例分区
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说,每个节点具有固定数量的分区。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在Cassandra中,默认情况下,每个节点有256个分区),新节点最终从现有节点获得公平的负载份额。
运维:手动还是自动平衡
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
这种自动化与自动故障检测相结合可能十分危险。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对已经超负荷的节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。
出于这个原因,再平衡的过程中有人参与是一件好事。这比完全自动的过程慢,但可以帮助防止运维意外。
请求路由
当客户想要发出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。
这个问题可以概括为 服务发现(service discovery) 。
这个问题有几种不同的方案:
图 - 将请求路由到正确节点的三种不同方式
- 允许客户联系任何节点(例如,通过循环策略的负载均衡(Round-Robin Load Balancer))。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
- 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡。
- 要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据。
图 - 使用ZooKeeper跟踪分区分配给节点
每个节点在ZooKeeper中注册自己,ZooKeeper维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变,或者集群中添加或删除了一个节点,ZooKeeper就会通知路由层使路由信息保持最新状态。
HBase,SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构,但它依赖于自己的配置服务器(config server) 实现和mongos守护进程作为路由层。
Cassandra和Riak采取不同的方法:他们在节点之间使用流言协议(gossip protocol) 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点。
执行并行查询
通常用于分析的大规模并行处理(MPP, Massively parallel processing) 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。