Jump Placement Map
是使用跳跃一致性哈希算法,以便在不同的故障域之间伪随机地分布对象。这样做是为了尽可能将他们分散到相互距离较远地故障域中,从而避免在当某个故障影响了整个故障域的情况下造成数据丢失。
跳跃一致性哈希算法是一种一致性哈希算法,它能将keys均匀的分布在一定数量的buckets中。即使buckets的数量增加时,也无需额外存储空间。跳跃一致性哈希算法以key和buckets的数量为输入参数并最终返回某个bucket的编号。
跳跃一致性哈希旨在满足这样一个约束条件:当扩展buckets的数量时,平均只有1/n的keys需要重新定位
。它通过对给定的key进行连续计算跳跃的目标来实现这一点。当计算出的数字大于当前buckets的数量时,它将使用范围内的最后一个数字作为该key的当前bucket。
因为跳跃一致性哈希算法会为给定key返回一个特定的bucket,所以可能会出现同一冗余组内的分片被放置在同一容灾域甚至同一target上。为了解决这个问题,Jump Map会跟踪已经使用的targets。并且,当发生hash冲突时,会对key重新进行哈希计算,并再次尝试放置,直到找到一个未使用的域或者target。
当计算一个对象的布局时,首先需要根据对象的类以及对象的元数据来确定副本要求。对象的类包含了有关副本的数量、纠删码、以及其他属性信息。解析这些要求会得出冗余组数量和冗余组大小,两者相乘就是对象布局的总大小。为了选择要写入对象的target,算法首先从pool map的根开始。然后使用跳跃性一致哈希算法,其输入如下:一个6位的对象ID,以及当前故障域中子组件的数量。然后将选定的组件标记位已使用,并将其指定为当前故障域。它会为每个故障域持续执行该操作,直到到达仅包含targets的故障域为止。(貌似是可以理解为递归嵌套处理)。当选定target后,会返回target的ID作为此分片存储的位置。在处理后续的分片的放置上会稍微有点不同。在选择组件期间,如果选择的组件已经被标记为已使用,则对key进行递增(结合了标准地CRC值)。然后再次调用跳跃性一致哈希,并计算出新的哈希值,直到找到一个未使用的组件。如果一个容灾域中所有的节点都被标记为已使用,则将他们全部标记为未使用。
CRC(循环冗余校验)之所以被用于该算法,是因为它在现代CPUs上运算速度极快,并且对于仅仅只有几个位区别的输入的keys来说,也能产生截然不同的且均匀分布的结果。如果没有类似CRC的算法来改变keys的次序,那么对于非常相似的keys,跳跃一致性哈希所产生的结果分布,就无法在实际应用中达到可接受的均匀程度。
uint32_t
d_hash_jump(uint64_t key, uint32_t num_buckets)
{
int64_t z = -1;
int64_t y = 0;
while (y < num_buckets) {
z = y;
key = key * 2862933555777941757ULL + 1;
y = (z + 1) * ((double)(1LL << 31) /
((double)((key >> 33) + 1)));
}
return z;
}
当某个容灾域出现故障时,Jump Map使用相同的机制来选择出需要重构的target。每个故障分片都会使用相同的跳跃一致性哈希算法重新映射到rebuild target上。
虽然故障target上的对象冗余数据可能不会丢失,但是因为故障原因而无法访问该分片时,该分片的对象必须以降级模式运行。检测到故障后,尽快重构这些对象数据是非常重要的,原因如下:
一旦某个target或者容灾域发生故障,作为重构过程的一部分,Jump Map会计算需要重构的对象的layout。计算layout的第一步是先计算出原始的layout。它将会从pool map最顶端开始,然后依次选择子域。不过,这次它将避免选择故障序列低于正在重新映射的target的target。这对于处理之前已经重构过一次的分片的故障是非常重要的。由于Jump Map在选择targets时会跟踪冲突,因此对于不同的对象分片,永远不会复用到相同的target。这也消除了专门预留targets作为备用的必要性。
故障target的负载平衡
Jump Map会单独重新映射对象的每一个分片。这意味着对象的每个分片都有同等机会被映射到任何一个target上。这就使得故障tearget上的负载,在很高概率下会被平均分配到所有可用的targets上。
Jump map非常适合用于存储池扩展,因为它结合了跳跃一致性哈希算法。当一个容灾域扩展时,作为再平衡操作的一部分,Jump Map会重新计算layouts。对于每一个对象,将使用之前的pool map来计算出原始的layout。然后再重新计算包含了新拓展的容灾域的layout。接着,将比较这2个layout,然后将发生位置变化的分片返回,以便再后续的再平衡操作中使用。
Drain:该操作也是将组件从pool中剔除,与exclude操作不同之处在于,在执行剔除操作前,需要将该组件中的数据迁移到其他组件中,pool不会处于降级状态,正在进行drain操作的组件依然可读。组件的最终状态被设置为DOWNOUT
。
步骤:
UPIN
状态时,将组件的状态设置为DRAIN
。DOWNOUT
。Reintegration:重新整合,目的是将组件重新加入到pool中,并将组件的最终状态设置为UPIN
。
步骤:
DOWNOUT
状态时,将组件的状态设置为UP
。UPIN
状态的组件中复制到UP
状态的组件中。UPIN
。Reintegration期间发生故障
无论故障时发生在UP
组件还是UPIN
组件上,在处理故障之前,reintegration会继续直到完成。
如果故障是发生在非reintegrating(UPIN)组件上,在reintegration操作完成之后,故障恢复将正常进行。如果故障发生在reintegrating(UP)组件上,则无需进行故障恢复,而是直接将它设置为DOWNOUT状态。因为该组件在第一次发生故障时,其上的数据已经在rebuild target上重构过了。
目前pool map的扩展是按照ranks列表进行扩展的,扩展的结果是将组件状态由NEW
变成UPIN
。
步骤:
NEW
状态的组件来实现扩展pool map。UPIN
状态的组件中复制到NEW
状态的组件中。UPIN
。无论何时使用placement来生成一个对象的layout时,在计算有多少个buckets可用时,它将会忽略那些处于NEW
状态的组件。比如,从3个ranks拓展到5个ranks时,新增的2个ranks状态将是NEW
。当使用placement时,它会使用一个包含5个ranks的pool map,但是跳跃一致性哈希将只会用之前的3个ranks来计算layout。因为,新增的2个ransks是NEW
,扩展过程并未真正完成,所以它们不会被考虑在内。
placement是通过fseq的值来识别哪些是NEW
组件,哪些是UPIN
组件。
Extend期间发生故障
无论故障是发生在NEW
组件还是UPIN
组件,在处理故障之前,扩展操作将继续执行直到完成。
如果故障发生在之前已经存在的组件上,则开始进行正常的故障恢复流程。
如果故障发生在NEW
组件上,这些组件会更新fseq的值,用来标记它们发生故障了,但不会将立即将组件的状态设置为DOWN
。目的是方便placement可以通过识别fseq的值来统计之前的组件。
当成功添加组件后,NEW
组件要么被设置为UPIN
,要么被设置为DOWN
(因为发生故障)。当设置为DOWN
时,就可以通过正常流程进行故障恢复了。请注意,这可能意味着一个非副本对象暂时不可用,因为它可能被迁移到一个发生故障的NEW
组件上了。在扩展期间,数据仍然可以访问(因为palcement是根据原始的layout大小计算的)。然而,一旦扩展完成(还未进行故障修复),即使UPIN
组件上的数据仍然存在,该数据也将无法访问(因为迁移之后的target位置发生变化了)。但是,由于原始数据并未删除,重构机制会在其他位置上重构这个不可访问的数据,一旦完成重构,该数据便可以访问。