系统设计心得之数据分片
Hash分片 vs. Range分片
所谓Hash分片指的是根据分区键的Hash值确定数据属于哪个分片,典型的系统是Cassandra和Redis Cluster。而Range分片则根据分区键的字典序确定属于哪个分片,典型的系统是HBase、TiKV。
一个系统选择Hash还是Range分片其实不是一个实现层面的决策,而是产品层面的。如果系统需要支持Range Query,即有序Scan,如对象存储中的List Objects操作,或者SQL的语义,那么一定是Range分片的,否则有序Scan操作都比较低效。
Hash分片的优势是显而易见的:各个分片之间“通常”是均衡的。之所以说是“通常”,因为诸如Cassandra、Redis Cluster都支持Clustering Key的能力(对于Redis就是Hash/Set/ZSet这类数据结构)。两个相同的分区键可能对应着完全不同数量的记录数。
正是因为Hash分片之间通常是均衡的,Hash分片数可以是固定的,如集群创建伊始就确定为4096个分片,而不支持动态的分裂或合并。而Range分片则不行,因为分片之间很难做到均衡,经常会出现热点分片。在Range分片的系统中,分片的分裂和合并是必须要实现的能力。
固定分片数
固定分片数虽然实现简单,但也有着自己的问题:
- 集群中节点数少时,每个节点管理的分片数较多。在做节点升级等运维操作时,需要切走的分片数也更多,导致升级时间长。总体看这个在实际中问题不大。
- 集群中节点数多时,每个节点管理的分片数较少。意味着每个分片容纳的数据量更多,一个分片的全量复制耗时更长;同时每个分片容纳的请求量也更大,节点间即使相差一个分片,数据量、请求量差别可能会非常大。
- 如果分片数较少,如1024个分片,那么集群规模也不能太大。
比较好的做法是初始设定分片数更多一些,如8192个分片甚至更大,如Redis Cluster是16384个。这带来的问题是,元数据的数据量更大。更大的元数据可能会对ZooKeeper/ETCD/Gossip协议等造成更大的压力。
分片和存储引擎实例
如果同一个节点可以管理多个分片,就需要在下述两个方案中进行选择:
- 单引擎:所有分片的副本由一个存储引擎管理,即One DB-Engine for all Replicas。如TiKV,多个Region共享一个RocksDB实例。
- 多引擎:每个分片的副本对应着一个存储引擎实例,即One DB-Engine per Replica。如YugaByteDB,每个Tablet对应一个RocksDB实例。
多引擎有非常多的优点,我们在讨论单引擎的缺点时会提及。多引擎唯一要解决的问题是,在一个节点拥有多个分片副本时,如何有效的管理、分配CPU、内存、磁盘资源。粗略的想,如果支持分片的分裂和合并的话,那么单个节点上的分片数就可以得到有效的控制,此时多引擎的弊端就相对可控。那么我们也可以说多引擎的缺点是需要实现分片的分裂和合并。
单引擎的缺点有:
- 全量复制需要Scan。而多引擎模式下,只要把SST直接拷贝过去就好了。
- 副本删除相对低效(采用RocksDB DeleteRange的功能则还好)
- 意味着宕机recovery log是公用的,但是复制用的log又是分片级别的。解决这个问题的做法是类似于pegasus的shared log(宕机recovery用)加private log(用于复制的分片级别的log)的方案。实际上,TiKV是用RocksDB来存放log,本质上也是这个方案(WAL是recovery log,SST组成了复制用log)。