去年接了一个数据平台的活,团队里几个新同学上来就被 Spark 配置折腾得够呛。明明看文档都会,一上集群就各种ClassNotFoundException、Container exited、物理内存超限。我自己的经验是,Spark 的配置说难不难,但它藏在一堆配置文件、版本兼容性和资源参数里,不把整张地图画出来,你永远都在碰运气。这篇东西我就按从零到 YARN 的顺序,把 Spark 配置这件事彻底拆开。
这篇指南适合三种人:刚接触 Spark 想在本机跑通第一个程序的新手,准备搭测试集群却被版本和路径问题卡住的同学,以及生产环境想从 Standalone 平滑迁移到 YARN 的开发者。整个配置过程我会按实际动手的顺序来讲,每个文件该写什么、为什么这么写、踩过哪些坑,都会交代清楚,你可以直接照着抄。
1. 动手前的准备:环境与版本一次说清
很多教程上来就让你下载 Spark,结果你按着老版本博客配完,跑任务时报一堆错,只能干瞪眼。我先把底层的环境问题说透。
1.1 服务器与依赖清单
Spark 是跑在 JVM 上的,所以第一件事是装 JDK。这里有个关键点:Spark 3.x 要求 Java 8 或 Java 11,Spark 4.x 之后主流推荐 Java 17。很多老博客还停留在 Java 8,你要是用 17 去跑老版本 Spark,大概率直接启动失败。
我在生产环境常用的组合是:
- 操作系统:CentOS 7.9 或 Ubuntu 20.04/22.04 LTS,两者没有本质区别,只是安装依赖包的命令不同
- JDK:OpenJDK 1.8 或 11
- Hadoop:3.2.x 或 3.3.x,主要用于 YARN 和 HDFS
- Spark:3.3.x 或 3.4.x
还有一个容易忽略的依赖是tar、wget、ssh,这些工具在搭建集群时缺一不可。用java -version确认 JDK 装好,再用which ssh确认 ssh 可用,这比啥都重要。
1.2 版本选型:几个关键对应关系
版本选型是配置里最容易踩坑的地方,我直接给你一张我实测过的对应表。
| Spark 版本 | JDK 要求 | Scala 版本 | Hadoop/YARN 兼容建议 |
|---|---|---|---|
| Spark 3.2.x | 8 / 11 | 2.12 | Hadoop 3.2+ |
| Spark 3.3.x | 8 / 11 / 17 | 2.12 | Hadoop 3.3+ |
| Spark 3.4.x | 8 / 11 / 17 | 2.12 | Hadoop 3.3+ |
| Spark 4.0.x | 17 / 21 | 2.13 | Hadoop 3.3+ |
注意一个反直觉的点:Spark 官网编译好的二进制包,默认是针对 Hadoop 3 的版本。你去 Apache 下载页能看到spark-3.3.4-bin-hadoop3.tgz,这个就是最省事的包。以前还有without-hadoop版本,那个要求你自己带上 Hadoop classpath,新手别碰。
选版本时记住一句口诀:先定 Spark,再定 Hadoop,最后定 JDK。这句话的意思是,你的 Spark 版本决定了它能不能跑在某个 Hadoop 版本之上,而 JDK 版本既受 Spark 支持范围限制,又受 Hadoop 支持范围限制。我遇到过最典型的例子是有人用 Hadoop 2.7 配 Spark 3.3,编译时能过,运行时 YARN 的容器调度直接不认,白折腾了两天。
1.3 目录规划与软链接
配置环境的第一个隐性成本是目录混乱。我习惯统一规划:
/opt/jdk # JDK 安装目录 /opt/hadoop # Hadoop 安装目录 /opt/spark # Spark 安装目录 /data/hdfs # HDFS 数据目录 /data/yarn # YARN 本地目录与日志目录 /data/spark-logs # Spark 历史日志用/opt而不是/usr/local的原因很简单:/opt给第三方软件包用,目录结构清晰,升级时直接替换整个目录不影响系统自带程序。多节点集群里,各机器目录保持一致,能少掉一半排查问题的时间。
2. 先把本地模式跑通
不管你想不想用 YARN,我强烈建议先把本地模式跑通。本地模式下 Spark 不需要集群,单机就能跑,它的作用是让你确认环境没问题、依赖没问题、代码逻辑没问题,之后再上 YARN 时定位问题就纯粹是资源问题了。
2.1 下载与解压
下载时我建议直接用 Apache 官方镜像:
wget https://dlcdn.apache.org/spark/spark-3.3.4/spark-3.3.4-bin-hadoop3.tgz tar -zxvf spark-3.3.4-bin-hadoop3.tgz -C /opt ln -s /opt/spark-3.3.4-bin-hadoop3 /opt/spark用软链接ln -s是个好习惯。因为 Spark 升级时,你只需要把软链接指向新版本,所有环境变量不用改,服务重启即可。这个习惯帮我省了大量升级时间。
2.2 环境变量配置
编辑/etc/profile.d/spark.sh:
export SPARK_HOME=/opt/spark export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin export HADOOP_HOME=/opt/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export YARN_CONF_DIR=$HADOOP_CONF_DIRHADOOP_CONF_DIR和YARN_CONF_DIR这两个变量,在你后面接 YARN 时是必须的。Spark 启动时会自动读取这两个目录下的core-site.xml、hdfs-site.xml、yarn-site.xml,不需要你手动写 HDFS 地址。
配好之后用echo $SPARK_HOME验证,然后spark-shell --version看版本信息,能打印出版本就说明基础环境 OK。
2.3 用 spark-shell 验证
启动本机交互式环境:
spark-shell --master local[2]看到Spark context Web UI available at http://localhost:4040就说明起来了。然后跑一个最简单的任务:
val df = spark.range(1000).filter(_ % 2 == 0).count()返回500就没问题。这一步实际验证了三件事:JVM 能启动、Spark 能读取依赖、任务调度能跑通。如果这一步有问题,你上 YARN 基本是浪费时间。
2.4 本地模式能干到什么程度
本地模式本质上是一个进程内模拟的分布式环境,local[2]是启用 2 个线程来模拟 2 个 executor。它能跑通大部分开发测试,但有两个明显问题:
- 没有 HDFS,你的数据要么在本地磁盘,要么自己指定
file://路径 - 没有真正的资源隔离,一个任务内存超了直接拖垮整个 JVM
所以我的建议是:本地模式只用来验证逻辑,真正的集群行为必须上 YARN 验证,特别是内存和并行度相关的问题,本地模式完全模拟不出来。
3. 集群模式下的一些基础架设
在配置 YARN 之前,你得先有 YARN。这一节我不会把 Hadoop 的安装全部展开,但会把和 Spark 衔接最关键的部分讲清楚。
3.1 节点规划建议
一个最小可用的测试集群至少三台机器:
node01 NameNode + ResourceManager node02 DataNode + NodeManager node03 DataNode + NodeManager生产环境里 ResourceManager 和 NameNode 通常分开部署,但测试环境合在一起没毛病。NodeManager 是真正运行 Spark Executor 的地方,所以它的内存和 CPU 直接决定你能跑多大的任务。
3.2 YARN 的几个核心配置
在 Hadoop 的yarn-site.xml里,这几个参数是 Spark 接入时必须对齐的:
<property> <name>yarn.nodemanager.resource.memory-mb</name> <value>16384</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>8</value> </property> <property> <name>yarn.scheduler.minimum-allocation-mb</name> <value>512</value> </property> <property> <name>yarn.scheduler.maximum-allocation-mb</name> <value>16384</value> </property>yarn.nodemanager.resource.memory-mb表示每台机器能分给 YARN 的总内存。注意,这个值不是物理内存总量,而是 YARN 可以拿去跑容器的上限,一般留 2 到 4GB 给操作系统和系统进程。minimum-allocation-mb和maximum-allocation-mb决定了单个容器能申请的内存范围,Spark 的 executor 内存必须落在这个区间内,不然要么分配失败,要么被强制压制。
3.3 验证 YARN 本身是好的
在配 Spark 之前,先用 Hadoop 自带示例验证 YARN 能正常工作:
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.4.jar pi 2 1000如果能跑通并输出一个近似圆周率的值,说明 YARN 调度正常、NodeManager 心跳正常、HDFS 可用。这一步能把你之后的问题范围从"环境坏没坏"缩小到"Spark 配置对不对"。
4. 核心章节:接入 YARN 的完整配置
到这里,前置条件都就位了,开始真正配置 Spark 的 YARN 模式。
4.1 为什么要切到 YARN
很多人会问,Spark 不是自带集群模式吗?为什么非要用 YARN?我的理解是:YARN 是 Hadoop 生态的统一资源调度层,它不只管 Spark,还管 MapReduce、Flink 等计算框架。生产环境里你的集群不可能只跑 Spark,YARN 能让所有计算框架共享同一批机器资源,而且能做到队列隔离和公平调度。
另外还有一个现实原因:你要读 HDFS 上的数据,就绕不开 YARN 的运行环境。虽然 Spark 可以直接读 HDFS,但 YARN 模式能让 Spark 应用以容器方式运行在 NodeManager 上,资源超了由 YARN 直接杀掉,不会拖垮整个节点。
4.2 核心配置文件逐项说明
Spark 接入 YARN 需要改两个文件:spark-env.sh和spark-defaults.conf。
spark-env.sh主要配置运行时环境:
export JAVA_HOME=/opt/jdk export HADOOP_HOME=/opt/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export SPARK_HOME=/opt/spark export SPARK_LOG_DIR=/data/spark-logs这里我特意写JAVA_HOME而不是直接写java,因为有些机器的默认 Java 是系统自带的 OpenJDK,版本不对。显式指定后,Spark 启动脚本会用你指定的 JDK,避免系统更新把默认 Java 版本换掉后 Spark 突然起不来。
spark-defaults.conf是另一个重头文件:
spark.master yarn spark.eventLog.enabled true spark.eventLog.dir hdfs://node01:8020/spark-logs spark.history.fs.logDirectory hdfs://node01:8020/spark-logs spark.yarn.archive hdfs://node01:8020/spark-libs/spark-archive.zip spark.driver.memory 2g spark.executor.memory 4g spark.executor.cores 2 spark.sql.shuffle.partitions 16spark.master设为yarn之后,Spark 的脚本会调用 YARN 的 ResourceManager 提交应用。spark.yarn.archive这个参数容易被忽略,它的作用是把 Spark 的依赖 jar 打包放到 HDFS 上,这样每个 executor 启动时从 HDFS 拉依赖,速度远快于从本地磁盘逐台分发。部署时执行:
cd $SPARK_HOME/jars zip -r spark-archive.zip . hdfs dfs -mkdir -p /spark-libs hdfs dfs -put spark-archive.zip /spark-libs/spark.eventLog.enabled和spark.history.fs.logDirectory配合使用,可以在 Spark History Server 上回看应用的事件日志,排查问题时特别有用。
4.3 资源参数:executor 内存与 core 怎么定
这是配置 YARN 模式的核心知识点。Spark 在 YARN 上会向 ResourceManager 申请两种资源:内存和 CPU。每个 executor 向 YARN 申请的总内存是:
总内存 = spark.executor.memory + spark.executor.memoryOverheadspark.executor.memory是堆内内存,主要给 RDD、DataFrame、Shuffle 数据用。memoryOverhead是堆外内存,默认是 executor.memory 的 10%,还设有下限 384MB,给 JVM 的 Metaspace、线程栈、网络缓冲等用。我在生产环境常用的经验参数是:
spark.executor.memory 4g spark.executor.memoryOverhead 1g spark.executor.cores 2这里最关键的检查点是:executor 总内存加 driver 内存,不能超过 NodeManager 可用资源。比如 NodeManager 有 16GB 内存、8 核,你有 3 台机器,那么并发起来最多同时跑的容器数大概是:
单节点容器数 = 16GB / (4GB + 1GB) ≈ 3如果你强行把 executor.memory 设成 8GB,单个容器 8GB 加 overhead 900MB,YARN 可能因为超过maximum-allocation-mb直接拒绝分配,任务卡在 ACCEPTED 状态不动。这是新手最常遇到的事。
4.4 在 YARN 上跑第一个任务
配置完成后,用官方示例验证:
$SPARK_HOME/bin/spark-submit \ --master yarn \ --deploy-mode cluster \ --class org.apache.spark.examples.SparkPi \ $SPARK_HOME/examples/jars/spark-examples_2.12-3.3.4.jar跑通后访问 ResourceManager 的 Web 界面(默认 8088 端口),可以看到一个SparkPi的应用状态从 ACCEPTED 变成 RUNNING,最后 FINISHED。再去 YARN 日志或者 Spark History Server 里看 stdout,能找到计算结果。
我习惯再跑一个读 HDFS 文件的 WordCount 来做更真实的验证:
val rdd = sc.textFile("hdfs://node01:8020/data/input.txt") rdd.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).saveAsTextFile("hdfs://node01:8020/data/output")这次验证覆盖了数据源、Shuffle、结果输出,只要你这三个环节都通,Spark 在 YARN 上的链路就算彻底打通了。
5. 内存配置再深挖
很多 Spark 任务跑挂了,九成跟内存有关。这一节我重点把内存配置背后的逻辑说透,而不是只让你抄参数。
5.1 Spark 内存模型快速梳理
Spark 3.0 之后启用了统一内存管理。一个 executor 的堆内内存分三块:
- Storage 区域:存 RDD 缓存和 Broadcast 数据
- Execution 区域:存 Shuffle、聚合、Join 产生的临时数据
- Reserved 区域:为系统预留
两类区域之间可以互相借用,Storage 空闲时 Execution 可以借过去,反之亦然。这就是"统一内存"的含义。所以在配置上,spark.memory.fraction默认 0.6,表示堆内内存 60% 分配给 Storage 和 Execution 共同使用,其余留给用户代码生成的临时对象。
5.2 常见内存配置参数对照
| 参数 | 默认值 | 作用 | 我常用的调整方向 |
|---|---|---|---|
| spark.executor.memory | 1g | 堆内内存上限 | 根据数据量调大,2g 起步 |
| spark.executor.memoryOverhead | 10% | 堆外内存 | 大 Shuffle 时手动提高到 1g |
| spark.driver.memory | 1g | Driver 端堆内内存 | 有collect操作时调到 2g 以上 |
| spark.memory.fraction | 0.6 | Storage+Execution 占比 | 纯 Shuffle 任务可调到 0.7 |
| spark.shuffle.file.buffer | 32k | Shuffle 写缓冲 | 大 Shuffle 时调到 128k |
一个容易忽略的点:spark.driver.memory。当你用collect把数据拉回 Driver,或者 Driver 要广播大字典时,默认 1g 远远不够。我见过一个任务把几十万行结果collect回 Driver,直接 OOM 把 YARN 上的 AM 容器干掉了,重启循环直到失败。
5.3 数据倾斜与内存问题的实战关联
很多内存异常根因是数据倾斜,而不是配置参数本身。数据倾斜最简单的表现是:整个任务卡在某个 stage 的 99%,日志里大量GC overhead limit exceeded。
我的排查思路是:先用 Web UI 看 Shuffle Read Size 是否严重不均,然后看单个 executor 的 GC 时间是否异常长。定位后优先通过加盐两阶段聚合、调整分区数解决,而不是一味调大内存。盲目把 executor.memory 从 4g 调到 8g,YARN 可能就直接没资源可用了。
6. 常见问题与排查技巧实录
最后这部分是我踩坑最多的地方,我整理成速查表,再讲几个典型的排查思路。
6.1 问题速查表
| 症状 | 常见原因 | 快速检查方法 | 解决方案 |
|---|---|---|---|
| Application ACCEPTED 后一直不启动 | 资源不足,等待调度 | 看 YARN UI 的 Pending | 降低 executor 内存或增加队列资源 |
Container exited with code 143 | executor 被 YARN 强杀(内存超限) | 看 NodeManager 日志 | 调大 memoryOverhead |
ClassNotFoundException: org.apache.spark.* | Spark 依赖未传到 executor | 检查 spark.yarn.archive | 执行 archive 打包上传 |
| Spark 起不来,Java 版本报错 | JDK 不兼容 | java -version对照 Spark 要求 | 到 spark-env.sh 指定 JAVA_HOME |
读 HDFS 报FileNotFoundException | HDFS 地址写错 | 确认hdfs-site.xml中的 NameNode 地址 | 用hdfs dfs -ls测试 |
日志显示No space left on device | 本地临时目录满了 | df -h看 /data 分区 | 配spark.local.dir到多块盘 |
6.2 一套靠谱的日志排查顺序
我的固定排查套路是自下而上看日志,先容器后框架:
- 先看 YARN ResourceManager 日志,确认应用是否被接收、被调度到哪个队列
- 再看 NodeManager 日志,确认容器启动失败的具体异常
- 然后看 Spark 应用的
stderr和stdout,这通常浓缩了真正的 JVM 异常 - 最后看 Spark History Server 里的事件日志,按 stage 定位失败点
很多人一上来就grep Exception,结果 Log 里一堆 Footer 异常,浪费时间。顺序不对,容易被假象带跑。
6.3 几条独家避坑心得
第一,统一各节点的时间。YARN 对容器启动有时间判断,如果 NodeManager 和 ResourceManager 之间时间偏差大,会出现容器被判定为超时莫名杀掉。集群里装个 NTP 同步服务,是最省心的操作。
第二,spark.local.dir 一定要配。默认情况下 Spark 用/tmp作为本地临时目录,而/tmp在部分系统里容量很小,Shuffle 数据一多直接写满。配到独立数据盘:
spark.local.dir /data/spark-tmp,/data2/spark-tmp多个目录用逗号分隔,Spark 会把临时文件分摊到不同磁盘,IO 压力也分散了。
第三,生产环境别轻易改 spark.sql.shuffle.partitions。默认 200 对大部分场景是合理的,不会太多也不会太少。我见过有人为了调性能改成 10,结果每个 task 的数据量暴增,Executor 直接 OOM。这个参数要结合文件大小、executor 数量来算,而不是拍脑袋。大概的参考是让每个 Shuffle 分片控制在 100MB 左右,用总 Shuffle 数据量除以 100MB 得到分区数。
第四,测试完记得关掉 Spark Shell。这个听起来像废话,但我见过开发环境里一堆僵尸 spark-shell 挂着,把 YARN