做大数据的人肯定都绕不过这一关:把一个Spark集群手工搭起来。现在很多托管组件能一键拉起环境,但自己从头搭一次Spark 3.2 Standalone分布式集群,会让你对Master、Worker、Driver、Executor这些角色建立非常直观的认识,后面哪怕切到YARN或者K8s部署,也能快速迁移思路,排查问题时更清楚问题出在哪一层。这篇我以三台CentOS 7虚拟机为例,不依赖任何外部调度框架,把Standalone模式下完整的搭建过程、配置参数和排错方法走一遍。适合刚接触Spark的朋友,以及那些已经在用Local模式跑过代码、但对集群启动原理没把握的同学。
1. 环境准备与整体设计思路
先明确一点:Spark本身只是一个计算框架,它不负责存储数据,也不强制要求你必须有Hadoop集群。它运行任务时需要一个资源调度入口,Standalone模式就是Spark自己内置的一套调度机制,包含一个Master进程和若干个Worker进程。Master负责接收任务请求、分配资源,Worker负责注册到Master并在自己的机器上启动Executor来干活。这有点像公司里的项目经理给组员派活:Master就是那个派活的人,Worker就是干活的员工,每个任务里真正执行计算的是Executor。
1.1 机器规划与角色分配
我准备了三台机器,全部装CentOS 7.9,每台配置是2核4G内存,这个配置对学习环境来说完全够用。如果你只有两台甚至一台机器,也可以搭建,但三台最能体现“分布式”的感觉,因为能明显看到不同Worker节点上的Executor进程和资源分配情况。
| 主机名 | 角色 | 内网IP | 建议配置 |
|---|---|---|---|
| master | Master + Worker(可选) | 192.168.10.11 | 2C4G |
| worker1 | Worker | 192.168.10.12 | 2C4G |
| worker2 | Worker | 192.168.10.13 | 2C4G |
这里有个关键决定:Master节点要不要同时当Worker用。学习环境强烈建议把master也加进workers文件里,这样三台机器都能跑执行任务,资源利用率高。生产环境一般不建议Master掺和跑任务,因为Master承担调度职责,资源被计算任务抢占了会影响整个集群的稳定性。下面的操作我会把master也作为Worker纳入集群。
1.2 为什么选Standalone而不是YARN或Local
很多人会问:现在搞Spark部署,是不是都应该用YARN?其实取决于你的使用场景。如果公司已经有现成的Hadoop集群,那Spark on YARN是主流选择,因为YARN能统一管理多个计算框架的资源。但如果你是从零开始搭一套独立的大数据环境,或者只需要一个轻量级的Spark提交入口,Standalone模式反而是最省事的选择:不需要额外安装任何资源管理组件,Spark安装包里自带Master和Worker,配置好直接就能用。
Local模式则是另一种极端,它只在本地机器上跑多线程,不涉及分布式,适合代码调试和功能验证。一旦你写好的离线任务要放在多台机器上跑,或者要做数据量较大的批处理,就必须进入Standalone或YARN模式。这一篇的核心目标就是让你通过手动配置理解分布式环境下资源的使用机制,后面再切换调度器时,你会知道哪些参数是需要迁移的,哪些是Standalone独有的。
1.3 版本选型与前置依赖清单
我这里选择的是Spark 3.2.x和CentOS 7这个组合。Spark 3.2是3系列里比较稳定的一个版本,对JDK 8和JDK 11都支持得很好,CentOS 7又是生产环境里大量存在的操作系统,所以这个组合的参考价值很高。
整个搭建过程需要准备的东西一共有四块:
- JDK:Spark是JVM应用,必须要有Java环境,建议用OpenJDK 1.8,版本不低于8u系列的常用发布版
- Spark二进制包:下载预编译版本即可,文件名一般是spark-3.2.x-bin-hadoop3.2.tgz,这个包里面已经包含了Scala运行库,不需要单独安装Scala
- SSH免密登录:Spark启动脚本需要通过SSH远程到每个Worker节点执行启动命令,如果不做免密,每次启动都要输密码,很痛苦
- 主机名与网络配置:三台机器要能互相ping通,/etc/hosts里要写清楚主机名和IP的映射关系
2. 前置依赖配置:JDK与SSH互信
前置依赖不处理好,后面启动集群时会碰到各种莫名其妙的问题。尤其是很多人明明安装好了JDK,Spark却一直报找不到JAVA_HOME,多半是环境变量没有写对位置,或者配置没有被source加载。
2.1 JDK安装与JAVA_HOME配置
在CentOS 7下安装JDK,最省事的方式是用yum安装OpenJDK:
yum install -y java-1.8.0-openjdk-devel安装完成后,用下面命令验证一下:
java -version which java readlink -f $(which java)很多教程省略了which和readlink这两步,但实际配置环境变量时你必须要知道JDK的真实安装路径。yum安装的OpenJDK默认会放在/usr/lib/jvm/下面,路径名可能带版本号。我建议使用/etc/profile.d/java.sh这种方式配置,而不是直接改/etc/profile,因为后者文件内容多、容易改坏,而且未来要维护多个版本时也不方便。
cat > /etc/profile.d/java.sh <<'EOF' export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk export PATH=$PATH:$JAVA_HOME/bin EOF source /etc/profile.d/java.sh这里有个容易被忽略的细节:JAVA_HOME必须精确到JDK的根目录,而不是bin目录。Spark的sbin/spark-daemon.sh脚本会调用${JAVA_HOME}/bin/java,如果你的变量配置错了,启动Master或Worker时就会直接报“JAVA_HOME is not set”相关错误。三台机器上都要做同样的操作。
2.2 SSH免密登录配置及排查思路
Spark启动集群时,你在master上执行start-workers.sh脚本,它会循环读取workers文件,对每个Worker节点通过SSH发起远程命令。如果每次都要交互式输入密码,自动化脚本根本没法跑,所以要配置master到所有节点的SSH免密登录,包括到自己的免密。
先检查一下本机是否已有密钥:
ls ~/.ssh/如果里面没有id_rsa和id_rsa.pub文件,就生成一对:
ssh-keygen -t rsa -b 2048 -P '' -f ~/.ssh/id_rsa-P ''表示空密码,-f指定生成路径。然后把公钥分发到目标机器的authorized_keys中:
ssh-copy-id -i ~/.ssh/id_rsa.pub master ssh-copy-id -i ~/.ssh/id_rsa.pub worker1 ssh-copy-id -i ~/.ssh/id_rsa.pub worker2第一次执行时SSH会询问你是否确认主机指纹,输入yes回车即可。配置完成后,在各个节点上互为测试一下连接,比如在master上执行:
ssh master hostname ssh worker1 hostname ssh worker2 hostname这里有个很经典的坑:authorized_keys文件和.ssh目录的权限如果不正确,即使公钥内容没写错,SSH也会拒绝使用这个公钥。通常要求.ssh目录权限是700,authorized_keys文件权限是600。如果遇到登录时还是提示要密码,就检查一下权限,然后看服务端日志,CentOS 7下可以查看/var/log/secure,如果看到Authentication refused相关记录,大概率就是权限问题。
3. 下载Spark安装包与核心配置解析
前置依赖搞定后,接下来是重头戏:下载Spark包并修改配置文件。这个阶段很多人喜欢照抄网上的配置,但完全不理解每个参数的含义,导致出了问题不知道怎么调整。我会逐个参数进行说明,并把参数的计算思路一并讲清楚。
3.1 下载Spark二进制包与目录规划
下载地址选择Apache官网镜像即可,找到spark-3.2.x-bin-hadoop3.2.tgz这个文件。下载完成后传到master节点,一般放在/opt目录:
cd /opt tar -zxvf spark-3.2.x-bin-hadoop3.2.tgz ln -s /opt/spark-3.2.x-bin-hadoop3.2 /opt/spark这里建议做一个软链接。这样做的好处是,以后如果要升级Spark版本,只需要把新版解压后替换软链接的指向,配置文件可以继续复用,不需要修改环境变量和脚本路径。很多老手都会用这个习惯。
文件名里的hadoop3.2是针对Hadoop 3.2客户端库预编译的版本。因为Spark经常需要读写HDFS、对接Hive等,如果后续要接YARN,选择预编译为hadoop3.2的包是最稳妥的。你要是只用Standalone,其实任意一个预编译包都能跑,但考虑到后续扩展,统一用它最省心。
解压完成后,需要把/opt/spark目录的属主改成你操作集群的用户,否则后面启动脚本会因为目录权限问题报错:
chown -R 你的用户名:你的用户组 /opt/spark-3.2.x-bin-hadoop3.2注意这一步经常被忽略,因为如果使用root操作,可能没问题;但为了生产环境的安全惯例,用普通用户来管理集群会更合理。
3.2 spark-env.sh核心参数详解
Spark的配置文件位于安装目录的conf子目录下。初次使用时,你会在里面看到一堆.template结尾的模板文件。需要先把spark-env.sh.template复制成spark-env.sh:
cd /opt/spark/conf cp spark-env.sh.template spark-env.sh vim spark-env.sh我的配置如下,直接写在文件末尾:
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk export SPARK_MASTER_HOST=master export SPARK_MASTER_PORT=7077 export SPARK_MASTER_WEBUI_PORT=8080 export SPARK_WORKER_CORES=2 export SPARK_WORKER_MEMORY=3g export SPARK_WORKER_WEBUI_PORT=8081 export SPARK_PID_DIR=/opt/spark/pids export SPARK_LOCAL_DIRS=/opt/spark/local_dirs逐个说明:
- JAVA_HOME:不用多说,指向JDK安装目录。
- SPARK_MASTER_HOST:Master节点绑定的主机名或IP。这里我填的是master,因为/etc/hosts里已经做了映射,Spark启动时会监听在这个地址上。很多新手在这里填了127.0.0.1,导致Worker从远程无法连接Master,这是最常见的坑。
- SPARK_MASTER_PORT:Master的通信端口,默认是7077。Worker连接Master时用的就是这个端口,可以理解为Master对外提供调度服务的“业务端口”。
- SPARK_MASTER_WEBUI_PORT:Web UI端口,默认8080,你打开浏览器访问http://192.168.10.11:8080就能看到集群监控页面。
- SPARK_WORKER_CORES:每个Worker节点最多可以分配给Spark的CPU核数。我这里机器只有2核,就填2。注意这个参数不是设置Worker进程数,而是告诉Spark,这个Worker能使用多少核来跑Executor的并发任务。
- SPARK_WORKER_MEMORY:每个Worker节点最多可以分配给Spark的内存大小。我给3g而不是4g,因为操作系统本身和各种后台进程也需要内存,全部分给Spark会导致机器卡死,Worker进程甚至可能被操作系统杀掉。
- SPARK_WORKER_WEBUI_PORT:Worker的Web UI端口,默认8081,多个Worker节点共用这个端口,但因为是不同机器,所以不会冲突。
- SPARK_PID_DIR:Spark启动时会记录Master和Worker进程的PID,方便停止脚本准确找到进程。默认放在/tmp下,但/tmp可能被系统定期清理,稳妥起见放到/opt/spark/pids。
- SPARK_LOCAL_DIRS:Spark在本地磁盘上存放shuffle临时数据和block数据的目录,默认是/tmp。如果磁盘空间不大,建议指定到一个空间充足的目录,并预先创建好。
你可以看到,内存和CPU参数的设置是有讲究的。我这台机器是2C4G,Spark Worker给了3g内存和2核,剩下1g内存留给操作系统。如果你用小内存机器,比如2G内存,我建议把SPARK_WORKER_MEMORY设为1g甚至更低,宁可牺牲执行性能,也要保证集群稳定运行。
3.3 workers文件配置
在Spark 3.2中,原来的slaves文件已经改名为workers。这个改动不仅是命名变化,更意味着官方在统一术语。把workers.template复制成workers然后编辑:
cp workers.template workers vim workers文件里的内容很简单,一行一个Worker节点主机名即可:
master worker1 worker2如果你不想让master参与计算,就不把master写进这个文件,只保留worker1和worker2。第一行前面默认带#号的行是注释示例,注意不要照搬。三台机器上这个文件的内容要一致,因为start-workers.sh脚本是在master上读取这个文件的,所以严格来说只需要在master上配置正确,但从维护角度建议同步到所有节点,让配置保持一致。
这里再提一下为什么推荐写主机名而不是IP。Spark在Web UI和日志里会显示主机名,方便人识别节点;如果配置了DNS或者/etc/hosts解析,主机名在语义上也比一堆IP地址清晰得多。但前提是每台机器的/etc/hosts里都同步维护了这些记录。
4. 集群分发与启停验证
配置文件写完,不代表集群就能跑了,还需要把Spark安装目录分发到其他节点,并确保每个节点的JDK、hosts文件也保持一致。这一步执行到位,后面启动集群时会非常顺畅。
4.1 同步文件到各节点
在master上把整个/opt/spark软链接指向的实体目录同步到worker1和worker2:
cd /opt rsync -av /opt/spark-3.2.x-bin-hadoop3.2 worker1:/opt/ rsync -av /opt/spark-3.2.x-bin-hadoop3.2 worker2:/opt/这里推荐用rsync而不是scp,因为rsync可以增量传输,文件夹很大时重传效率更高,而且用-av参数可以保留权限属性。如果没有安装rsync,先执行yum install -y rsync。我一般不直接软链接同步,因为软链接需要重新在目标机器上创建,直接把整个实体目录拷过去最省事。
同步完成后,在worker1和worker2上分别创建软链接:
ln -s /opt/spark-3.2.x-bin-hadoop3.2 /opt/spark mkdir -p /opt/spark/pids /opt/spark/local_dirs chown -R 你的用户名:你的用户组 /opt/spark /opt/spark-3.2.x-bin-hadoop3.2同时把master上的/etc/hosts文件内容同步到worker1和worker2,保证所有机器的主机名解析一致。很多人搭建时只配了master的hosts,结果worker节点启动后解析不了master,通信直接失败。用下面的命令快速拷贝:
scp /etc/hosts worker1:/etc/hosts scp /etc/hosts worker2:/etc/hosts4.2 启动集群与角色确认
确认好配置后,在master上执行启动命令。在Spark 3.2里,你可以单独启动Master和Worker,也可以直接使用sbin/start-all.sh一把梭。我的习惯是先启动Master,再启动Worker,这样如果某个角色出错,日志定位会更精准。
先启动Master:
cd /opt/spark sbin/start-master.sh然后启动所有Worker:
sbin/start-workers.sh如果是在旧版本里,这个脚本叫start-slaves.sh,Spark 3.x已经换成workers之后,新脚本名就是start-workers.sh。注意运行这个脚本的前提,就是之前配置好的SSH免密登录。
启动后会看到类似“starting org.apache.spark.deploy.worker.Worker, logging to /opt/spark/logs/spark-xxx”的日志路径提示。通过jps命令来确认各节点的进程状态:
- master上会有Master、Worker两个进程
- worker1和worker2上会有Worker进程
jps如果jps没装,先执行yum install -y java-1.8.0-openjdk-devel来补齐JDK工具。启动成功后,浏览器打开http://192.168.10.11:8080,能看到集群总览页,显示Workers节点数量、每个Worker的CPU核数、内存总量以及可用资源。状态为ALIVE就说明注册成功。
4.3 提交SparkPi任务验证
集群起来了,到底能不能跑任务?最简单的验证方式就是用Spark自带的SparkPi示例程序,这个程序会通过蒙特卡洛方法估算圆周率。在master上执行:
/opt/spark/bin/spark-submit \ --master spark://master:7077 \ --deploy-mode client \ --class org.apache.spark.examples.SparkPi \ /opt/spark/examples/jars/spark-examples_2.12-3.2.1.jar \ 100参数说明:
- --master指定了Spark集群的URL,格式是spark://Master主机名:7077
- --deploy-mode client表示Driver运行在提交任务的客户端机器上,也就是master节点。如果你用cluster模式,Driver会运行在某个Worker上,这种模式适合真正生产环境,但学习和调试用client更直观
- --class指定主类
- 最后面的100是传给SparkPi的参数,数值越大计算量越大,最终输出结果精度会更高
执行后如果能在日志里看到类似“Pi is roughly 3.14159...”的输出,说明整个分布式集群已经正常工作了。任务运行期间,刷新一下Master的Web UI,可以看到一个RUNNING状态的Application,点进去能看到各Executor分布在哪台Worker上,以及每个Executor占用的内存和CPU核数。
4.4 配置History Server延伸验证
任务跑完,你会发现在Master Web UI里的Application记录很快就不见了。这是因为默认情况下,Spark没有开启事件日志持久化,任务结束后的Executor和Stage信息不会保留。为了后续排查问题和做性能分析,建议顺手把History Server配起来。
在conf目录下复制一份spark-defaults.conf.template为spark-defaults.conf,并追加:
spark.eventLog.enabled true spark.eventLog.dir file:///opt/spark/logs/eventLogs spark.history.fs.logDirectory file:///opt/spark/logs/eventLogs先创建eventLogs目录:
mkdir -p /opt/spark/logs/eventLogs chown -R 你的用户名:你的用户组 /opt/spark/logs然后启动History Server:
/opt/spark/sbin/start-history-server.sh之后每次提交任务,Spark都会把事件日志写入这个目录,History Server的默认端口是18080,访问http://192.168.10.11:18080你就会看到历史上运行过的所有应用记录。这里有一个注意点:eventLogs目录必须在所有Worker上都能访问。因为日志默认是Executor所在节点直接写入的,如果多个节点都要写,要么目录共享,要么每台机器上都有这个路径。最简单的方式是把/opt/spark整个目录同步到所有节点,里面自然就包含了eventLogs目录。如果没有共享存储,那就每个Worker本地建一个同样的路径即可。
5. 常见问题与排查技巧实录
搭建过程中踩坑是必然的,我自己反复搭过很多次,把最常见的几类问题和处理手段整理成一张速查表。建议收藏,遇到问题先对照一下,比自己硬猜效率高很多。
| 现场现象 | 可能原因 | 排查命令 | 处理方式 |
|---|---|---|---|
| Worker启动后几秒就消失 | Worker分配内存过大,系统内存不足触发OOM | dmesg | tail -30,查看日志目录下的worker日志 | 调低SPARK_WORKER_MEMORY,并确认机器剩余内存 |
| Worker启动时报Connection refused | Master没有监听在预期的IP/端口,或防火墙拦截 | 在Worker上执行telnet master 7077 | 检查SPARK_MASTER_HOST配置和/etc/hosts,测试时可以先放行7077、8080、8081端口 |
| 提交任务后一直WAITING,没有被调度 | Worker可用内存只剩几百MB,无法分配Executor | 看Master Web UI的Workers页面,检查可用内存 | 调低Spark任务executor内存,或者调低Workers内存占用 |
| jps看不到Master或Worker进程 | 配置错误或启动脚本报错 | 查看/opt/spark/logs下的out日志 | 按日志里提示的错误逐一解决,最常见是JAVA_HOME没找到 |
| 浏览器无法访问8080页面 | 防火墙开启且未放行端口 | firewall-cmd --list-ports | 开发环境可临时firewall-cmd --add-port=8080/tcp,生产环境谨慎处理 |
| 主机名无法解析 | /etc/hosts内容不一致 | 在每台机器执行hostname,ping master | 同步所有机器的/etc/hosts |
5.1 Worker反复挂掉的教训
这里面我最想单独说的是OOM问题。学习用的机器内存不大,如果你把SPARK_WORKER_MEMORY设成了一个超出实际内存的值,Worker启动后不会立刻报错,它会正常注册到Master,但可能几分钟后进程就没了。这时候去日志目录下看一眼,很常见的是Out Of Memory并伴随内核杀进程的记录。我之前就遇到过一位同学在2G内存的机器上硬设了2g给Spark Worker,结果操作系统内存耗尽,Worker直接被kill掉,页面上一会儿显示这个节点ALIVE,一会儿又显示DEAD,状态反复横跳。
解决办法就是合理给Worker分配内存。建议预留总内存的百分之二十五到百分之三十给操作系统和基础服务,特别是机器上还要跑Master进程时,Master自身也要占用一两百兆内存。更稳的做法是先free -h看实际内存,再根据25%余量去配置。性能优化这件事,是建立在稳定之上的,先把资源预算做保守,再慢慢扩大也不晚。
5.2 通过日志定位复杂问题
当你遇到Web UI上看不到Worker,或者任务莫名其妙失败时,最重要的是看日志。Spark的日志输出位置在安装目录的logs子目录下,命名格式通常是spark-用户-角色-主机名.out。查看最新日志:
tail -100 /opt/spark/logs/spark-xxx-master-master.outWorker端的日志也一样:
tail -100 /opt/spark/logs/spark-xxx-worker-worker1.out日志里经常出现的异常包括各种ClassNotFoundException、FileNotFoundException、连接超时等。ClassNotFoundException多半是包问题,比如换了一个Spark版本后旧的jar包没有同步;FileNotFoundException多半是路径配置问题,检查spark.local.dir和eventLog目录是否存在;连接超时则需要回顾一下网络和防火墙配置。养成看日志的习惯后,你会发现大部分问题都能在日志前几十行里找到原因,远比自己瞎改配置文件靠谱。
从我的实操经验来看,搭建过程中真正耽误时间的通常不是高深原理,而是hosts映射不一致、内存配太大、防火墙没放行这几类基础问题。优先排查网络连通性,再看资源数值是否合理,最后再深挖日志,这个顺序能节省你大量时间。
最后再多说一句:如果你手头机器有限,其实在一台机器上把Master和Worker都启动起来,用submit提交一个SparkPi任务,也能体会到完整调度流程。但想真正理解分布式,最好还是像我这样用三台虚拟机,亲眼看看任务是怎么被分配到不同节点的。搞定Standalone之后,你可以再去试试把Hadoop部署起来,将Spark切到YARN模式下运行,两相对比,整个资源管理体系的轮廓就会非常清晰了。