适用范围与问题定义
本文以 Hadoop 3.4.1 的 HDFS 为基础,讨论数仓、日志落地与批处理中的小文件治理;使用 Spark 重写数据时,示例语义限定 Spark 3.5.7。Hive 事务表、Iceberg、Hudi、Delta 等表格式必须使用各自支持的维护机制,不能直接替换底层文件。文中没有执行集群扫描或合并,状态保持 PENDING。
“小”没有适用于所有业务的固定字节线:重点是文件数量、单位扫描数据的打开成本、计算任务开销,以及 NameNode 元数据压力。应同时统计分区增长与查询模式。一个很少被读取的归档目录,与每天被大量查询的热分区,即使文件数相同,也可能需要不同方案。
为什么磁盘有空闲仍会慢
HDFS 在 NameNode 内存中维护文件、目录与块映射;大量小文件增加命名空间对象和元数据操作量。读取一批文件还会产生目录发现、文件打开与计算调度开销。因此,问题不只是“每个小文件浪费完整块大小的磁盘”,也不能以总字节数推算 NameNode 负载。机制背景见 HDFS 架构。
治理应分别观测存储层和引擎层。NameNode RPC、堆内存与垃圾回收反映元数据压力,SQL 的文件发现时间、扫描文件数、任务时长分布反映查询成本。若瓶颈是单个热点关联键,文件合并未必能改善,应避免把所有作业慢都归因于小文件。
先确定一个可比较的采样分区
选择一份已经停止写入、归属明确且规模已知的日分区。避免从根目录做递归列表,也不要把 head 接在全量递归输出后就宣称扫描已被限制。以下命令只覆盖经确认的小范围样本:
hdfs dfs -count -q 'hdfs://REPLACE_WITH_NAMESERVICE/REPLACE_WITH_CLOSED_PARTITION'
hdfs dfs -du -s 'hdfs://REPLACE_WITH_NAMESERVICE/REPLACE_WITH_CLOSED_PARTITION'
hdfs dfs -stat '%b %n' 'hdfs://REPLACE_WITH_NAMESERVICE/REPLACE_WITH_ONE_FILE'count -q 提供配额及目录、文件、内容大小信息;du -s 要区分逻辑大小与含冗余的存储消耗,不能混用。平均文件大小可由逻辑字节数除以文件数估算,但平均值不能替代文件大小分布,混入隐藏标记文件也可能改变解释。对较大分区优先使用已有清单与监控,扫描成本超出预算就停止。命令定义见 文件系统 Shell。
从生产链路定位持续增长来源
把文件创建时间与上游批次、分区数、写入并行度和失败重试关联。常见来源包括过细的时间分区、每个业务实体独立建目录、短触发间隔、每个任务都写一个很小结果,以及重试后遗留多个输出批次。先找到“谁持续制造文件”,再安排历史治理,否则合并任务会变成永远追赶的后台负载。
区分正常数据文件、临时输出和废弃分区,并让数据负责人确认它们的语义。临时文件可能仍被运行中的提交协议使用;名称看起来像临时目录不构成删除依据。若同分区还有迟到数据写入,应先设计不可变批次、维护锁或受支持的事务机制,而不是直接冻结未知上游。
在合并、归档和源头控制之间选择
- 调整写入批次与分区策略:更适合的目标:减少新增小文件;需要先确认的边界:延迟、迟到数据、资源与下游契约。
- 用格式感知引擎重写:更适合的目标:经常查询的结构化分区;需要先确认的边界:schema、压缩、记录语义与发布原子性。
- Hadoop Archive:更适合的目标:少变更且仍需按文件路径访问的归档;需要先确认的边界:HAR 访问支持、不可变语义、恢复路径。
- 生命周期清理:更适合的目标:已超过保留期限的数据;需要先确认的边界:归属、快照、法律与业务保留要求。
HAR 归档本身不会删除原始输入,并且归档不可变;只创建 archive 不会自动释放原始命名空间。是否支持 HAR URI 还取决于读取应用。文件从加密区进入非加密区的归档会改变保护边界,目标位置需要一并核对。依据 Hadoop Archives Guide。
用独立候选目录重写有界样本
以下是供受控演练使用的 Spark 3.5.7 片段:输入必须是一份已关闭且 schema 已确认的 Parquet 样本,目标必须是尚不存在的候选目录,不指向生产表路径。
source = "hdfs://REPLACE_WITH_NAMESERVICE/REPLACE_WITH_CLOSED_PARQUET_SAMPLE"
candidate = "hdfs://REPLACE_WITH_NAMESERVICE/REPLACE_WITH_NEW_CANDIDATE_PATH"
df = spark.read.parquet(source)
df.repartition(8).write.mode("errorifexists").parquet(candidate)8 只是演练中的计划并行度,不是推荐的生产参数,也不保证所有情形下恰好生成八个数据文件。空分区、按列分区和写入配置都会影响输出。repartition 带来数据重分布成本,应该用样本字节量、任务耗时和目标文件分布调整。候选结果验收前保持独立;本示例不执行发布或删除。Parquet 支持与读写行为见 Spark 3.5.7 Parquet 文档。
验证语义比文件数下降更重要
比较同一不可变输入窗口下的记录数、主键重复、空值分布、关键金额汇总、最早与最晚业务时间,并按业务精度处理浮点汇总误差。对于允许重复的数据,不能仅以去重后记录数相同判为一致;对于分区目录,还要验证分区列推断与目录注册是否保持一致。
再比较文件数量与分布、相同查询的扫描量、运行时间、Shuffle 和失败率。测试需固定数据、SQL 与资源窗口,冷缓存和热缓存分开记录。压缩与布局变化后逻辑行内容可能相同而文件二进制摘要不同,不能强求输出文件摘要等于输入文件摘要。
常见误区与不适用操作
不要用字节拼接合并 Parquet 或 ORC 等带结构元数据的文件。getmerge 的输出是本地文件,它既不是 HDFS 原地合并,也不会自动维护表格式结构。不要为了追求一个文件普遍使用 coalesce(1),单任务瓶颈和重试成本可能抵消收益。
也不要直接移动或覆盖事务表的文件来绕过引擎维护接口。普通文件系统的路径更名与目录系统、查询缓存、在途读取之间并不是一个跨系统事务,发布需要对应表或平台的受支持流程。若治理同时牵涉 DataNode 容量,可以联合 HDFS 容量与副本健康巡检 判断是否有足够双份数据空间。
验收、发布与回退边界
验收包应包含输入快照或不可变清单、候选目录、写入版本、语义比对、性能对照及目标空间预算。先让少量代表性查询读取候选数据,再按数据平台的发布机制切换入口。若持续写入无法隔离、结果不一致或候选负载影响在线作业,停止扩面并保留原输入。
候选未发布时可以放弃本次候选版本,清理由生命周期流程处理;入口切换后但没有新增写入时,可依据已验证机制返回旧版本。新入口已经接收新数据后,不能简单改回旧目录,否则会遗漏新增记录;应先厘清增量并制定重放方案。原数据删除后,回退依赖快照或备份,恢复计划见 Hadoop 高可用与恢复规划。