单条记录超过分区大小时,Spark 会如何读取和处理?
结论
是的。按行读取文本时,Spark 底层的读取器可以越过输入分片的字节边界,继续读完当前记录。spark.sql.files.maxPartitionBytes 用于文件扫描的分区规划,不是单条记录大小的硬限制,也不是 task 的内存配额。单条记录超过该值,任务也不一定失败。Spark 分区配置说明
一条约 300 MB 的 JSON 文本记录即使跨越多个规划分片,也不会自动变成多条可独立处理的记录。负责输出它的 task 仍需承担整条记录的读取和处理开销。
先确认“一条记录”的读取方式
原问题没有提供读取代码,需要区分两种情况:
spark.read.text(path)默认按行生成记录。只有整个 JSON 位于文件的同一行时,才会自然形成“一文件一记录”。spark.read.option("wholetext", "true").text(path)将整个文件读为一条记录,文件内部可以有换行。
这两种方式都会得到字符串类型的 value 列,不会推断 JSON 内部字段的 schema。Spark 文本数据源说明
单行记录跨越分片边界时会发生什么
以未压缩、可切分的文本文件为例。假设文件只有一行,大小约 300 MB,扫描规划采用约 128 MiB 的分片:
- 规划阶段按文件字节偏移划分输入范围,不需要先找出所有行边界。
- 从文件开头读取的 task 会继续读取,直到遇到行结束符或文件末尾,实际读取范围可能超过分片终点。
- 从同一条长记录中间开始的其他分片需要跳过开头的不完整记录,避免重复输出。
因此,部分分片可能读取或跳过了数据,却没有输出记录。分片的字节范围和最终记录边界并不完全一致。Hadoop 的 LineRecordReader 实现包含跨越分片终点读行、跳过非首分片开头记录的逻辑。Hadoop 读取器源码
如果使用 wholetext=true,Spark 的文本数据源会将该文件视为不可切分输入,交给一个 task 读取。文件大小仍可能超过配置的分区规划值。一个 task 也可能顺序读取多个文件,所以“一文件一记录”不等于“一分区一个文件”。Spark TextFileFormat 实现
为什么会 OOM,调大配置为何可能有帮助
文件分区通常通过迭代器逐条读取,不要求整个分区同时驻留内存。但要把完整 JSON 放进一行字符串,读取过程就必须构造这条大记录。后续复制、序列化或解析还可能增加内存开销;如果传入 Python UDF,Python 进程也可能持有额外的数据表示。
因此,300 MB 的文件不能直接对应为“只需要 300 MB 内存”,也不能据此推算出通用的安全倍数。
调大 spark.sql.files.maxPartitionBytes 会改变输入分片及文件组合方式,可能减少分片数量、跨边界扫描,或某些运行条件下的并发压力。这些都是现象的可能原因。调大配置也可能让一个 task 负责更多输入,增加处理时间和内存压力。
仅凭“调大后改善”无法确定 OOM 的原因,还需要读取 API、Spark 版本、压缩方式、异常堆栈和执行计划。尤其在 wholetext=true 时,调大配置并没有让单个文件变小或变得可切分。
如何排查和处理
先核实实际使用的读取方式,再根据文件结构选择下面的一种写法:
path = "/data/json"
# 整个 JSON 位于一行:默认按行读取
lines = spark.read.text(path)
# JSON 可能跨多行,且业务要求一个文件对应一条记录
files = (
spark.read
.option("wholetext", "true")
.text(path)
)
wholetext=true 用于明确记录边界,不会降低大记录的内存需求。如果使用的是 SparkContext.textFile 或 wholeTextFiles,也不能直接套用 Spark SQL 文件源配置的解释。
接着按以下顺序检查:
- 定位 OOM 出现的位置。区分 executor JVM、Python worker 和 driver,查看失败发生在读取、JSON 解析、UDF 还是结果收集阶段。
collect()或toPandas()引起的 driver 内存问题需要单独处理。 - 对比配置调整前后的任务情况。在 Spark UI 中查看扫描阶段的 task 数、实际输入量和并发情况,确认调参改变了什么。
- 按单条记录的处理需求安排资源。如果业务必须保留整份 JSON,需要为完整记录及后续处理留出内存,并结合 executor 内同时运行的 task 数评估压力。
- 能调整数据格式时,缩小逻辑记录。如果大 JSON 中包含可独立处理的元素,可以在上游或预处理阶段使用流式解析,将它们输出为较小的记录。不能直接按固定字节数切割 JSON,也不能仅插入换行就认为得到了有效记录。
读取后调用 repartition() 可以重新分配已有记录,但无法把一条 300 MB 的字符串自动拆开,也无法解决读取该记录时已经发生的 OOM。溢写机制同样不能保证任意大的单条字符串都能成功构造。
备注:内容仅供参考。
