Dark Side of Cloud Storage —— 数据对像的分块消重

数据对像(可以通俗地认为是文件)的分块存储具有久远的历史。长久以来,单机文件系统一直将文件切分为若干固定大小的小块。其主要目的是为了进行有效的空间管理。互联网时代,大规模数据存储逐步发展起来。出于降低成本的考虑,人们在分块存储的基础上进行数据块的复用,即所谓的“消重”。但对于大型的在线对象存储而言,分块消重是有害的。

具体来讲,分块消重是将数据对像切分成固定大小的数据块。数据对像之间有些数据块可能是一样的,那么就让它们共享数据块。换句话说,相同的数据块只留一份,所有用到这个数据块的对象,都指向这块数据。因为重复的数据块都被去除了,所以实际存储空间小于总的数据量。

数据对像被切块之后,需要对每个数据块做指纹运算,通常会采用数据块的哈希值。每个数据块计算指纹之后,在存储系统中检索数据块是否存在。如果不存在,就把数据块保存下来;如果存在,就不再保存了。一旦所有数据块都保存完成,就需要为数据对像构造一个索引数据块,依次引用每个数据块,而数据对像最终指向的是这个索引块。

数据块的大小决定了消重效率。数据块越小,重复的可能性越大,消重效率越高。但是随着数据块尺寸的减小,一个数据对像引用的数据块数量越来越多,因而索引数据块的尺寸越来越大。对于尺寸很大的数据对像,比如数GB,乃至数百GB,索引需要多个数据块构成,有时需要多级(树形)的索引块。相反,过大的数据块尺寸会降低消重效率。

在线对象存储系统由于庞大的数据存储规模,过小的数据块会引发数据块管理的困难,通常都会选择较大的数据块尺寸,一般在几MB至几十MB之间。但是,根据统计数据,来自互联网的数据对像,大部分是在1MB以下的。这意味着,MB级别的数据块大小大多数情况下都是一个数据对像占据一个数据块,块级消重退化为对象级消重,效率大幅降低。

分块消重的第一大问题是没法删除数据。造成这一问题的根源在于分布式环境和巨大的数据量。在分块消重里,一个数据块可能被多个数据对像引用。一个数据对像被删除,它所引用的数据块却不能随便删。因为可能还有其他数据对像正在引用这些数据块。只有在确认一个数据块没有被引用,才能将其删除。而在一个在线存储系统中,确认一个数据块是否被引用,是无法做到的。

我们可以通过检索所有的数据对像,生成一个数据块到数据对像的反向索引。然后找到那些没有被引用的数据块,进行删除作业。这就像内存管理中的垃圾回收功能,定期找出那些不再被引用的内存进行回收。但是,这样做有一个条件,就是整个系统在删除数据块期间不能有数据对象的写入。就如同垃圾回收需要短暂地停止程序的运行。所不同的是,存储系统的垃圾回收的停摆时间是以小时计算的。

内存的垃圾回收需要对内存数据做标记和拷贝,内存非常快,可以很迅速地完成清理。但磁盘的速度同内存相差几个数量级,并且在线对象存储的数据量规模庞大,数以P计。完成这样一次引用管理的梳理所需的时间以小时,甚至以天计算。

垃圾收集的模式不能满足要求,另一种办法就是引用计数。每个数据块维护一个引用计数值,用以记录有多少数据对像引用了这个数据块。这种技术在内存管理和资源管理中也有着广泛的应用。但是,在线对象存储系统中,数据存放在一个庞大的分布式集群里。跨越网络的引用计数增减远远没有内存中的操作来的可靠。从一台服务器向另一台服务器发起对引用计数的加减经常会有失败的情况。更具破坏性的一种情况是,增减引用计数的请求被正确执行,但执行结果的反馈没有成功传递。于是发起方认为引用计数操作失败,但执行方却认为成功,两者不一致随即会导致引用计数的偏差。

更混乱的情况是,在线存储为保证可靠性,往往使用多副本,即一份数据块有多个拷贝,分布在集群中不同的服务器上。多个副本的引用计数的一致性保障同样非常困难。无论是采用事务,还是使用WRN等一致性算法,都只能保证最终一致性,无法严格保证强一致性。这种非一致性时刻在发生,从而导致任何时候我们试图确定一个数据块的引用计数的时候,都无法确保这个数字是准确的。

