今天想和大家聊的是Spark集群部署中从单机到分布式搭建的完整路径。很多朋友一开始都是在单机模式下写Spark代码,跑通WordCount就以为万事大吉,结果一上分布式就各种连不上、起不来、OOM。这篇文章我把从Local到伪分布式再到真正分布式集群的过程完整捋一遍,包括版本选型、配置步骤、启动验证、调优思路和踩坑记录。适合刚接触Spark、想自己搭一套实验环境,或者准备在生产环境落地Spark集群的人参考。
1. 先搞清楚:单机、伪分布式与真正分布式
1.1 三个阶段的本质区别
很多人把“单机模式”和“分布式模式”的差异理解成“一台机器跑和几台机器跑”,这个理解太粗了。Spark的单机模式(Local模式)严格来说是运行在当前JVM里的一个线程或线程池,Driver、Executor都在一个进程里,数据也是本地的,它解决的是“代码逻辑能不能跑通”的问题,完全不涉及网络通信、资源调度和任务分发。
伪分布式解决的是“环境结构”的问题。单台机器上启动多个JVM进程,每个进程扮演不同角色,比如HDFS的NameNode、DataNode,YARN的ResourceManager、NodeManager,Spark的Master和Worker,全部塞在一台物理机上。它看起来像分布式,但本质上还是在模拟,目的是让你把配置逻辑、启动逻辑、进程管理逻辑都过一遍,以后切换到多台机器时不会手忙脚乱。
真正的分布式才是生产环境里的样子。多台物理机或虚拟机,各自跑独立进程,通过网络通信完成数据交换和任务调度。这里就要注意,进程一分散,主机名解析、防火墙、端口连通性、网络带宽、磁盘空间这些问题全部冒出来,很多单机跑得好好的作业,一分布式就翻车,原因不一定是代码,而是部署环境没对。
用生活化的类比就是:单机模式是“自己在家里做一道菜”;伪分布式是“一个人同时扮演厨师、洗菜工、服务员,在一个厨房里模拟完整流程”;真分布式是“后厨、前台、传菜口分在不同地方,靠对讲机配合”。很多人在模拟流程时觉得没难度,但真正开餐厅之后才发现,对讲机频道没对上,菜就全卡在路上了。
1.2 部署前需要想清楚的事
动手之前,先问自己三个问题:这套集群是给谁用的,数据量有多大,能接受的停机容忍度是多少。
如果只是学习和跑Demo,单机资源够用就好,不需要纠结高可用;如果是给部门做数据分析,至少要有一套3节点起步的小集群,并预留扩展空间;如果是核心生产链路,那还要考虑Spark Master的HA、HDFS的HA、YARN的ResourceManager HA,以及监控告警。
接下来是组件范围。很多人以为“搭Spark集群”就是下载一个Spark装到几台机器上,但真实场景里Spark通常要和Hadoop、YARN、Hive、HDFS配合使用。你至少要先想清楚存储用什么、调度用什么。最轻量的是Spark Standalone模式,只依赖Spark自带的Master/Worker;最主流的是Spark On YARN,由YARN统一管理CPU和内存资源,HDFS负责存储。学习阶段建议先把Standalone跑通,再过渡到YARN,不要一上来就追求全组件。
还有一件事容易被忽略:机器资源规划。我曾经见过一台2核4GB的云主机硬要跑伪分布式,结果HDFS、YARN、Spark三个进程把内存吃满,系统直接卡死。所以部署前要盘点内存和磁盘,至少给系统预留30%的资源,别把机器塞满。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本选型
2.1 硬件与操作系统基础
先说硬件。如果你只是搭建单机或伪分布式做学习,8GB内存、4核CPU、40GB磁盘就够了。注意,这里的8GB内存是给Spark用的,系统本身还要吃2GB左右,所以机器最好能到10GB或12GB。如果想要跑一个三节点的小集群,每台节点建议16GB内存起步、4核以上、100GB磁盘,网络要千兆交换机。
操作系统方面,生产环境常见的组合是CentOS 7.9、Rocky Linux 8、Ubuntu 20.04/22.04。CentOS 7虽然已停止维护,但很多企业内部还在用,配置文档也最多;新部署建议Rocky Linux或Ubuntu LTS版本。不管用哪个系统,都要注意关闭SELinux或设置成Permissive模式,否则容易踩到权限坑。
bash复制# CentOS/RHEL系查看SELinux状态
getenforce
# 临时关闭
setenforce 0
# 永久关闭,修改 /etc/selinux/config
SELINUX=disabled
2.2 软件版本组合怎么选
版本选型是新手最容易翻车的地方。Spark的发行包有不同构建版本,比如Spark 3.3.2-bin-hadoop3和Spark 3.3.2-bin-without-hadoop,前者已经内置了Hadoop客户端库,后者需要你自己准备SPARK_DIST_CLASSPATH,新手用前者省事得多。
我自己在实验环境里比较喜欢用Spark 3.3.2或3.5.x搭配Hadoop 3.3.x,再配JDK 8。原因有三个:第一,3.x系列同时支持Standalone和YARN,文档多、踩坑少;第二,JDK 8对Scala 2.12的兼容性最稳;第三,Hadoop 3.3.x已经是非常成熟的版本,很多企业都在用。如果你是做AI或GPU相关的新功能探索,可以选Spark 4.x预览版,但生产环境别追新。
版本组合可以参考这个表格:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8u202或11 | 8最稳,11也兼容,不要用17以下无法跑旧Scala |
| Scala | 2.12 | 与Spark官方构建对应,除非自己源码编译 |
| Spark | 3.3.2 / 3.5.1 | 3.3.x最经典,3.5.x功能更新 |
| Hadoop | 3.3.4 / 3.3.6 | 与Spark内置hadoop3版本匹配即可 |
| Python | 3.8+ | 使用PySpark时要求 |
打开Spark官方下载页面时,建议下载spark-3.3.2-bin-hadoop3这种打包好的tgz,不要选源码包,不然还要自己编译。
2.3 网络、SSH与主机规划
分布式部署最基础的配置是主机名和IP映射。我见过太多案例,Worker起来了,但在Master的Web UI里看不到,最后发现是/etc/hosts里主机名没配或配了IPv6地址,导致Spark进程之间无法通过主机名连接。
规划一个三节点集群,可以这样设定:
| 主机名 | IP | 角色 |
|---|---|---|
| node01 | 192.168.10.11 | Master, Worker |
| node02 | 192.168.10.12 | Worker |
| node03 | 192.168.10.13 | Worker |
然后把每个节点的/etc/hosts都写上这三条映射。注意,如果你的环境启用了IPv6,Spark Web UI有默认绑定IPv6地址的情况,建议在spark-env.sh里强制设置SPARK_LOCAL_IP为IPv4地址,避免节点间无法访问。
bash复制cat >> /etc/hosts <<EOF
192.168.10.11 node01
192.168.10.12 node02
192.168.10.13 node03
EOF
节点之间还需要配置SSH免密登录,尤其是Master需要免密到所有节点启动Worker进程。配置完成后用ssh node02 hostname验证,如果还要输入密码就是没配好。
3. 从单机模式开始:Spark Local 模式部署
3.1 下载解压与目录规划
即使目标是分布式,我也建议先在本机把Spark跑起来。单机模式能验证安装是否正确,也能让你先熟悉Spark Shell和代码开发,省得到最后分布式的坑和代码的坑堆在一起。
下载地址推荐Apache官方镜像或Archive目录,避免网盘链接失效。这里以Spark 3.3.2为例:
bash复制wget https://archive.apache.org/dist/spark/spark-3.3.2/spark-3.3.2-bin-hadoop3.tgz
tar -zxvf spark-3.3.2-bin-hadoop3.tgz
sudo mv spark-3.3.2-bin-hadoop3 /opt/spark
服务安装时我习惯把版本号放在目录名里,再用软链接指到固定路径,这样以后升级版本不用改一堆脚本。比如:
bash复制sudo ln -s /opt/spark-3.3.2-bin-hadoop3 /opt/spark
3.2 环境变量与spark-env.sh配置,这步错了后面全乱
单机模式虽然不要求复杂的配置,但环境变量必须设置好,否则后面所有模式都会出问题。编辑/etc/profile或~/.bashrc,添加:
bash复制export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export HADOOP_HOME=/opt/hadoop
export SPARK_HOME=/opt/spark
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$SPARK_HOME/bin:$SPARK_HOME/sbin
保存后执行source使其生效,然后确认Spark版本:
bash复制spark-shell --version
如果你使用的是without-hadoop版本,还需要在$SPARK_HOME/conf/spark-env.sh里加入:
bash复制export SPARK_DIST_CLASSPATH=$(hadoop classpath)
新手常常只配了JAVA_HOME忘了配HADOOP_CONF_DIR,结果提交Spark到YARN时报错找不到HADOOP_CONF_DIR。这一步宁可提前写清楚。
3.3 本地模式跑第一个数据分析案例
配置完后,可以用PySpark跑一个简单的WordCount,验证整个链路。先准备一个数据文件:
bash复制echo "hello spark hello hadoop" > /tmp/data.txt
echo "spark is fast" >> /tmp/data.txt
然后启动pyspark:
python复制from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("LocalWordCount") \
.master("local[*]") \
.getOrCreate()
rdd = spark.sparkContext.textFile("file:///tmp/data.txt")
counts = rdd.flatMap(lambda line: line.split(" ")) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda a, b: a + b)
counts.foreach(print)
这里local[*]表示用当前机器所有CPU核心运行,如果内存不够,也可以写成local[2],只用2个核。跑完之后你会看到Spark Shell里弹出一堆日志,任务能正常结束就说明安装没问题。
4. 伪分布式部署:先在本机模拟集群
4.1 单机跑Hadoop DFS与YARN
要过渡到分布式,绕不开Hadoop和YARN。伪分布式部署最好的一点是,配置思路和真分布式一样,但只需要一台机器。你可以先把Hadoop下载下来,解压到/opt/hadoop,然后配置core-site.xml、hdfs-site.xml、yarn-site.xml。
core-site.xml最关键的是fs.defaultFS,需要指定HDFS的NameNode地址:
xml复制<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://node01:9000</value>
</property>
</configuration>
hdfs-site.xml里设置副本数为1,因为伪分布式只有一台DataNode,副本数为3会一直显示副本不足:
xml复制<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>/data/hadoop/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>/data/hadoop/data</value>
</property>
</configuration>
配置完成后,首次需要格式化NameNode:
bash复制hdfs namenode -format
然后启动HDFS和YARN:
bash复制start-dfs.sh
start-yarn.sh
jps
正常能看到NameNode、DataNode、ResourceManager、NodeManager这几个进程。如果DataNode起不来,最常见的原因是数据目录和元数据目录默认指向/tmp/hadoop-*,重启后文件被系统清理了。
4.2 Spark On YARN的配置
Hadoop能正常启动后,Spark要提交到YARN,还需要让Spark知道Hadoop的配置位置。编辑$SPARK_HOME/conf/spark-env.sh,添加:
bash复制export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export HADOOP_HOME=/opt/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export SPARK_DIST_CLASSPATH=$(hadoop classpath)
然后提交一个测试任务:
bash复制spark-submit \
--master yarn \
--deploy-mode client \
--num-executors 2 \
--executor-cores 1 \
--executor-memory 2g \
examples/src/main/python/wordcount.py hdfs://node01:9000/data.txt
这里--master yarn指定用YARN作为资源调度器,--deploy-mode client表示Driver在本地运行,方便看日志。如果日志里出现ApplicationMaster启动成功,就说明Spark On YARN已经通了。
4.3 伪分布式常见的坑
伪分布式最容易踩的坑无非这么几个:第一是NameNode格式化后,DataNode的存储目录里如果已经有旧数据,会报Incompatible clusterIDs,解决办法是清掉数据目录和日志目录再重新格式化;第二是YARN启动后集群节点显示为Active但对内存不满意,默认YARN最少申请内存是1GB,如果机器内存小,需要在yarn-site.xml里调小yarn.scheduler.minimum-allocation-mb和yarn.nodemanager.resource.memory-mb;第三是Spark任务提交时报Unrecognized Hadoop major version number,通常是Spark和Hadoop版本不匹配,建议统一用官方构建的hadoop3版本包。
说实话,伪分布式阶段如果能完整跑一遍“HDFS存数据,YARN调度资源,Spark做计算”,后面切真分布式只是把配置里的IP从127.0.0.1换成分散到多台机器而已。
5. 真正的分布式集群搭建
5.1 主机角色规划:Master/Worker、Driver/Executor
到这一步,你已经理解了本机模拟,接下来就是把进程铺到多台机器上。先明确概念:Spark集群层面有Master和Worker,Master负责资源管理和任务分配,Worker负责启动Executor并汇报资源;Spark应用层面有Driver和Executor,Driver负责应用的main函数和任务调度,Executor负责真正执行计算任务。
很多人会把Master和Driver搞混。其实Master是集群的常驻进程,Driver是某个Spark应用启动时才出现的进程。一个Master可以服务很多应用,但每个应用只有一个Driver。规划角色时,三节点集群最简单的分配方式是node01同时做Master和Worker,node02和node03只做Worker,这样每个节点都能跑计算任务。
| 节点 | Spark角色 | 说明 |
|---|---|---|
| node01 | Master + Worker | 管理节点,同时可以承担计算 |
| node02 | Worker | 计算节点 |
| node03 | Worker | 计算节点 |
生产环境如果想做高可用,可以部署两个Master,通过ZooKeeper做Leader选举,但学习阶段先不用管。
5.2 Spark Standalone集群配置步骤
先在node01上配置$SPARK_HOME/conf/workers文件。注意Spark 3.2以前叫slaves,3.2之后改成了workers,很多老教程还在写slaves,会导致新版本无法识别。这里加入所有Worker主机名:
bash复制node01
node02
node03
然后编辑spark-env.sh:
bash复制export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export SPARK_MASTER_HOST=node01
export SPARK_MASTER_PORT=7077
export SPARK_WORKER_CORES=4
export SPARK_WORKER_MEMORY=12g
SPARK_MASTER_PORT默认就是7077,如果没改可以不写。SPARK_WORKER_CORES和SPARK_WORKER_MEMORY建议显式设置,否则Worker会根据机器实际资源自动推断,但推断的值不一定符合你的预期。
配置好之后,需要把Spark安装目录分发到其他节点。最省事的办法是用scp:
bash复制scp -r /opt/spark node02:/opt/
scp -r /opt/spark node03:/opt/
同时记住,其他节点也要配置好JAVA_HOME和SPARK_HOME环境变量,否则Worker启动时找不到Java。都准备好后,在node01上执行启动脚本:
bash复制$SPARK_HOME/sbin/start-all.sh
启动后访问http://node01:8080,能看到Master的Web UI,以及所有Worker的状态。如果某个Worker状态为DEAD,第一件事就是去该节点看日志,日志在$SPARK_HOME/logs/目录下。
5.3 生产环境为什么要优先考虑Spark On YARN
Standalone模式最简单,但生产环境里我更推荐Spark On YARN。原因是YARN作为统一资源调度器,可以同时调度Spark、MapReduce、Flink等多种计算引擎,避免Spark集群和别的计算框架各占资源、互相抢内存。
YARN模式下,Spark不再需要独立的Master进程,而是把应用的ApplicationMaster提交到YARN上,由ResourceManager分配Container来运行。整个集群里只需要部署Hadoop的YARN组件和Spark客户端,Spark Shell或spark-submit通过YARN配置找到ResourceManager,就可以提交任务。
如果你在伪分布式阶段已经配好了Hadoop和YARN,切到多节点时主要做两件事:把Hadoop集群从单机扩展成多节点,保证yarn-site.xml里ResourceManager指向正确的节点;在Spark客户端的spark-env.sh里设置HADOOP_CONF_DIR。提交时--master yarn,即可。
5.4 启动、验证与Web UI监控
Standalone集群启动后,可以提交一个官方示例验证:
bash复制spark-submit \
--master spark://node01:7077 \
--deploy-mode client \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.3.2.jar \
10
如果能算出Pi值,说明集群基础链路正常。然后去http://node01:8080查看Worker状态,提交任务后再去http://node01:4040查看Spark应用UI。注意4040是Driver进程起的端口,如果Driver部署在node02上,就要访问node02:4040,这点很容易搞混。
生产环境里从YARN的Web UI跳转Spark应用UI时,经常需要配置Web Proxy或修改yarn.resourcemanager.proxy.address,否则页面跳不过去。这个问题不算难,但新人排查时容易卡住。
6. 分布式集群调优与资源管理
6.1 Executor内存与Core配比怎么算
集群搭起来后,最核心的问题就是资源怎么分配给Spark作业。很多人以为--executor-memory越大越好,其实不是。要给操作系统和YARN的NodeManager留余量,否则节点直接OOM。
假设每台Worker有16GB内存、4核CPU,用户可用内存按70%算,大约11GB,核数4核。如果每个Executor分配2核、4GB内存,那同一节点最多2个Executor,内存刚好够。但如果每个Executor配4核、8GB内存,系统只剩3GB,很容易把节点拖死。
我自己一般按这个公式估算:每个Executor的内存设为4GB~8GB,Executor核数设为2~4,单节点Executor数量 = 可用核心数 / Executor核数。然后还要留20%左右内存给系统和其他进程。比如32GB内存的节点,executor-memory最多给6GB,executor-cores给3,这样一台节点能跑3个Executor,加起来18GB,剩下的留给OS。
6.2 Spark OOM排查思路
Spark OOM是分布式部署后遇到最多的问题,也是最考验排查思路的问题。Executor OOM一般分为堆内OOM和堆外OOM。堆内OOM通常是数据量超过Executor内存,常见场景是单分区数据量太大、groupByKey后单key数据涨了几倍、或者广播变量没调优;堆外OOM则多是网络缓冲、序列化缓存、读写HDFS或RPC开销过大导致。
排查时先看Web UI上的Stages页,找到失败Stage的输入数据量和Shuffle读写数据量。如果某个Stage输入只有几百MB,但执行器挂了,大概率不是单纯内存不够,而是数据倾斜。如果某个Stage输入几个GB,Executor内存只有2GB,那就要调整资源或加宽重分区。
另外,spark.memory.offHeap.enabled和spark.memory.offHeap.size只在显式启用时才有效,不要以为是开了executor-memory就有堆外内存。很多人写代码时用了spark.conf.set("spark.memory.offHeap.enabled", "true"),但没配置size,等于没开。
6.3 数据倾斜与动态资源分配
数据倾斜是个经典问题。一个最直接的判断方法是看Web UI里的Task耗时分布:少数Task处理的数据量是其他Task的几十倍,那你就要怀疑分区不均衡了。常用解决办法有几种:对Key加随机前缀,把倾斜Key拆成多个子Key之后再做聚合;小表用广播变量替代Join;调整spark.sql.shuffle.partitions让Shuffle分区数量匹配数据量,不要默认200个分区一动不动。
动态资源分配是Spark里很适合中小集群的功能,开启后Spark会根据任务负载动态增减Executor,避免高峰期资源不够、低谷期资源闲置。在Standalone模式下开启需要spark.shuffle.service.enabled=true并部署外部Shuffle服务;在YARN模式下同样需要开启spark.dynamicAllocation.enabled=true,并配置最小、最大Executor数:
bash复制spark.dynamicAllocation.enabled true
spark.dynamicAllocation.initialExecutors 2
spark.dynamicAllocation.minExecutors 2
spark.dynamicAllocation.maxExecutors 20
动态分配虽然好用,但不要在小集群上无脑调大maxExecutors,否则多个任务同时提交时,很可能瞬间把资源占满,其他任务排队排到怀疑人生。
7. 常见问题与排查技巧实录
7.1 网络与端口问题
分布式部署后,最常见的错误之一是Worker无法连接Master,或者应用提交后一直等待。第一嫌疑就是端口不通。Spark常用端口有这几个:
| 用途 | 端口 |
|---|---|
| Master Web UI | 8080 |
| Master注册端口 | 7077 |
| Application Web UI | 4040 |
| YARN ResourceManager Web UI | 8088 |
| YARN NodeManager Web UI | 8042 |
如果Worker日志里出现Connection refused,先检查防火墙是否放通了这些端口,再检查Master所在节点的hostname -i是不是返回了奇怪的IP。很多Linux机器默认返回的可能不是你想用的IP,这时可以在spark-env.sh里强制指定:
bash复制export SPARK_LOCAL_IP=192.168.10.11
7.2 环境变量与配置不生效问题
第二个容易被忽视的问题是环境变量。我从接手过的集群里看到过不少机器,/etc/profile里设了JAVA_HOME,但通过SSH执行脚本时没加载,导致启动Worker报找不到Java。解决办法是在spark-env.sh开头显式写入Java路径,而不是只依赖全局环境变量。
另外,改配置后一定要重启对应的Spark进程。很多人在spark-defaults.conf里改了参数,但直接提交任务时发现没生效,就是因为没区分参数的作用范围。有些参数确实不需要重启集群,但像SPARK_WORKER_MEMORY这种Worker级参数,必须重启Worker才会重新分配。
7.3 部署后的日常运维与我的个人体会
集群搭好之后,日常运维比搭建更考验耐心。我建议每个星期至少登录一次Web UI,看Worker的状态、已使用内存和磁盘剩余空间。日志目录会不断增长,特别是Spark EventLog和Worker日志,要定期清理或配置日志滚动。spark.eventLog.enabled打开后,默认写本地目录,生产环境最好配置到HDFS或S3,避免单点磁盘写满。
最后再分享一个小技巧,每次改完配置或者升级版本,先在node01执行一次start-all.sh前先执行stop-all.sh,不要嫌麻烦。Spark进程不像传统服务那样改完配置就热加载,新旧Worker混跑会导致资源分配混乱,排查成本远高于重启成本。
我在实际部署里最多的体会就是,Spark集群能不能跑好,七成在配置,三成在代码。配置又是环环相扣的,从hosts到环境变量到资源配置,每一环都值得认真对待。希望这篇文章能把从单机到分布式的路给你趟平。
