1. 问题现象
Spark 跑批任务偶尔失败,异常信息如下:
Exception in thread "main" java.io.IOException:
Unable to close file because the last block
BP-2064594970-172.16.76.254-1582895310342:blk_1313413793_239728256
does not have enough number of replicas.
从报错信息看,失败点不是 Spark SQL 语法,也不是 Executor 计算逻辑,而是写 HDFS 文件时,在关闭文件阶段发现最后一个 block 的副本数不够。
这个问题通常会出现在下面几类场景中:
- Spark 写 HDFS、Hive、Parquet、ORC 时任务失败。
- 写入过程中某些 DataNode 短暂不可用。
- HDFS 集群磁盘空间不足,或者部分节点磁盘满。
- 文件写完 close 时,最后一个 block 没有达到最小副本要求。
2. 先理解 HDFS 写文件过程
HDFS 写文件时,客户端不是直接把数据写到 NameNode。NameNode 负责元数据和 block 分配,真实数据会写到 DataNode。
一个文件会被切分成多个 block,每个 block 按照副本数写入多个 DataNode。默认情况下,HDFS 副本数通常是 3。
写入流程可以简单理解为:
客户端请求创建文件
NameNode 分配 block 和 DataNode
客户端向 DataNode pipeline 写入数据
DataNode 之间按 pipeline 复制副本
文件写完后客户端 close 文件
NameNode 检查 block 状态
如果最后一个 block 在 close 时没有达到足够副本,客户端就可能看到:
Unable to close file because the last block does not have enough number of replicas
3. 这个异常说明什么
这个异常不是简单的“Spark 失败”,而是 HDFS 对文件完整性检查没有通过。
重点是这几个词:
close file
last block
not have enough number of replicas
含义是:
| 关键词 | 含义 |
|---|---|
| close file | 文件已经写到最后阶段,准备关闭 |
| last block | 出问题的是文件最后一个 block |
| replicas | HDFS block 副本 |
| not enough | 当前可用副本数没有达到要求 |
所以排查方向应该先放到 HDFS 集群健康状态,而不是先改 Spark 参数。
4. 常见原因
4.1 DataNode 不稳定
写入过程中,如果某个 DataNode 宕机、网络抖动、磁盘故障,pipeline 中的副本写入就可能失败。
可以先看 DataNode 状态:
hdfs dfsadmin -report
重点看:
Live datanodes
Dead datanodes
Decommissioning datanodes
如果任务失败时刚好有 DataNode 掉线,基本就能解释这个异常。
4.2 HDFS 空间不足
如果集群剩余空间不足,或者部分 DataNode 磁盘打满,HDFS 可能无法为最后一个 block 补齐副本。
查看 HDFS 空间:
hdfs dfsadmin -report
也可以查看具体目录:
hdfs dfs -du -h /path/to/output
如果 DataNode 之间空间不均衡,还可以考虑执行 balancer:
hdfs balancer
4.3 副本数设置过高
如果集群可用 DataNode 数量不足,但文件副本数要求较高,也会导致副本无法满足。
查看目录或文件副本数:
hdfs dfs -stat %r /path/to/file
如果只是测试环境,可以临时降低副本数:
hdfs dfs -setrep -w 2 /path/to/file
生产环境不建议盲目降低副本数。副本数降低之后,容错能力也会下降。
4.4 存在损坏 block 或丢失 block
草稿里提到“hdfs 丢 block 了吧”“副本不够了”,这个判断方向是对的。
可以通过 fsck 检查目标路径:
hdfs fsck /path/to/output -files -blocks -locations
如果想看缺失 block:
hdfs fsck / -list-corruptfileblocks
如果输出中出现 MISSING、CORRUPT、Under replicated,说明 HDFS 本身已经存在 block 健康问题。
5. 解决办法
5.1 先确认集群是否健康
优先执行:
hdfs dfsadmin -report
hdfs fsck /path/to/output -files -blocks -locations
确认:
- DataNode 是否全部存活。
- 磁盘空间是否充足。
- 是否存在 under replicated block。
- 是否存在 corrupt block。
5.2 等待 HDFS 自动补副本
如果只是短暂副本不足,并且 DataNode 已恢复,HDFS 通常会自动补副本。
可以观察一段时间后再次执行:
hdfs fsck /path/to/output -files -blocks -locations
如果 under replicated 数量下降,说明集群正在自动恢复。
5.3 删除失败输出目录后重跑任务
Spark 写 HDFS 失败后,目标目录可能只写了一部分数据。不要直接在这个目录上继续跑。
建议先删除失败输出:
hdfs dfs -rm -r /path/to/output_tmp
然后重新提交任务。
如果是 Hive 分区写入,建议先清理对应分区目录,再重跑该分区。
5.4 修复 DataNode 或磁盘问题
如果 dfsadmin -report 显示某些 DataNode 已经不可用,需要先处理节点:
systemctl status hadoop-hdfs-datanode
查看 DataNode 日志:
tail -200 $HADOOP_LOG_DIR/hadoop-*-datanode-*.log
重点关注:
DiskErrorException
Volume failed
DataXceiver
PacketResponder
如果是磁盘故障,要先处理磁盘或下线故障 DataNode,再让 HDFS 重新复制副本。
6. Spark 侧可以怎么处理
Spark 侧能做的是减少这类问题的放大影响,而不是修复 HDFS。
可以检查输出路径是否使用临时目录:
/path/to/table/.spark-staging-xxx
任务失败后要清理临时目录,避免下次任务读到半成品数据。
如果输出数据量很大,也可以适当控制写文件数量:
df.repartition(200)
.write
.mode("overwrite")
.parquet("/path/to/output")
文件数太多会带来更多 block 和 NameNode 元数据压力,但文件数太少又可能导致单文件过大。这个参数需要结合数据量和集群规模调整。
7. 总结
这个异常的根因不是 Spark 计算错误,而是 HDFS 写文件 close 阶段发现最后一个 block 副本数不足:
Unable to close file because the last block does not have enough number of replicas
排查时优先看 HDFS:
hdfs dfsadmin -report
hdfs fsck /path/to/output -files -blocks -locations
hdfs fsck / -list-corruptfileblocks
如果是偶发问题,通常和 DataNode 抖动、磁盘空间不足、block 副本恢复不及时有关。生产上不要只重跑 Spark 任务,应该把 HDFS 副本不足的原因查清楚,否则后续跑批还会继续偶发失败。