Design Data-Intensive Applications 读书笔记十六 第六章:键值数据分区
分区
上一章,我们讨论了备份,在多个节点上部署相同数据的备份,对于非常大的数据库或者查询量很高的数据库,这是不够的,这章我们来讨论分库,如何将数据划分成不同的部分 partitions,也叫分区sharding.
一般而言,每一片数据(一行,一条记录,或者一个文档)都属于特定的一个分区。每个分区都是一个小数据库,同时数据库可以同时操纵多个分区。
分区的最主要原因就是可扩展。不同的分区可以放在一个无交集的集群的不同节点上。因此大型数据库可以跨越多个网盘,查询负载也可以跨越多个处理器。
每个查询可以独立的在所在分区进行,所以可以通过添加节点来提高查询吞吐量。大量复杂的查询可以跨过多个节点并行执行,尽管这实现起来很困难。
这章我们首先会查看不同的分区方式,数据是如何插入到分区中的;然后会讨论再平衡,这对于往集群中添加和删除节点是必要的。最后我们会看到数据库是如何将请求导向至正确的分区以及执行查询。
分区和备份
分区通常是和备份同时使用的,所以一个分区的不同备份会存储在不同的节点上,一个节点也会存储不同的分区。如果使用主从备份模型,分区和备份的关系如图6-1。每个分区的主分区安排给一个节点,从分区安排给其他节点。每个节点可能是一些分区的主分区,以及其他分区的从分区。备份相关,在第5章以及讨论过了,分区和备份相互独立,所以这章就忽略备份。
键值数据分区
分区的一个方法是将键的范围划分为一系列连续区间,例如百科全书的卷的划分,如图6-2。如果你知道了键的边界,那么你可以知道在那个分区能找到指定值,你就可以直接访问该分区。
键的区间的跨度,不需要平均,因为数据可能不是均匀分布的。例如百科全书,第一卷包含以A,B开头的内容,但是第12卷包含T至Z的内容,为了让数据能平均分布,需要调整区间的宽度。分区可以手动指定,一些软件也可以自动划分。BigTable的分区策略,等价于 HBase, RethinkDB, 和2.4版本前的MongoDB。
在每个分区内,我们可以让键有序排列(类似于SSTable和LSM-Tree)。好处就是可以容易的进行范围扫描,可以在一次查询中使用多列索引获取到多个相关值。例如:一个应用要存储网络传感器信息,键是时间戳。可以很容易使用范围扫描,因为能很容易地就获取到指定月份内的数据。
使用键区间来分区的缺点是特定的读取模式会导致热点。如果键是时间戳,那么分区就对应时间,比如一个分区对应一天,如果我们每次测量发生时都将传感器数据写入数据库,那么所有的写入都会传送至相同的分区(对应的一天),会出现一个分区过载,但是其他分区闲置的情况。
为了避免这种情况,需要用其他信息作为第一主键,例如在时间戳前面加上传感器的名字。如果你同时有多个活动的传感器,那么写入会更加均匀的分布到多个分区。如果你想查询一个时间范围内的多个传感器的数据,可以对每个传感器单独执行一个查询。
哈希键分区
因为不均衡和热点的风险,很多分布式数据存储系统使用哈希函数来决定一个键该放到哪个分区。
一个好的哈希函数是输入非均衡的值,输出均匀分布的值。例如一个输出32位bit的韩系函数,输入一个字符串,输出的值应该看上去是0至2^32-1之间的随机值,即便输入值非常类似,输出值也应该是均衡分布。
只是为了分区,函数不需要强加密功能, Cassandra 和 MongoDB 使用 MD5, Voldemort 使用 Fowler–Noll–Vo 。很多编程语言用内置的简单哈希函数,它们可能不适用于分区: Java的 Object.hashCode() 和 Ruby的 Object#hash,相同的值在不同进程中会输出不同的值。
一旦有了合适的哈希函数,就可以给每个分区分配一个哈希值的范围。每个键都对应一个哈希值,落在哪个范围就分配至哪个分区,如图6-3:
这个方法对于分布式的键很公平,分区边界可以很均匀,或者是伪随机的。
但是使用键的哈希值来分区,丢失了按键的范围来分区的一个特性:能有效地进行范围查询。相邻的键可能散落在所有的分区,它们之间的顺序丢失了。在MongoDB中,如果你使用哈希方法来进行分区,所有的查询都会被发送至所有的分区。 Riak , Couchbase , Voldemort都不支持对于主键的范围查询。
Cassandra在两种分区方式之间做了平衡。Cassandra使用多个列来作为一张表的主键, a compound primary key,混合主键。主键的第一部分用来计算哈希值来确定分区,其他的部分作为Cassandra中SSTable的用来排序的联合索引。因此查询不能用混合主键的第一列来做范围查询,但是如果制定了第一列的值,可以在其他列上做范围查询。
混合索引的方案适用于一对多的数据模型。例如一个社交网站,如果使用 (user_id, update_timestamp)来做主键,那么你可以很容易地获得一个用户在某个时间段的按照时间顺序的更新。不同用户的更新可能分布在不同的分区,但是每个用户的更新都会按照时间顺序存储在一个分区上。