随业务井喷,DB出现变化:
查询负载增加,需更多CPU处理负载
数据规模增加,需更多磁盘和内存来存储
节点可能故障,需要其他节点接管失效节点
所有这些更改都要求数据、请求可以从一个节点转移到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为 再平衡(rebalancing)。无论哪种分区策略,分区rebalancing通常至少要满足:
rebalancing后,负载、数据存储、读写请求应在集群内更均匀分布
rebalancing执行时,DB应能继续正常读写
避免不必要的负载迁移,以加速rebalancing效率,并尽量减少网络和磁盘 I/O 影响
4.1 再平衡策略
4.1.1 反面教材:hash mod N
图-3提过,最好将hash值分成不同区间范围,然后每个区间分配给一个分区。
那为何不使用mod(Java中的%运算符)。如hash(key) mod 10 返回介于 0 和 9 之间的数字。若有 10 个节点,编号为 0~9,这似乎是将每个K分配给一个节点的最简单方法。
但问题是,若节点数量 N 变化,大多数K将需从一个节点移动到另一个。假设 hash(key)=123456 。最初10个节点,则该K一开始在节点6(因为123456 mod 10 = 6):
当增长到 11 个节点,K需移动到节点 3(123456 m o d 11 = 3 123456\ mod\ 11 = 3123456 mod 11=3)
当增长到 12 个节点,K需要移动到节点 0(123456 m o d 12 = 0 123456\ mod\ 12 = 0123456 mod 12=0)
这频繁的迁移大大增加rebalancing的成本。
所以重点是减少迁移的数据。
4.1.2 固定数量的分区
还好有个很简单的解决方案:创建比节点更多的分区,并为每个节点分配多个分区。如10 个节点的集群,DB可能会从一开始就逻辑划分为 1,000 个分区,因此大约有 100 个分区分配给每个节点。
若一个新节点加入集群,新节点可以从当前每个节点中窃取一些分区,直到分区再次达到全局平衡。过程如图-6。若从集群中删除一个节点,则会发生相反情况。
选中的整个分区会在节点之间迁移,但分区的总数不变,K到分区的映射关系也不变。唯一变的是分区所在节点。这种变更并非即时,毕竟在网络上传输数据总需要时间,所以在传输过程中,旧分区仍可接收读写操作。
原则上,也可以将集群中的不同的硬件配置因素考虑进来:性能更强大的节点分配更多分区,从而能分担更多负载。在ES 、Couchbase中使用了这种动态平衡方法。
使用该策略时,分区数量通常在DB第一次建立时确定,之后不会改变。虽然原则上可拆分、合并分区,但固定数量的分区使得操作都更简单,因此许多固定分区策略的 DB 决定不支持分区拆分。因此,初始化时的分区数就是你能拥有的最大节点数量,所以你要充分考虑将来业务需求,设置足够大的分区数。但每个分区也有额外管理开销,选择过高数字也有副作用。
若数据集的总规模难预估(如可能开始很小,但随时间推移会变异常得大),此时,选择合适的分区数就很难。由于每个分区包含的数据量上限是固定的,因此每个分区的实际大小与集群中的数据总量成正比:
若分区里的数据量很大,则再平衡和从节点故障恢复的代价就很大
若分区太小,则会产生太多开销
分区大小应“恰到好处”,若分区数量固定了,但总数据量却变动很大,则难以达到最佳性能。
4.1.3 动态分区
对于使用K范围分区的DB,若边界设置有问题,可能导致所有数据都挤在一个分区而其他分区基本为空,则设定固定边界、固定数量的分区将很不便:而手动去重新配置分区边界又很繁琐。
对此,K范围分区的DB,如HBase采用动态创建分区:
当分区的数据增长超过配置的阈值(HBase默认10GB),就会拆分成两个分区,每个承担一半数据量
相反,若大量数据被删除,并且分区缩小到某阈值以下,则将其与相邻分区合并
这有些类似B树的分裂过程。
每个分区分配给一个节点,而每个节点可承载多个分区,和固定数量的分区一样。大分区拆分后,可将其中一半转移到另一个节点,以平衡负载。HBase中,分区文件的传输通过 HDFS实现。
动态分区的一个优点,分区数量可自动适配数据总量:
若只有少量数据,少量分区就够,开销也很小
若有大量数据,每个分区的大小则被限制在一个可配的最大值
但一个空DB,因为没有确定分区边界的先验信息,所以会从一个分区开始。数据集开始时可能很小,直到达到第一个分区的分裂点前,所有写操作都必须由单节点处理,而其他节点则处于空闲状态。为解决该问题,HBase、MongoDB允许在一个空DB配置一组初始分区(预分割,pre-splitting)。在K范围分区策略下,预分割需要提前知道K的分布情况。
动态分区不仅适于K的范围分区,也适用于hash分区。MongoDB 2.4 开始同时支持范围和hash分区,且都支持动态分割分区。
4.1.4 按节点比例分区
动态分区策略,分区数与数据集大小成正比,因为拆分、合并过程使每个分区的大小维持在固定的min和max之间
固定数量的分区方式,每个分区的大小与数据集大小成正比
两种情况下,分区数都和节点数无关。
Cassandra则采用第三种方案,使分区数与集群节点数成正比。即每个节点具有固定数量的分区。此时,每个分区的大小和数据集大小成正比,而节点数不变,但是当增加节点数时,分区将再次变小。由于较大数据量通常需大量节点来存储,因此这种方法也使每个分区的大小保持稳定。
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择可能产生不公平的分区分割,但平均分区数较大时(Cassandra默认每个节点有256个分区),新节点最终会从现有节点获得相当数量的负载。 Cassandra 3.0引入优化算法,可避免不公平的分割。
随机选择分区边界要求使用hash分区策略(可从hash函数产生的数字范围中设置边界)。这种方法也最符合一致性哈希的定义。
4.2 运维:手动 or 自动再平衡
动态是自动还是手动执行?
全自动的再平衡(即由系统自动决定,何时将分区从一个节点迁移到另一个节点,无须人工干预)和完全手动(即分区到节点的映射由管理员显式配置)之间有个权衡。如Couchbase会自动生成一个推荐的分区分配,但需管理员确认生效。
全自动再平衡更方便,正常维护之外操作工作很少,但可能不可预测。再平衡是个昂贵操作,因其需重新路由请求,并将大量数据从一个节点迁移到另一个节点。若出现异常,可能会使网络或节点的负载过重,并降低其他请求的性能。
自动平衡和自动故障检测相结合也可能存在风险。假设某节点过载,且对请求的响应暂时很慢,而其他节点得出结论:过载节点已失效,并自动平衡集群,转移其负载。客观上,这会加重该节点、其他节点和网络的负载,从而使情况更糟,甚至级联失效。
对此,再平衡过程中有人参与是更推荐做法。这比全自动响应慢一点,但可有效防止意外。