Spark 小文件合并优化实践

随着 Delta Lake 的开源以及 spark3 preview发布,很多 spark/大数据 的痛点都看到了一个新的解决方向,大数据刀耕火种的时代可能就要翻篇了。这篇文章要是再不写,怕是以后也没机会放出来了_(:з」∠)_

背景

Spark 生成小文件是个老问题了,不再长篇大论,这里简单提一下形成原因及影响。

  • 原因:开发人员无法判断作业写出的数据量,shuffle write 阶段分区数设置过多,导致写出的文件数量多。
  • 影响:数据读取/写入的速度会降低,并且会对 hdfs namenode 内存造成非常大的压力。

一般遇到这种情况,也只能让业务或者开发人员主动的合并下数据或者控制下分区数量。早期还试过 spark 的 adaptive-execution(AE) ,不过效果也并不好,且在不少场景下还发现了一些BUG,比如单分区 hang 住之类的。

因此我们进行了一些尝试,希望能自动化的解决/缓解此类问题。

一些工作

大致做了这么一些工作:

  1. 修改 Spark FileFormatWriter 源码,数据落盘时,记录相关的metrics信息(分区/表的记录数量+文件数量)

  2. 在发生落盘操作后,会自动触发 merge 检测,判断是否需要追加合并数据任务

  3. 实现一个 MergeTable 语法用于合并表/分区碎片文件,用于系统或者用户直接调用

第1和第2点主要是平台化的一些工作,一旦发生数据落盘,会对metrics和作业信息经过一些校验,判断是否需要进入MergeTable逻辑,下面主要说一下 MergeTable 的一些细节。

MergeTable

语法:

  1. 允许指定表或者分区进行合并
  2. 如果直接合并分区表但不指定分区,则会递归所有分区进行合并
  3. 如果指定了生成的文件数量,就会跳过规则校验,直接按该数量进行合并
merge table [表名] [options (fileCount=合并后文件数量)]  --非分区表
merge table [表名] PARTITION (分区信息) [options (fileCount=合并后文件数量)] --分区表

整体逻辑还是比较简单的:
Spark 小文件合并优化实践_第1张图片

一些优化点:

  1. 只合并碎片文件

    ​ 例如设定的阈值是128M,只会读取小于该大小的文件进行合并,如果碎片文件数量小于一定阈值,就不会触发合并(合并任务存在一定性能开销,允许系统中存在一定量的小文件)

  2. 分区数量及合并方式

    ​ 为了提高性能,定义了一些规则用于计算输出文件数量及合并方式的选择。

    ​ 根据是否启用 dynamicAllocation 来选择 spark.executor.instances * spark.executor.coresspark.dynamicAllocation.maxExecutors * spark.executor.cores 来获取当前 Spark 作业的最高并发度,该并发度将用于计算数据的分块大小。根据数据碎片文件的总大小选择合并(coalesce/repartition)方式。

    例子1:并发度100,碎片文件数据100,碎片文件总大小100M,如果这个时候用了 coalesce(1),很显然只会有1个线程去读/写数据,如果改为 repartition(1),则会有100个并发读,一个线程顺序写。

    例子2:并发度100,碎片文件数量10000,碎片文件总大小100G,如果这个时候用了 repartition(200),会有100G的数据发生 shuffle,如果使用 coalesce(200),则能在相同并发的情况下避免200G数据的IO。

    例子3:并发度200,碎片文件数量10000,碎片文件总大小50G,要是使用 coalesce(100),会保存出100个500M文件,但是浪费了一半的计算性能。如果使用 coalesce(200),正常情况下合并耗时会下降为原来的50%。

    ​ 通过上述例子,清楚 spark 运行原理的同学应该很快就能明白,这些操作的核心就是为了尽可能多的使用计算资源以及避免不必要的IO。

  3. 修复元数据
    ​ 因为 merge 操作会修改目录的创建时间和访问时间,所以在目录替换时会额外操作将元数据信息修改到 merge 前的一个状态,该操作还能避免冷数据扫描的误判。

  4. commit 前进行校验
    ​ 在最后一步会对数据做校验,判断合并前后数据量(从数据块元数据中直接获取数量,避免发生IO)是否发生变化,如果异常则会进行回滚。

后记

做了相关优化后,MergeTable 的速度对比原生暴力 Merge 的方式,在不同的数据场景下,性能会有数倍至数十倍的提升。

该方式已经在线上运行了1年多,成功的将平均文件大小从150M提升到了270M左右,同时 namenode 的内存压力也得到了极大缓解。

很多时候,并不是一定用到许多花里胡哨的技术才能达到目的,只要能解决问题都是好办法。

一些不足
数据合并过程中数据不可用,其实可以通过 MVCC 就能很简单的实现,但是会显著提高存储成本及运维成本。

不过合并操作通过加入我们自定义的工作流后,并不会影响到下游任务,已经满足业务需求了。

你可能感兴趣的:(Spark,生产环境中的spark)