3. 小文件问题剖析

在金融湖仓的日常运维中,小文件问题几乎是最常见、也最容易被低估的“慢性病”。它不像数据倾斜那样会立刻让某个任务卡死,也不像数据丢失那样触发告警,但它会持续侵蚀查询性能、推高存储成本、加重元数据负担,最终在月末报表、监管报送、实时风控等关键场景集中爆发。理解小文件的定义、产生原因及其影响链路,是后续设计 Compaction 策略的前提。

一、什么是小文件:从“大小”到“效率”的定义

小文件并没有一个绝对统一的字节阈值,它本质上是一个相对效率概念。在 HDFS、对象存储或 Iceberg、Hudi、Delta Lake 等湖仓表格式中,如果一个文件的大小远小于底层存储或计算引擎的“理想块大小”,导致读取该文件时用于打开、寻址、解压、初始化读取器的开销占比过高,就可以被视为小文件。

工程上常用的经验阈值如下:

存储/表格式 典型块或目标文件大小 常被判定为小文件的区间
HDFS 128 MB / 256 MB < 32 MB
Iceberg / Hudi / Delta 128 MB ~ 512 MB < 64 MB
金融实时入湖场景 按分钟/秒级提交 KB ~ 几 MB

需要强调的是,小文件的核心危害不在于“小”本身,而在于文件数量与数据量之间的比例失衡。一个 10 GB 的表如果由 10 个 1 GB 文件组成,是健康的;如果由 10 万个 100 KB 文件组成,即使总大小相同,也会带来数量级差异的元数据与调度开销。

二、小文件从哪里来:金融湖仓的典型产生路径

小文件的产生通常不是单一原因,而是写入模式、调度频率与表格式机制共同作用的结果。在金融场景中,以下路径最为常见:

  • 高频微批写入:实时风控、交易流水、行情快照等场景常以秒级或分钟级触发 Flink/Spark Streaming 写入,每次提交都生成新文件。
  • 分区粒度过细:按“日期 + 机构 + 产品 + 渠道”等多级分区,单分区数据量很小,却各自产生文件。
  • 频繁 MERGE / UPSERT:Iceberg、Hudi 的 MOR/COW 模式在更新时会产生新的 delta 文件或重写文件,更新越频繁,碎片越多。
  • 小批量补数/回跑:金融业务经常按机构、按产品补历史数据,每次补数范围小,形成大量零散文件。
  • 失败任务残留:Spark/Flink 任务中途失败,已写出的临时文件未被清理,长期堆积。
在金融湖仓中,小文件往往不是“写坏了”,而是“写得太勤、太细、太碎”。治理的第一步不是立刻合并,而是先定位主要来源,否则 Compaction 只是反复打扫同一片落叶。

三、三重影响:查询性能、存储成本与元数据压力

小文件的影响不是线性的,而是通过“文件数量”这个放大器,同时作用于计算、存储和元数据三个层面。

1. 对查询性能的影响

查询引擎在扫描表时,需要为每个文件创建读取任务、打开文件、读取 footer、初始化解码器。文件越小,单位数据量对应的任务数越多,调度和 I/O 初始化开销占比越高。在 Spark 中,这直接表现为 Task 数量暴涨、调度时间超过实际计算时间;在 Trino/Presto 中,则表现为 Split 过多、Worker 排队严重。

-- 典型现象:一个 50 GB 的表,因小文件导致 20 万个 Split
-- Spark UI 中可见:
-- Stage 0: 200000 tasks, duration 18 min
-- 其中实际读取数据时间占比不足 30%,其余为任务调度与文件打开

-- 查看表文件数量与平均大小(以 Iceberg 为例)
SELECT
  count(*) AS file_count,
  sum(file_size_in_bytes) / 1024 / 1024 AS total_mb,
  avg(file_size_in_bytes) / 1024 / 1024 AS avg_file_mb
FROM demo.finance_db.orders.files;

2. 对存储成本的影响

每个文件在 HDFS 或对象存储中都有固定的元数据、副本或请求开销。小文件越多,NameNode 内存占用、对象存储的 PUT/GET 请求次数、以及冷热分层时的管理成本都会显著上升。对于按请求计费的对象存储,大量小文件的读取还会直接放大 API 调用费用。

3. 对元数据与 Catalog 的压力

在 Iceberg/Hudi/Delta 中,每个数据文件都会在 manifest、metadata 或 transaction log 中留下记录。小文件数量膨胀会直接导致:

  • manifest 文件变大,查询规划阶段需要读取更多元数据;
  • Hive Metastore 或 REST Catalog 的 partition 与 file 统计变慢;
  • 快照过期、清理和 Compaction 本身变得更耗时。
很多团队只关注“查询慢”,却忽略了元数据膨胀。当 manifest 或 transaction log 大到影响查询规划时,即使数据文件本身不大,整个表也会变得难以维护。

四、案例复盘:一个金融流水表的“碎片化”过程

某金融团队将交易流水表按“日期 + 机构”分区,使用 Flink 每 5 分钟写入一次。上线初期数据量小,查询正常。三个月后,单日分区内文件数达到 8 万,平均文件大小不足 2 MB。月末对账查询从 3 分钟恶化到 40 分钟,同时 HDFS NameNode 内存告警。

复盘发现,问题并非写入逻辑错误,而是写入频率与分区粒度不匹配:每 5 分钟提交一次,每次只写入少量增量,却各自生成文件;分区又按机构拆分,进一步放大了文件数量。后续通过调整提交间隔、引入按分区的小文件合并策略,才逐步恢复。

金融湖仓小文件:产生路径与三重影响 高频微批写入 分区粒度过细 频繁 MERGE/UPSERT 补数/失败残留 文件数量膨胀 平均文件大小下降 查询性能下降 Task/Split 暴涨,调度开销高 存储成本上升 副本、请求、管理开销放大 元数据压力 manifest/log 膨胀,规划变慢 治理方向:定位来源 → 控制写入粒度 → 定期 Compaction → 监控文件数与平均大小

五、常见误区与治理前的判断清单

在动手做 Compaction 之前,先避免几个常见误区:

  1. 只看总大小,不看文件数:总大小正常不代表健康,文件数才是关键指标。
  2. 盲目合并所有分区:冷分区合并收益低,应优先处理高频查询与高频写入分区。
  3. 忽略写入侧治理:只靠事后合并,等于一边漏水一边拖地。
  4. 不监控平均文件大小:没有基线,就无法判断 Compaction 是否有效。
建议在治理前先建立三个基线指标:文件总数、平均文件大小、单分区最大文件数。后续所有 Compaction 策略都应围绕这三个指标设定目标与验收标准。