本篇为分布式系统数据分区设计章节总结,当一个数据系统存储的数据单位达到TB级别往往都会采用多个服务器机架存储;面对海量的数据集合和非常高的查询请求的压力,往往都会把这些压力分摊到多个数据分片中处理。很多数据库也支持数据分区,可以把分区看成电脑上分区不同的盘管理着不同的资料文件。一个大型的数据库可以拆分成多个小数据库,和上面篇讲解的数据按地理位置分布效果类似,分而治之。数据分区主要的目的就是为了数据做冗余存储和异地多活,防止因为一个数据节点故障导致不能正常访问数据,另外可以提高整体系统查询数据吞吐量,将查询请求路由到不同数据分片;设计数据分区主要的问题是如何进行数据分区?查询请求如何定位到数据所在的分区?数据如何均匀的分布?热点数据如何处理?
数据分区
数据复制和分区通常一起使用,把整体数据系统分成多个数据分区,每个分区节点都有分区的副本,每条记录属于特定的分区,而同样的数据内有在不同分区存在副本,这样就可以提高整体系统可容错性。分区主要设计目的就是为了将数据分散起来存储,并且能通过分区来负载查询请求压力,理论上如果有 10 个节点,那么相比单节点就可以提供 10 倍的读写吞吐量,也提高 10 倍的系统容错性,如下图:
像上图这样将数据分区到不同节点上做冗余存储,这样可以提高整体系统容错性,当某一个分区节点故障时,其他节点也能正常通过服务,但是这种模式会出现热点数据倾斜问题。数据倾斜,在极端情况下会导致分区效率严重下降,例如如果有 10 节点,一个热点数据节点,那么剩下的9个节点就为空闲状态,系统瓶颈就在最忙碌节点之上也会成为整个系统的热点。
避免热点数据方法就是将其数据记录分区将其分布到多个分区中,这就会有另外一个问题如何的访问这些数据?如何查找到需要的那条数据?这时就要设计一个数据分片规则来存储数据记录,将其分布到不同的节点中保存?
目前数据库相关的存储设计是简单的键和值数据模型,可以通过键计算哈希值,通过特定的算法将其分布到节点,这可以查看我之前写的一篇文章一致性哈希数据分片算法。但是哈希算法缺点不支持范围扫描,只能通过特定的哈希值来取决于具体存放位置,如果算法存在某些问题或者对应键计算处理的值不能满足均匀分布一样会带来另外一个后果数据分片不均匀。例如图书馆书籍按照26个字母进行排列分区,某些书架的区间比较大那么存储的书籍就越多,产生了分片不均匀的情况,这时就要设计一个合理的数据分界条件。
例如物联网传感器设备采集的数据,如果按照键存储分区可能会出现分布不均匀情况出现热点数据情况,但是可以按照时间段分区处理,而基于时间戳则可以对应一个时间范围来做,例如将每天的数据存储在为一个分区,我们可以通过键和时间戳一起组合使用来达到区间访问的效果,这叫做复合键。
二级索引分区
上面介绍分区的方式是通过记录的键进行的分区,还有一种就是通过记录类型进行分区和特定的属性进行分区。例如有一个二手车交易网站存储不同品牌的汽车的记录数据,这时平台查询要通过根据特征的查询操作,这时就可以通过属性进行分区和建立二级索引的方式。可以基于唯一的文档 ID 进行分区,ID 的范围分区,0 至 599 存放在 0 号区域其他以此类推,这时针对这些区域存储数据记录进行构建二级索引,可以是汽车的颜色和车龄等...这种方式的分区要求每个分区自己能针对所存储数记录的属性建立维护一个二级索引,并且每个分区都是独立维护自己的二级索引,索引数据来自本分区;其缺点也很明显,每次查询特定颜色的汽车时,要查询多个分区,并且最后汇总数据,如果是串行的方式很有可能出现尾部响应问题。
二级索引的另一种实现将其设置为全局二级索引,每个分区不需要维护本地分区索引,全局的索引方式也可以分区例如通过汽车的颜色进行分区,将颜色为红色的汽车收录到在一个索引color:red
词条中,索引本身也分区将其按照 26 个小写字母排列顺序分区,这种分区称之为词条分区,这种方式不需要再查询某个特定颜色的汽车时不需要搜索全局的分区,查找符合条件的数据;但是缺点明显是当写入一个新的数据记录要跨区操作涉及到分布式事务问题,索引的数据要更新速度会受影响。
动态分区
分区原则是在数据库创建时就要设计好其数量,这样就可以应对未来的容量需求,但是如果设计的太多会导致一部分节点空着没有充分利用。分区的实际大小应该与集群中的数据总量成正比,小了可以缩容,大了可以扩容,数据的数量肯定是不确定的这就给设计带来一个问题如何设计扩容的策略?
上面都根据键进行的分区策略,边界条件没有设置好可能会出现所有数据出现在少数几个分区里,而大部分分区却没有数据,导致数据倾斜到某几个分区节点上。业界一些有名的存储项目例如 HBASE 采用的动态分区策略,当数据超过某个阀值时进行拆分,当数据被删除时就自动调整合并一个分区,这和 LSM-Tree 的按大小策略扩容很相似,与 B-Tree 树的分裂操作很接近。动态分区的一个优点就是分区数量可以自动适配数据的数量,如果数据少可以使用少量的分区节点,降低系统开销,如果数据多就添加节点分区。 MogngoDB 的做法是预先分区,在初始化的时候预先分区; Cassandra 则在节点维护一个最大值和最小值的策略,来维护分区的个数,让分区的数量和数据记录数量成正比,采用的分区策略也是上文提到过的一致性哈希算法,如下图:
不管什么样的分区策略都为了面对数据量增长和减少能让分区更好应对,动态分区在调节的时候数据记录会发生一些迁移活动,会产生短暂性能开销,所以好的分区策略会降低这些问题,可以设计一个自动检测算法和自动故障调节算法来自动完成这些操作是最理想,但是因为网络因素可能还会出现一些问题,这时就需要引入管理员手动配置才是最可取的。
数据查询
因为数据记录被分区存储在不同的节点之上,又会带来一个问题是如何知道需要查询的数据在集群中第几个分区里?当客户端连接时该连接到那个节点获取数据?而且分区可能在运行过程中出现了分区调整,会动态扩缩容分区节点,这时该如何面对?
肯定大家的了解过微服务中的服务发现通过注册中心来解决,查询到相应的 IP 和端口然后继续读取数据完成查询操作,这种方式相比上一篇讲解的 Amazon Dynamo 采用多节点读取的方式,来说直接读取数据所存储分区节点,而不是批量读取多个节点。目前主流做法就通过中间层、或数据源信息共享机制、客户端路由表形式来做,各种方式的对比图如下:
由于分区节点通过网络连接那么就要依赖着网络,这时分区的元数据信息就要依靠着分布式的协议来做数据信息交换,例如使用ZooKeeper来协调分区元数据信息,当有节点动态删除或者添加时就要通知路由层处理新状态;这里还可以使用其他一些协议gossip
这种最终一致性协议,这是 Redis 集群模式默认使用的数据分区协调协议,下图为基于 Zookeeper 的方式架构图:
小 结
说话天下大势,分久必合,合久必分!这句话在分布式系统上体现的淋漓尽致,将大规模的数据集划分成小集合来管理,将数据均匀得分布在不同分区节点中来降低热点数据问题,每个分区能独立运行并且还能按照地里位置进行分区,且能提高系统整体容错性和可用性。但就因为分区节点又会带来一个问题就是如何保证节点之间通过网络连接状态的一致性,这方面可以阅读后面更新的分布式事务的文章。