HDFS的"一次写入"特性导致每次INSERT操作都会生成新文件而非追加到原文件,这是出于并发安全、简单设计和容错性的考虑。
流式写入由于追求低延迟,频繁写入小批量数据(如每5秒一次),会生成大量小文件(如每天上万个小文件)。
小文件会带来NameNode内存压力、Map任务过多和查询性能下降等问题。
解决方案包括定期合并文件、调整流式写入批次大小、使用HBase或Hive3.0+的事务功能等。
批量写入产生少量大文件,而流式写入则产生大量小文件,需要根据场景选择合适的写入策略和后续合并方案。
为什么每次 insert 都会生成新文件,以及流式写入会生成大量小文件
核心原因:HDFS 的“一次写入”特性 + 流式写入的“小批量”特点
这个问题需要从两个层面理解:HDFS 的底层机制和流式写入的方式。
一、为什么每次 INSERT 都会生成新文件?
1.1HDFS 不支持“在文件中间插入数据”
text
传统文件系统(如 Windows): 打开文件 → 找到第100行 → 插入新行 → 保存 ✅ 可以修改原文件 HDFS:打开文件 → 只能追加到末尾,不能修改中间内容 而且,Hive 每次 INSERT 会创建新的独立文件1.2 具体例子
sql
-- 第一次 INSERT insert into table scores values (101, '张三', 90); -- HDFS 生成:/table/scores/000000_0 -- 第二次 INSERT insert into table scores values (102, '李四', 85); -- HDFS 生成:/table/scores/000001_0 (新文件,不是追加到旧文件)HDFS 目录结构:
text
/user/hive/warehouse/scores/ ├── 000000_0 ← 第1次 insert 产生的文件 ├── 000001_0 ← 第2次 insert 产生的文件 ├── 000002_0 ← 第3次 insert 产生的文件 └── ...
1.3 为什么不追加到已有文件?
| 原因 | 说明 |
|---|---|
| 并发安全 | 多个任务同时写同一个文件会导致数据错乱 |
| 简单设计 | 每次写新文件,避免了复杂的锁机制 |
| 容错性 | 某个任务失败,只影响它自己的文件,不会破坏整个表 |
| HDFS 特性 | HDFS 设计为"一次写入,多次读取",追加不是主要场景 |
二、为什么流式写入会产生大量小文件?
2.1 什么是流式写入?
text
传统批量写入: [等待一天] → [一次性把 1亿 条数据写入 HDFS] → 生成 10 个大文件(每个 100MB)流式写入(如 Flume、Kafka): [每 5 秒] → [写入 1000 条数据] → 生成 1 个小文件(可能只有 64KB) [每 5 秒] → [写入 1000 条数据] → 又生成 1 个小文件 [每 5 秒] → [写入 1000 条数据] → 再生成 1 个小文件 ... 一天 86400 秒 ÷ 5 = 17280 个小文件!2.2 形象比喻
| 方式 | 比喻 | 文件情况 |
|---|---|---|
| 批量写入 | 一年买一次衣服,一次买 10 件 | 大文件 |
| 流式写入 | 每天买一件衣服,每次买完打包成一个包裹 | 很多小文件 |
2.3 为什么流式写入不能等一等再写?
流式系统的设计目标是低延迟:
Kafka:数据来了就要马上处理,不能攒着
Flume:实时采集日志,不能缓存太久
如果等 1 小时再写,数据就延迟了 1 小时
矛盾点:
text
流式系统目标:低延迟(马上写) → 产生小文件 大数据最佳实践:大文件(批量写) → 但会延迟三、图解对比
批量写入(可控,大文件)
text
┌─────────────────────────────────────────────────────────┐ │ 等待数据攒够 │ │ ↓ │ │ [1000万条数据] │ │ ↓ │ │ 一次性写入 → 生成 10 个文件(每个 100MB) │ │ ✅ 文件数量可控 │ └─────────────────────────────────────────────────────────┘
流式写入(不可控,小文件)
text
┌─────────────────────────────────────────────────────────┐ │ 第1批(5秒)→ 1000条 → 文件1(64KB) │ │ 第2批(10秒)→ 1000条 → 文件2(64KB) │ │ 第3批(15秒)→ 1000条 → 文件3(64KB) │ │ ... │ │ 第17280批(24小时)→ 文件17280(64KB) │ │ ❌ 文件数量爆炸 │ └─────────────────────────────────────────────────────────┘
四、为什么小文件是“坏”的?
| 问题 | 说明 |
|---|---|
| NameNode 内存压力 | 每个文件/目录占用 NameNode 约 150 字节内存,1 万个文件占 1.5MB,1 亿个文件占 1.5GB |
| Map 任务过多 | 每个小文件至少启动一个 Map 任务,任务启动开销大 |
| 查询慢 | 扫描 1 万个小文件 vs 扫描 10 个大文件,后者快得多 |
| HDFS 不适合 | HDFS 是为大文件设计的(块大小 128MB),小文件浪费元数据 |
五、解决方案对比
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 定期合并 | 每小时执行一次INSERT OVERWRITE | 最常见 |
| 调整流式写入参数 | 设置 Flume 的batchSize更大(如 10000 条再写) | 可以接受一定延迟 |
| 使用 HBase | HBase 基于 HDFS 但支持随机写,不会产生小文件 | 实时读写场景 |
| 改用 ORC + 事务 | Hive 3.0+ 支持 ACID,可以合并小文件 | 需要更新/删除的场景 |
常用合并方法
sql
-- 方法:定期执行合并 set hive.merge.mapfiles=true; set hive.merge.mapredfiles=true; set hive.merge.size.per.task=268435456; -- 256MB -- 覆盖写回(自动合并小文件) insert overwrite table target partition(dt='2024-01-01') select * from target where dt='2024-01-01';
六、总结对比表
| 对比项 | 批量写入 | 流式写入 |
|---|---|---|
| 写入频率 | 低(每小时/每天一次) | 高(每秒/每5秒一次) |
| 单次数据量 | 大(GB 级) | 小(KB 级) |
| 文件大小 | 大(100MB+) | 小(几十 KB) |
| 文件数量 | 少 | 多(爆炸增长) |
| 是否需要合并 | 通常不需要 | 必须定期合并 |
七、一句话总结
INSERT 每次都生成新文件,是因为 HDFS 不支持修改已有文件,写新文件是简单可靠的默认行为。
流式写入产生小文件,是因为它要保证低延迟,数据一来就写,来不及等数据攒够再写。