解决小文件问题

2022-07-21 13:42:34 浏览数 (1)

海量小文件的的根源

小文件的问题其实以前也一直困扰着我,对于传统数仓,导致小文件多的原因非常多:

  1. 分区粒度,如果你分区非常多,就会导致更多的文件数产生
  2. 很多流式程序是只增操作,每个周期都会产生N个文件,常年累月,积石成山。
  3. 以前为了解决更新问题,经常一份数据会有中间好几个存储状态,也会导致文件数很多。

为了解决小文件问题,我们也是八仙过海各显神通,一般而言可能都是写个MR/Spark程序读取特定目录的数据,然后将数据重新生成N个文件。但是在以前,这种模式会有比较致命的问题,因为在生成的新文件要替换原来的文件,而替换的过程不是原子过程,所以这个时候如果正好发生读,是会影响的。其次,很多读的程序,都会缓存文件路径,因为我们重新生成了文件,文件名称也变化了,导致读的程序的缓存失效,会发生比如文件找不到等异常。对于在一个进程比较好说,做下刷新就行,但是读往往是在不同的进程实例里,这个时候通知他们也是很难的事情。再极端一点,读取这个表的程序可能是另外一个团队维护的。所以其实小文件并没有想象的那么好解决,或者说能够优雅的解决。

为什么海量小文件是问题

前面,我们谈到了小文件的根源。那么文件多就多了,为什么是个问题呢?核心原因在于HDFS的设计问题,他需要把文件meta信息缓存在内存里,这个内存只能是单机的,所以变成了一个很大的瓶颈。虽然后面HDFS一直尝试解决这个问题,比如引入联邦制等,但是也变相的引入了复杂性。

Delta如何解决小文件

我们知道,其实大部分存储的问题都有小文件的多的问题,比如HBase等,他们的解决方案是做compaction,本质上就是讲小文件合并成大文件。HBase还有minor compaction和 major compaction之分。截止到目前(0.4.0版本),Delta还没有提供类似的compaction功能,但是基于Delta已经提供的扩展接口,我们也可以很轻易的实现compaction的功能。Compaction的核心点是,在做compaction的过程不能影响读写,而Delta的版本设计可以很简单的做到这一点。

我在Delta Plus里实现了一个compaction的版本。但是目前这个版本也有点限制,就是能够被compact的delta表不能包含update/delete操作。那为什么不能包含upsert操作呢?原因是compaction也是一个非常重的操作,持续的时间可能非常长,并且他是依赖于他开始那一瞬间读到的数据的。如果发生了upsert操作,意味着他读到的数据可能已经失效了,这个时候它会失败需要吃重新读,重新合并,重新写,而这个过程很长,可能它再次重试的时候,又有数据进行了upsert,那么可怜的它似乎永远都不能完成自己的工作了。而假设我们只允许新增数据,那么因为以前的文件不会发生变更,所以我们可以对以前的数据做合并然后产生新的文件,标记删除以前的文件,整个过程不会阻止数据的新增和读取。

似乎是不完美,但是在前面的章节中,我们说到,upsert在发生upsert的时候会动态调整控制文件的数目,所以他相当于自动具备了自己的compaction机制。而只有append操作的表,他的文件是一个一直增长的过程,所以需要我们手动进行compaction操作。

Delta compaction过程

Delta的compaction因为有了上面的约束,会变得异常简单。

  1. 读取某个版本之前的数据
  2. 将涉及到标记删除的文件真实物理删除
  3. 将标记为add的文件按分区(如果有分区)进行合并操作产生新的文件,然后标记删除这些文件。物理删除这些文件。
  4. 获取事务并且尝试提交。

compaction有个特殊的设计是,他并不会在开始工作前就尝试获取事务,而是直到所有的实际工作都做完了,才最后获取事物并且进行提交。这得益于前面我们说的对应表的数据只增特性。

0 人点赞