需求分析:
许多用户想要在 HBase 中存储海量文档、图像等大约100kb~10MB的对象(MOB),并希望以低延迟读取和写入这些对象。例如银行想要存储客户签名文档的扫描副本,或者运输机构存储交通和移动汽车快照。这样的对象如果直接存入HBase或者直接存入HDFS都有瓶颈,需要重新设计一个独立功能来满足用户既定的需求。这些对象有如下特点:
1、 对 MOB 的操作通常是写入密集型的,很少更新或删除,读相对较少。
2、 MOB 通常与其元数据一起存储。元数据如车号、速度、颜色等,它们相对于MOB文件体来说非常小。因此通常只访问对象元数据以进行文件分析。
3、 用户通常只用MOB的rowkey进行精确查询。
因此,社区在考虑该功能的设计时,主要强调以下特性:
1、 更好、更可预测的性能。
2、 与HBase提供的数据一致性强。
3、 对 API 和操作的最小更改。
4、 尽可能复用使用现有的 HBase 功能,例如快照、批量加载、复制、安全机制等。
现有方案:
我们先来分析MOB功能出现之前,我们如何来处理小文件的存储和访问问题。
思路1:
如图,我们可以将MOB data和Metadata一同对待,存储在HBase的同一行row中,用不同的CF区分,这是最直接的思路,也是没有MOB功能前,用户可能采取的策略。该方案的问题是,MOB data的大小往往相对较大,如果将其当成默认的cell,可能写几十个文件就会写满memstroe,按默认的Hfile管理策略,就会经常触发compaction和split行为。导致写放大,占用大量io,反过来降低了集群的吞吐量。
思路2:
如图,我们将MOB data对应的Hfile区隔开,不做compaction。这样可以缓解写放大问题。但是,由于我们配置的memstore和普通的Hfile没有区分(如256m),可能一个Hfile就放几十个mob对象。这样,在读的时候,就会导致打开文件数太多,并占用大量内存。产生读瓶颈。
思路3:
这种方案比较极端,既然HBase现有的功能无法满足读写同时处于一个可以接收的状态。不如只用hbase存储元数据,并记录对应mob文件在hdfs上的路径。再由客户端自己将文件存储进hdfs对应路径下。这是一个客户端解决方案,事务由客户端控制。HBase上读写比较快。但是这种方法对LOB还行,但对MOB,由于HDFS元数据设计对海量文件也不友好,因此会制约MOB的上限。
思路4:
作为思路3的改进,我们可以将小的 MOB 文件组装成一个更大的文件。因此我们不仅需要存储hdfs文件路径,还必须将这些小文件偏移量和长度存储在 HBase 表中。但这很难保持 MOB 的一致性,文件修改可能会导致Seq file空洞;也很难管理已删除的 MOB 和可能的小型 MOB 文件。要求业务不能经常改动已经入库的文件。
功能设计:
MOB功能的整体设计如图,类似于方案4,我们可以将元数据和MOB数据分开存储。区别之处在于在把对象文件写入hdfs前,先使用rs上的memstore来缓存MOB数据。这样MOB在HDFS上会有多个对应的落盘文件,而不是方案4的一对一。MOB文件被存储在一个HBase管控之外的特殊区域(单独文件夹)。所有的MOB文件读写都复用当前的HBase API。
查询过程:
MOB查询遵循以下步骤,1、在HBase表中查询MOB文件路径对应的单元格。2、通过文件路径查找MOB文件。3、通过rowkey查找MOB文件的单元格。MOB文件不需要split和compact,因为我们通过普通hfile 索引查找,不会在mob Hfiles里遍历,因此很多mob files不会影响读。
写入过程:
当MOB单元格被写入的时候,也是被写入WAL和memstore,在flush的时候被刷写到MOB专用的的HFiles,而metadata和mob path值也会被刷写到对应的常规HFile里。此设计内置了数据一致性和 HBase 复制功能。所有属于同一张表同一个列族的MOB文件会被放到同一个hdfs文件夹里。
MOB文件的文件名分成三个部分:region的MD5编码的start key,MOB file的最后日期和文件的uuid。这样,要是要删除过期的文件,可以用文件名的第二部分对比meta里定义的TTL,超时的文件就可以删除。mob的path单元格的value的格式为:
在读MOB文件的时候,首先会打开Scanner同时从memstore和StoreFile里扫描。通过(配置)可以启用MOB的blockcache,这样对于热点文件可以加快访问。
MOB有两种读缓存的方法,包括普通的通过blockcache方式进行缓存。或者通过池化缓存MOB读线程reader的方法,来防止缓存的加载太频繁,对内存回收造成不良影响。
压缩和清理机制:
a、 自动清理TTL和版本过期的MOB文件。
b、 把mob文件做compaction,把小文件们合并成更大一些的文件,同时清理掉过期的文件。这样可以减少存储空间的放大和hdfs上的小文件问题。
一开始的做法是在master上起一个线程定期扫描。不过这种做法会导致所有的IO都从master server走,有很大的性能问题。后面社区重构了这里的设计,通过整个集群并行的方式,增强了文件清理过程的可扩展性。
后面又改为通过起MR任务,但是这样MOB文件的压缩和普通HFile的压缩是两条路径,在特定条件下会导致数据丢失。目前社区重构了这块,MOB文件的合并需要用户用major_compact命令手动触发,通过表结构配置来实现二者的区分。
HBase2.5以后的实现里,MOBFiles是由HMaster和各个RS里的专门的定时线程维护的,主要是用来处理TTL超时清理和压缩。这种压缩并不经常发生,主要是用来控制HDFS中的文件总数,我们总是假设MOB数据很少更新或者删除。一个被删除的MOBFile 会被移动到archive文件夹,因为mob file有dummy region id,所以它和普通的hfile都会被一起放到归档文件夹里。
这些文件清理的定时线程同时也负责最终从正常区域删除不需要的Hfile和MOBFile归档文件。还有,如果有MOB表启用了快照,那清理机制会确保这些MOBFiles保留在归档区域,只要他们会被snapshot引用。
每当启用MOB的列族的memstore执行刷新时,HBase都会将超过MOB阈值的memstore flush到特定的MOB hfile。当普通的region 发生compaction时,RS会重写普通的HFiles,但不会去重写MOBFiles。而这个过程对于hbase client来说是透明的。
此外,比如删除或者更新MOB 单元格的时候,如果不重写合并,就会出现很多MOBFile里的文件不被引用,还放在那里就会浪费很多空间。所以此时需要做一些重写合并MOBFiles的压缩操作。因此在HMaster定期会启动一个线程任务,让rs执行特殊的majorCompaction,用来重写新MOB Files。在重写完MOBFiles后,rs还会去扫描HFile,将对应的引用单元格的value对应更新。最终的结果就是每个region对应的MOBFiles会合并为一个。默认这个压缩进程每周执行,通过设置hbase.mob.compaction.chore进行配置。
默认情况下,周期性的MOB压缩线程将尝试让每个region都并行进行压缩,从而最大化集群效率。如果需要调整此压缩在底层文件系统上生成的IO量,可以通过设置hbase.mob.major.compaction.region.batch.size来控制并发度。如果将配置设置为0,就是完全并行执行。
MOB文件生命周期:
最终,我们将拥有不再需要的MOB文件。客户端覆盖该值,或者MOB重写压缩将存储对较新的较大MOB hfile的引用。由于任何给定的MOB单元最初都可能被写入当前区域或先前某个时间点存在的父区域,因此各个区域服务器不会决定何时归档MOB文件。相反,Master中的一个定期线程会评估MOB文件以进行归档。
HMaster归档的条件主要有两种:
1、 MOB Files 比列族定义的TTL老,超时了。
2、 MOB Files 没有被hfile中的任何列族引用了。
为了确定MOB HFile是否满足第二个标准,chore从给定表的每个启用MOB的列族的常规HFile中提取元数据。该元数据列举了满足正常HFile区域中存储的引用所需的完整MOB HFile集合。
可以通过设置hbase.master.mob.cleaner来配置清理程序的周期。周期设置为正整数秒。它默认为每天运行。除非短的TTL或非常高的MOB文件更新率以及相应高的非MOB压缩率,否则不需要对其进行调整。