1. 为什么Spark环境搭建这么容易卡壳
我见过不少刚接触Spark的同事,第一周几乎都在跟环境搏斗:有人卡在版本不匹配,装完Spark起不来;有人被一大串ClassNotFoundException整到怀疑人生;还有人跟着老教程配了半天,结果连spark-shell都进不去。说实话,Spark的开发环境搭建本身不算复杂,但它踩坑的点非常集中——版本、依赖、配置、权限,任何一个环节出问题,都会在启动那一下集中爆发。
这篇博文的定位很明确:基于Linux环境,带你从零开始完整搭建一套Spark开发环境,从版本选型、JDK/Scala/Python准备、Spark安装、核心配置文件逐项拆解,到本地模式验证、Standalone集群启动,最后把最常见的三类故障排查链路完整走一遍。适合第一次接触Spark、或者之前只在Windows上跑过单机示例、现在想正经在Linux上搭一套可扩展开发环境的同学参考。
我会尽量把每一步“为什么要这么做”讲清楚,而不是扔给你一条命令了事。因为只有理解了配置背后的逻辑,后面遇到诡异报错时你才知道往哪个方向查。
2. 动手之前:版本选型与全局规划清单一
很多教程上来就让你下载安装,但版本选型才是这整套环境能不能稳定跑起来的关键。Spark的版本号只是表面,真正影响你安装决策的是它和Hadoop、JDK、Scala、Python之间的匹配关系。
2.1 为什么我必须先强调Spark与Hadoop版本的匹配
Spark本身不存储数据,它需要对接存储层。本地开发时可以不依赖HDFS,但生产环境的Spark必然要读写HDFS。Spark官方发行版分两种:一种是源码包,需要自己用Maven编译,除非你要改源码,否则不推荐;另一种是预编译版本,官网会明确标注Pre-built for Apache Hadoop 3.3.4之类的字样,下载解压就能用。
预编译版本里已经内置了对应Hadoop版本的客户端依赖,省去了你自己处理依赖冲突的痛苦。我的建议是:如果你没有特殊要求,直接选Spark 3.5.x + Hadoop 3.3.x的预编译包,这是目前我用下来最稳的组合。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Linux发行版 | Ubuntu 20.04/22.04 LTS或CentOS 7.9+ | 内核版本不要太老,影响网络和IO性能 |
| JDK | 8或11(首选11) | Spark 3.x在JDK 8/11上最稳妥,17部分版本有兼容问题 |
| Spark | 3.5.x | 当前稳定版,API和生态最成熟 |
| Hadoop | 3.3.x | 与预编译版匹配即可,本地不装HDFS也行 |
| Python | 3.8~3.10 | PySpark对3.11/3.12的支持目前仍然比较保守 |
| Scala | 2.12 | 跟随Spark自带的Scala版本,不要自己另装,除非你需要开发Scala应用 |
这里插一句:很多新人会问“我已经装了Hadoop,还需要再装一遍吗?”——如果你只是搭建Spark开发环境跑示例和开发调试,不强制装HDFS。Spark本地模式用file://协议读文件就行,等真正要模拟分布式存储时再补Hadoop不迟。但如果你打算跑Standalone集群并且希望它有生产感,还是建议配上HDFS。
2.2 JDK、Scala、Python的选择逻辑
JDK是硬依赖。Spark本身用Scala编写,运行在JVM之上,没有JDK连start-master.sh都跑不起来。我的习惯是装OpenJDK 11,不要装Oracle JDK,除非公司有明确要求。OpenJDK在Ubuntu上一条apt install openjdk-11-jdk就能装好,省事。
Scala这块最容易出问题。Spark 3.5默认使用Scala 2.12编译,你在写Spark应用时也建议用Scala 2.12版本,否则会遇到二进制不兼容的报错。但要注意:如果你只是用spark-shell或spark-submit跑Spark自带的程序,不需要单独安装Scala——Spark发行包里的jars/目录已经带了必需的Scala库。很多人以为要单独配Scala环境,其实那是开发Scala应用时才需要做的事。
Python的选择则关系到你用不用PySpark。如果只用Scala写Spark作业,Python版本不匹配也无所谓;但只要你会用到pyspark,就必须把PYSPARK_PYTHON环境变量指到可用的Python解释器上。而且用PySpark时要注意,driver和executor用的是同一个PYSPARK_PYTHON路径,集群里每台机器都要有相同版本的Python,否则执行阶段必然报ModuleNotFoundError。
2.3 资源需求与目录规划
别小看资源规划,很多环境起不来就是因为磁盘被日志塞满,或者内存分配超出机器物理上限。
- 磁盘:Spark安装包解压后约3GB,加上日志和临时文件,建议预留20GB以上。
- 内存:本地模式至少4GB可用内存;如果跑Standalone集群,每台Worker建议8GB以上。
- CPU:开发环境2~4核够用,集群环境按需求调整。
目录规划我推荐一套通用方案:
/opt/spark # Spark主目录(通过软链接指向解压目录) /data/spark/tmp # Spark临时文件目录 /data/spark/logs # Spark日志目录 /home/spark # 专门用户的主目录,存放脚本和测试代码为什么要拆这么细?因为Spark运行时会大量写临时文件,日志也会持续增长。如果你把临时目录放在根分区或者系统目录,跑几天就会发现磁盘满了。专门划一个数据盘挂载到/data,后面清理和扩容都方便。
3. Linux环境基础依赖的安装与验证
Spark安装本身的步骤不多,但基础依赖如果没准备好,后面每一步都可能炸。这一节我按顺序讲清楚每项依赖怎么装、怎么验证。
3.1 JDK安装:tar包方式为什么更可控
在Ubuntu上安装OpenJDK 11,最快的方式是:
sudo apt update sudo apt install -y openjdk-11-jdk但我个人更推荐用tar包方式安装,尤其当你需要在多台集群机器上保持完全一致的JDK版本时。apt源里的版本可能随源更新而变化,而tar包可以锁死版本:
# 下载OpenJDK 11的tar包,解压到/opt/jdk-11 sudo tar -zxvf openjdk-11.0.21_linux-x64_bin.tar.gz -C /opt/ # 创建软链接,方便更换版本 sudo ln -s /opt/jdk-11.0.21 /opt/jdk然后配置JAVA_HOME环境变量:
echo 'export JAVA_HOME=/opt/jdk' >> ~/.bashrc echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc source ~/.bashrc验证:
java -version如果输出类似openjdk version "11.0.21",说明JDK就绪。这一步错不了,但如果你的Linux环境之前装过其他版本JDK,务必确认java -version输出的确实是你指定的版本,不要被PATH顺序干扰。
3.2 Python环境的坑提前踩掉
如果你打算用PySpark,Python的安装要特别注意版本。根据我的实测,Python 3.8~3.10和Spark 3.5配合最顺畅,Python 3.11也能用但偶尔会碰到第三方库还没适配的情况,Python 3.12则建议避开。
# Ubuntu上安装Python 3.10 sudo apt install -y python3.10 python3.10-venv python3-pip安装完后,一定要明确PYSPARK_PYTHON指向哪个解释器:
echo 'export PYSPARK_PYTHON=/usr/bin/python3.10' >> ~/.bashrc这个变量很多人容易忽略,但Spark在执行Python任务时会通过它找到Python解释器。不设置的话,Spark会默认使用python命令,而某些Linux系统里python默认指向Python 2,分分钟报错。
3.3 SSH免密登录与hosts解析
这一节主要面向Standalone集群模式。如果你只跑本地模式,可以跳过,但建议还是配好,毕竟后面一旦扩展到多节点就会用到。
# 生成密钥对(已有可跳过) ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa # 将公钥加入 authorized_keys cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys验证:
ssh localhost如果不用输密码就能登录,说明免密配置成功。同时检查/etc/hosts,确保本机的主机名能被解析:
# 把本机IP和主机名加入 hosts echo "192.168.1.100 spark-master" | sudo tee -a /etc/hosts hostname # 确认主机名这个细节很容易被忽略,但Spark的Master启动时会注册主机名,Worker节点也需要通过主机名访问Master。如果主机名解析不了,你会看到Worker一直在报Unable to connect to master。
4. Spark本体安装:下载、解压与目录结构
基础依赖准备好后,就可以装Spark本体了。这里每一步我都会写清楚验证方式,避免“装完之后不知道成没成功”的尴尬。
4.1 下载预编译包:源码包不要碰
进入 Spark官网下载页面 ,选择:
- Spark版本:3.5.x(最新稳定版)
- 包类型:Pre-built for Apache Hadoop 3.3.4(或3.3.x)
- 下载方式:直接下载tgz
# 用wget下载到/opt目录 sudo wget https://archive.apache.org/dist/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3.tgz -P /opt/下载完成后,建议先做sha512校验(官网提供了校验文件)。这一步虽然多花半分钟,但能防止下载到损坏的包。校验方法:
cd /opt echo "<从官网复制的sha512值> spark-3.5.1-bin-hadoop3.tgz" | sha512sum -c -输出OK就说明文件完整。
4.2 解压与目录迁移
sudo tar -zxvf /opt/spark-3.5.1-bin-hadoop3.tgz -C /opt/ # 创建软链接,统一管理版本 sudo ln -s /opt/spark-3.5.1-bin-hadoop3 /opt/spark # 创建你规划好的目录 sudo mkdir -p /data/spark/tmp /data/spark/logs sudo chown -R $(whoami) /data/spark /opt/spark*这里我把/opt/spark用软链接指向具体版本目录,好处是以后升级Spark时只需要重新解压、改软链接,环境变量和配置都不用动。
解压完成后,建议花一分钟时间熟悉一下目录结构:
/opt/spark/ ├── bin/ # 可执行脚本(spark-shell, spark-submit等) ├── sbin/ # 集群管理脚本(start-master.sh, start-worker.sh等) ├── conf/ # 核心配置文件目录 ├── jars/ # 所有依赖jar包 ├── examples/ # 官方示例 ├── python/ # PySpark源码与包 └── data/ # 一些示例数据如果你需要经常和Spark打交道,建议把bin/目录加入PATH;但如果只是偶尔用,直接用全路径也可以。
4.3 环境变量配置与验证
编辑~/.bashrc,追加以下内容:
export SPARK_HOME=/opt/spark export PATH=$SPARK_HOME/bin:$SPARK_HOME/sbin:$PATH export HADOOP_CONF_DIR=/opt/spark/conf # 如果没有Hadoop,这一行可以省然后:
source ~/.bashrc spark-submit --version正常情况下会输出Welcome to Spark和版本信息。如果这里报错,99%的原因是JAVA_HOME没设置好,回到第3.1节检查。
5. Spark配置文件的修改逻辑:不仅要改,还要懂为什么
Spark安装完可以直接跑,但要跑得舒服、跑得明白,必须理解它的几个核心配置文件。很多人一上来就照着网上的配置模板复制粘贴,结果参数填错或者含义理解偏差,出了问题根本无从排查。
5.1 spark-env.sh:集群运行时的环境入口
在$SPARK_HOME/conf目录下,先把模板复制一份:
cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh这个文件里可以配置全局环境变量。以下是开发环境很常用的一组配置:
# 指定JDK路径(如果用tar包安装,这里必须显式指定) export JAVA_HOME=/opt/jdk # 指定Python解释器 export PYSPARK_PYTHON=/usr/bin/python3.10 # Master节点主机名 export SPARK_MASTER_HOST=spark-master # Worker节点的CPU核心数与内存 export SPARK_WORKER_CORES=4 export SPARK_WORKER_MEMORY=8g # 日志目录 export SPARK_LOG_DIR=/data/spark/logs # 临时目录 export SPARK_TMP_DIR=/data/spark/tmp每项配置的含义:
JAVA_HOME:如果你是通过apt装的OpenJDK,可以省略;如果是tar包方式,必须指定,否则Spark找不到JVM。SPARK_MASTER_HOST:Master绑定的主机名或IP。如果不设置,默认取本机主机名,一旦/etc/hosts里没有对应映射,Worker连不上就是这里出的问题。SPARK_WORKER_CORES和SPARK_WORKER_MEMORY:决定每个Worker节点能分配多少资源给Executor。注意这个值要小于物理机的实际资源,比如8GB内存的机器可以设6g,留一部分给操作系统和JVM自身开销。我看到有人把SPARK_WORKER_MEMORY直接设成和物理内存一样大,结果OS直接OOM,这是初学者很容易踩的坑。
5.2 spark-defaults.conf:作业级参数的默认值
同样先复制模板:
cp $SPARK_HOME/conf/spark-defaults.conf.template $SPARK_HOME/conf/spark-defaults.conf这个文件控制的不是集群本身,而是每个Spark作业提交时的默认参数。推荐的开发配置:
spark.master spark://spark-master:7077 spark.driver.memory 2g spark.executor.memory 2g spark.executor.cores 2 spark.serializer org.apache.spark.serializer.KryoSerializer spark.sql.shuffle.partitions 200 spark.ui.port 4040重点解释两个:
spark.serializer: Spark默认使用Java序列化,但性能和压缩率不如Kryo。开发阶段可以不换,但生产环境强烈建议换成Kryo。不过Kryo需要注册类,如果你用了自定义类,要在代码里做配置,否则反而可能报序列化错误。spark.sql.shuffle.partitions: 默认值是200,这个参数和Shuffle时的分区数有关。如果你的数据量不大,200个分区会产生大量空任务,白白浪费资源。在开发调试时,可以调小到20~50,让任务跑快一点。
5.3 workers文件与日志级别
在Standalone集群中,Master靠conf/workers文件来发现Worker节点。默认情况下这个文件叫workers.template,打开后把需要作为Worker的机器主机名或IP填进去,一行一个:
spark-master spark-worker-1 spark-worker-2如果你只有一台机器做伪分布式,就填本机主机名。这里要注意:Master是靠SSH去连接并启动各个Worker进程的,所以第3.3节做的免密配置在这里生效。
日志级别也值得调一下。默认Spark日志非常啰嗦,启动时INFO刷屏,看久了很混乱。编辑conf/log4j2.properties(没有就复制模板),把根日志级别从INFO改成WARN:
rootLogger.level = WARN这样运行任务时只会输出WARN和ERROR级别的日志,干净很多。排查问题时再临时改回INFO也不迟。
6. 本地模式跑通,再升级到Standalone集群模式
配置文件的修改逻辑清楚后,下面进入实战。我建议的顺序是:先跑本地模式确认环境没问题,再启动Standalone集群。这样每次引入新变量时,都能准确定位问题出在哪一层。
6.1 本地模式验证:先解决“能不能跑”
直接执行:
spark-shell --master local[*]local[*]表示使用本机所有可用的CPU核数来运行。如果配置正常,你会看到Spark的logo和scala>提示符。
在Scala shell里跑一个简单验证:
val data = 1 to 100 val result = sc.parallelize(data).filter(_ % 2 == 0).count() println(s"偶数个数: $result")如果输出偶数个数: 50,说明Spark核心环境已经正常工作。
如果你想验证PySpark,先确认之前设置的PYSPARK_PYTHON变量生效了,然后执行:
pyspark --master local[*]在Python里跑:
rdd = sc.parallelize(range(1, 101)) result = rdd.filter(lambda x: x % 2 == 0).count() print(f"偶数个数: {result}")输出同样是50。到这里,本地模式就没问题了,代码能跑,说明依赖完整、配置正确。
6.2 Standalone集群启动流程
本地模式OK后,我们把模式升级到Standalone。先启动Master:
start-master.sh启动后,访问http://localhost:8080,你应该能看到Master的Web UI,里面会显示Master的地址,默认为spark://spark-master:7077。
然后启动Worker:
start-worker.sh spark://spark-master:7077也可以一次性启动所有配置在workers文件里的节点:
start-workers.sh打开Master的Web UI,如果Worker状态显示ALIVE,说明集群节点注册成功。
再跑一个官方示例验证集群模式:
spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://spark-master:7077 \ $SPARK_HOME/examples/jars/spark-examples_2.12-3.5.1.jar \ 10这个程序会计算圆周率,最后一个参数10表示迭代次数。正常运行时,你会看到Application出现在Web UI的应用列表里,日志最后会输出一个非常接近3.14的数值。
如果这一步能通过,你的Spark开发环境就已经完全就绪了,无论是写Scala作业还是PySpark作业,都可以在这个基础上进行开发调试。
7. 踩坑实录:最典型的三类问题排查链路
这一节我想认真写一下我实际踩过、也帮别人排查过的三类高频问题。直接给答案没有意义,重要的是把排查思路走一遍,下次遇到类似问题你知道往哪里看。
7.1 JAVA_HOME或主机名解析导致的启动失败
错误特征:执行start-master.sh后Master进程秒退,没有明显报错;或者spark-shell启动时卡住不动,一段时间后报连接拒绝。
排查思路:
# 先看前台日志 start-master.sh 2>&1 | head -50 # 再看Spark日志目录 tail -100 $SPARK_LOG_DIR/spark-$(whoami)-org.apache.spark.deploy.master.Master-1-*.out通常会发现日志里有JAVA_HOME is not set或Could not resolve hostname之类的信息。前者说明spark-env.sh里的JAVA_HOME没配好;后者说明SPARK_MASTER_HOST指定的主机名在/etc/hosts里没有解析条目。
修复步骤:
- 确认
java -version能正常输出。 - 在
spark-env.sh里显式设置export JAVA_HOME=/opt/jdk(如果用的是软链接,用readlink -f $(which java)追踪到真实路径)。 - 确保
/etc/hosts里有本机IP和主机名的映射,然后用hostname -f确认完整主机名是否被解析。
7.2 内存分配不均导致的Worker启动失败
错误特征:start-workers.sh执行后,Master UI里看不到Worker,或者Worker显示DEAD。
排查思路:查看Worker日志:
tail -100 $SPARK_LOG_DIR/spark-$(whoami)-org.apache.spark.deploy.worker.Worker-1-*.out如果日志里有Not enough memory to launch worker,说明SPARK_WORKER_MEMORY设得比物理可用内存还大,或者系统本身可用内存不足。用free -h查看剩余内存。
修复步骤:把spark-env.sh里的SPARK_WORKER_MEMORY调小。经验值是留出物理内存的20%~30%给OS和JVM本身,比如16GB物理内存的机器,设10g比较稳。
另外还有一个隐藏坑:如果Master和Worker在同一个节点上跑,Master本身会占用一定内存,Worker内存设置过大会导致二者内存竞争。这种情况优先降低Worker内存。
7.3 版本错配导致ClassNotFoundException或依赖冲突
错误特征:spark-submit运行作业时报一堆NoClassDefFoundError或java.lang.NoSuchMethodError,且这些类看起来和Hadoop或Netty相关。
排查思路:这类问题几乎都是版本错配引起的。最常见的情况是:你下载的Spark包是Hadoop 3.3的预编译版,但环境变量里手动指定的Hadoop客户端是其他版本;或者你本地有多个Spark版本,SPARK_HOME指向了旧版。
修复步骤:
- 确认
SPARK_HOME指向的确实是你要用的Spark版本:echo $SPARK_HOME。 - 检查
spark-submit --version输出的Hadoop版本标签,看它和预编译包的后缀是否一致。 - 如果你为了读写HDFS额外加了Hadoop相关依赖,确保版本和Spark预编译版匹配。最简单的做法是先不加这些依赖,跑通内置示例后,再逐步引入外部依赖。
提示:遇到依赖问题时,先查看
$SPARK_HOME/jars下的jar包版本,再排查自己引入的外部依赖,不要盲目升级某个jar到最新版。Spark对Hadoop和Netty的版本是敏感的,强行升级反而容易引发新的兼容问题。
8. 环境跑通之后,我建议你马上做的几件事
搭建到这一步,你已经有一个可用的Spark开发环境了。但在正式投入开发之前,还有几个配置值得花点时间补齐,它们会让你的日常开发顺畅很多。
8.1 配置Spark History Server
默认情况下,Spark应用运行结束后再打开Web UI是看不到历史记录的。开发调试时,想回头看某个作业的日志和统计信息,就会发现界面已经404了。配置History Server可以解决这个问题。
在spark-defaults.conf里加一行,并建好日志目录:
spark.eventLog.enabled true spark.eventLog.dir file:///data/spark/logs/eventmkdir -p /data/spark/logs/event然后启动History Server:
start-history-server.sh以后跑的每个作业都会持久化Event Log,在http://localhost:18080上就能查历史作业。这个配置对开发调试特别有用,跑完的作业可以随时复盘,不用重新执行。
8.2 确认PySpark的Python版本与依赖隔离
PySpark开发时,一个很常见的问题是:本地pip install pyspark装了一个版本,SPARK_HOME/python里自带的又是另一个版本,运行时不知道被谁覆盖。我的做法是:不额外pip安装pyspark,直接用Spark发行包内置的python目录,这样能保证版本和JVM端的Spark完全一致。
做法很简单,在.bashrc里把$SPARK_HOME/python加到PYTHONPATH:
export PYTHONPATH=$SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-*-src.zip:$PYTHONPATHsource ~/.bashrc然后运行pyspark,它就会使用Spark自带的Python包,避免双版本混乱。
8.3 同一套配置用在多台机器上的注意点
如果后续打算扩展到多节点集群,有两点经验值得分享:
spark-env.sh可以在多台机器上保持相同内容,因为JAVA_HOME、PYSPARK_PYTHON这些路径只要保持一致就行。workers文件只在Master节点上生效,Worker节点不需要维护这个文件。所以实际部署时,通常先把Master节点的整个/opt/spark目录同步到各Worker节点,再在Master上配置workers文件指定Worker主机名,免密做好,就能一键拉起整个集群。
我自己在实际部署时,习惯用rsync把Spark安装目录和配置文件同步到所有节点:
rsync -avz /opt/spark/ spark-worker-1:/opt/spark/同步完再检查各节点的JAVA_HOME和Python版本是否一致。这一步如果搞不定,后面任务量一大,各种奇奇怪怪的报错就会冒出来。
最后再说一个个人体会:Spark环境搭建这件事,本质上是版本管理、资源规划和配置理解三项能力的组合。版本不出错,环境就成功了一大半;资源规划不贪心,Worker和Executor就不会反复OOM;配置不盲抄,遇到问题才不会被“网上说这样写”带偏。这套环境我目前在多个项目里用了两年多,从单机开发到三节点集群都比较稳定。你按照这个流程搭完,至少第一阶段的学习和开发是完全够用的。