3、大文件问题详解

聊完小文件的坑,咱们来看看另一个极端——大文件。很多人觉得文件越大越好,省得切分。其实不然。大文件带来的问题,有时候比小文件更隐蔽,也更难排查。

我个人习惯把超过 HDFS 默认块大小(128MB)好几倍的文件,称为大文件。但说实话,这个定义得看场景。在 Spark 里,一个文件如果导致单个分区处理时间远超其他分区,那它就是大文件。

核心观点:大文件不是文件本身大,而是它让 Spark 的并行度失效了。

3.1 到底多大算大文件?

先给个参考标准。我一般这么划分:

文件大小 分类 典型影响
128MB ~ 1GB 中等文件 基本可控,但需关注
1GB ~ 10GB 大文件 资源倾斜开始明显
10GB+ 超大文件 GC 压力、数据本地性失效

注意,这个表不是的。如果你的集群有 1000 个核,一个 1GB 的文件可能不算大。但如果你只有 10 个核,那 1GB 文件就能让你头疼。

3.2 大文件导致的资源倾斜

这是大文件最直接的问题。Spark 读取文件时,默认按文件块划分分区。一个 10GB 的文件,如果只被分成 1 个分区,那这个任务就得独自处理 10GB 数据。

其他任务可能几秒就跑完了,这个任务要跑几十分钟。整个 Stage 的耗时,就被这一个任务拖死了。

我在项目中遇到过这种情况:一个 5GB 的 CSV 文件,集群 100 个核,结果只有一个 task 在处理它。其他 99 个 task 都在等。当时我盯着 Spark UI 看,那个 task 的进度条走得跟蜗牛一样。

避坑指南:我曾经以为加资源就能解决,加了 200 个核,结果还是只有一个 task 在处理那个大文件。后来才明白,问题不在资源,在分区数。

为什么会这样?因为 Spark 默认的 spark.sql.files.maxPartitionBytes 是 128MB。但如果你只有一个文件,它不会自动切分。说白了,文件内部的切分,取决于文件格式是否支持 split。

支持 split 的格式(如 Parquet、ORC、纯文本)可以按偏移量切分。不支持 split 的格式(如 Gzip 压缩的文本、某些序列化格式)就只能整个文件作为一个分区。

3.3 GC 压力:大文件的隐形杀手

资源倾斜是看得见的,GC 压力是看不见的。但它往往更致命。

当一个 task 处理大文件时,它需要加载大量数据到内存。如果数据量超过了 Executor 的内存上限,就会频繁触发 GC。

我见过一个案例:一个 20GB 的 JSON 文件,Executor 内存给了 4GB。结果 GC 时间占了总运行时间的 60%。任务跑了 2 小时,其中 1 小时 12 分钟在 GC。

GC 的典型表现:

  • Young GC 频繁:每秒好几次,CPU 飙升
  • Full GC 出现:任务卡顿,甚至 OOM
  • 任务进度条不动:其实是在 GC

注意:GC 问题不是加内存就能解决的。加内存只是把问题推迟了。根本办法是减少单个 task 处理的数据量。

怎么判断是不是 GC 问题?看 Spark UI 的 Task 页面,有个 GC Time 列。如果 GC Time 占总时间的 20% 以上,就得警惕了。

3.4 数据本地性失效

这个点很多人会忽略。Spark 调度任务时,会尽量让 task 在数据所在的节点上运行,这叫数据本地性。但大文件会破坏这个机制。

举个例子:一个 10GB 的文件,存储在 3 个节点的 HDFS 上。但 Spark 只把它分成 1 个分区。那这个 task 只能在一个节点上运行。其他两个节点上的数据,就得通过网络传输过来。

数据本地性从 PROCESS_LOCAL 降级为 NODE_LOCAL,甚至 RACK_LOCAL。网络传输的开销,比本地磁盘读取慢 10 倍以上。

我遇到过最夸张的情况:一个 50GB 的文件,数据本地性全是 ANY。任务跑了 3 小时,其中 2 小时在等网络传输。

关键点:数据本地性失效,本质上是分区粒度和存储粒度不匹配。文件块分布在多个节点,但分区只有一个,本地性自然就丢了。

3.5 大文件问题的核心逻辑

我把大文件问题的因果关系画了张图,方便你理解:

大文件问题核心逻辑 单个大文件 分区数少(1个) 资源倾斜 GC压力增大 数据本地性失效 任务耗时剧增,性能急剧下降 核心矛盾:文件存储的分布式 vs 任务处理的单点化

你看,大文件问题的根源就一个:文件太大,分区太少。然后三个问题同时爆发,互相叠加。资源倾斜让某个 task 变慢,GC 压力让这个 task 更慢,数据本地性失效又增加了网络开销。最后就是整个作业被拖垮。

3.6 怎么判断是不是大文件问题?

给你几个快速判断的方法:

  1. 看 Spark UI 的 Stage 页面:如果大部分 task 很快完成,只有一两个 task 耗时特别长,大概率是资源倾斜。
  2. 看 GC Time 占比:如果某个 task 的 GC Time 超过 20%,说明内存压力大。
  3. 看数据本地性级别:如果大部分 task 是 NODE_LOCAL 或 ANY,而不是 PROCESS_LOCAL,说明本地性失效。
  4. 看输入数据量:如果某个 task 的输入数据量是其他 task 的 10 倍以上,那肯定是大文件问题。

小技巧:我习惯在 Spark UI 的 SQL 页面看 Input Size / Records 列。如果某个节点输入量特别大,直接点进去看是哪个文件。十有八九就是大文件。

3.7 大文件问题的本质

说到底,大文件问题的本质是 并行度与数据分布的错配。Spark 的并行度取决于分区数,而分区数又受限于文件的可切分性。当文件不可切分时,并行度就失效了。

你想想看,Spark 设计之初就是为了处理海量小文件?不是,它是为了处理大文件。但这里的"大"是相对的。Spark 希望的是"大而可切分"的文件,而不是"大而不可切分"的文件。

所以,解决大文件问题的思路也很明确:要么让文件可切分(换格式),要么手动增加分区(repartition)。具体怎么做,咱们后面章节会详细讲。

嗯,这里要注意一点:不要一看到大文件就想着 repartition。如果文件本身支持 split,Spark 会自动切分。只有不支持 split 的格式,才需要手动干预。