作为应对,我们可以采用mark-delete的方式,在引用计数归零的时候,将一个块标记成delete。当有用户对被标记的数据块进行读取的时候,将其delete标记清除掉,恢复成正常数据块。在一段时间后,把那些始终未被恢复的数据块删除。但是,多长时间以后呢?对于“热”(即经常被访问)的数据块,错误删除后,很快就可以得到恢复。而对于“冷”(即长时间不被访问)的数据块,可能数周乃至数月不被访问。这样的数据块一旦被删除,便会造成数据丢失。

所以,分块消重将会导致无法(确切地说“不敢”)删除任何一块数据。在经年累月的运行之后,将会存在大量的垃圾数据块,导致存储系统的空间利用率降低,很大程度上抵消了消重带来的收益。

无法清除垃圾数据是运营成本和效率的问题。但是,接下来的问题涉及到在线存储的根本,它们有时是致命的。

我们知道,人造的东西都会坏。在计算机集群中,每一种硬件都会失效。当磁盘失效后,所携带的数据就会丢失。数据一旦丢失,就要尽快恢复。修复的手段就是从其他副本那里将数据复制一份过来。在实际的在线存储系统中,不会对一块盘直接恢复数据。那样太慢,一块有1TB数据的磁盘恢复一下至少需要20000秒,将近6小时。在大型的在线存储系统中,这段时间完全可能有其他磁盘损坏,如果凑巧失效的磁盘有同一数据对像的不同副本,那么该数据对像就可能丢失。所以,我们会将丢失的副本恢复到副本所在的子集群中,让其他服务器共同承担数据恢复的负载。

但是,这样的方式会导致被恢复的副本存储位置发生变化。副本存储位置一旦发生变化,需要尽快更新元数据。但是,当采用分块消重方式的时候,副本位置变化影响的不只是元数据,索引块的内容也需要做相应的变化。检索并修改所有涉及的索引数据块是严重的自虐行为。

对于一个数据块,如何知道它被哪些索引块引用?最基本的方法是扫描每一个索引块。鉴于存储系统的海量数据,完成一次扫描的时间虽然比不上数据块的引用检索那么漫长,但也是数以小时计。这种扫描会消耗大量的存储服务器的资源。而且随着每次数据副本修复,无论是单个数据对像,还是整盘的修复,都需要进行这样的扫描。频繁的检索对系统的稳定性和处理能力产生很大的干扰。

合理的解决方式是在数据块和索引块的层面引入一层间接。每个数据块和索引块赋予唯一的标识,元数据和索引都只引用标识。通常,数据块的标识会使用数据块的的摘要,比如MD5或者SHA1等等。数据块的标识同数据块的存储位置之间的映射通过一组元数据保存。当数据块副本的存储位置发生变化时,只需修改这组元数据,而所有对数据块的引用都无需变化。

但引入数据块层面的元数据使系统增加了复杂性。其问题不仅仅是增加一套数据库那样简单。同任何元数据一样,数据块的元数据需要保障非常高的可靠性,可用性和一致性。此外还有数据块的元数据和数据存储之间,以及数据块元数据同数据对像(索引块)之间的数据同步问题。一套额外的元数据大幅增加系统的复杂性和运维难度。

如果将数据块和数据对像的元数据存放在一套元数据系统里,问题会得到一定的缓和。维护一套元数据系统会比两套系统简单很多。但两级元数据中存在的数据同步问题依然不能解决,依然需要额外的数据核对和修复操作。

元数据是整个存储系统的核心和关键路径,是最复杂的部分,也是容易出现性能瓶颈,数据差错的地方。元数据的量越大,维持可靠性、可用性和一致性越困难。数据块和数据对像的元数据集中在一起,元数据量将会是数据对像数量的两倍以上。随着数据块的尺寸的变小,消重效率会提高,但元数据量会越来越大,可能达到数据对像数量的数倍。对于数据对像数量达到几十,乃至上百亿的时候,这种放大效应产生的影响非常可观。

于是,分块消重陷入一个两难的境地:收缩数据块尺寸,可以提高消重效率,但可能压垮元数据;如果放大数据快尺寸,会减小元数据系统的压力,但消重效果越来越差。再加上无法确定数据块是否被引用,从而无法删除未被使用的数据块,最终导致垃圾数据的积累,而抵消消重带来的好处。所以,在大型的在线对象存储系统中,分块消重是一个破坏者,造成的麻烦远比带来的好处更多。

你可能感兴趣的:(Dark Side of Cloud Storage —— 数据对像的分块消重)