最近几周连续有朋友问同一个问题Spark源码和原理看了不少一到自己动手搭集群就卡壳尤其是该选哪种部署方式这一步。我给的回答一直很简单——学习阶段别急着上YARN或K8S先用Spark Standalone分布式搭建把整个资源调度过程跑通性价比最高。这篇教程系列第九篇就是基于Spark 3.2在CentOS7上完整搭建Standalone分布式集群的实战记录踩过的坑、每个配置项背后的原因都会写清楚适合正准备从单机local模式跨到分布式环境的读者也适合那些搭完集群但总报各种诡异问题、想一次性弄明白的人。1. 为什么我建议用Standalone模式做Spark 3.2入门集群1.1 三种部署模式的第一层取舍先纠正一个常见的认知误区很多人觉得学Spark就应该直接奔着生产环境的标准去一上来就搭YARN甚至K8S结果环境还没跑通先被资源管理器本身的部署给劝退了。实际学习阶段的诉求和线上完全不一样——你需要的是最快速度看到一个Task怎么被调度到多台机器上执行而不是先搞懂数百项的容器编排配置。Spark本身支持四种部署方式local、Standalone、YARN、K8S/Mesos。local模式只能单机跑理解不了分布式到底分布在哪里YARN需要额外维护HDFS和ResourceManager一套Hadoop环境搭下来少说两个小时K8S就更重了里面牵扯的资源模型和Spark自身完全隔离。而Standalone模式最大的优势是Spark自带完整的Master-Worker调度体系不需要任何外部组件解压配置就能起集群。从原理层面讲Standalone模式下的Master节点负责接收作业请求、给Worker分配ExecutorWorker节点负责启动Executor进程并汇报心跳。这套调度模型跟YARN其实很像只是实现得朴素很多——但对你理解Driver请求资源、Executor执行任务、Task并行度这些核心概念来说朴素反而更友好因为你能直接看到每一步发生了什么。1.2 这次搭建的集群规划与角色分配我这次准备了三台CentOS7虚拟机节点命名统一用node101、node102、node103内存各4G、双核CPU。三台是底线一台Master加两台Worker分布式特征才能完整呈现——比如一台Worker挂了作业还能不能继续跑、Executor在多个节点上如何横向分布这些场景两台Worker才能玩得开。角色分配如下节点角色核心职责node101Master Worker资源调度中枢同时参与计算node102Worker启动Executor执行任务node103Worker启动Executor执行任务为什么不单独用一台纯Master原因很简单三台机器每台只有4G内存如果Master单独占一台剩下的两台Worker各4G内存跑几个像样的测试作业就吃紧了。我自己测试下来让Master节点兼任Worker只要SPARK_WORKER_INSTANCES设置合理后面会讲对调度稳定性几乎没有影响但能多出整整一台机器的计算资源。当然如果你是8G内存以上的机器纯Master节点分工更干净排查问题时角色边界也更清晰。2. 环境准备阶段三个绕不过的坑2.1 JDK版本与Spark 3.2的兼容性这一步看着简单实际是整篇搭建过程中最容易埋雷的地方。Spark 3.2官方支持JDK 8和JDK 11听起来很宽松但当你用yum直接装OpenJDK的默认版本时CentOS7默认源可能会给你装上openjdk 1.8.0的某个老版本。这本身没问题问题出在版本号细节Spark 3.2需要JDK 8u201及以上版本否则部分Java类在编译或运行时会出现奇怪的UnsupportedClassVersionError。我的建议是一律手动安装固定版本。在三台机器上分别执行yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel装完立刻验证版本确认输出中不是过旧的小版本号java -version然后是JAVA_HOME环境变量。这一步容易被忽略的是OpenJDK通过yum安装后路径通常是/usr/lib/jvm/java-1.8.0-openjdk但这个路径是个软链接实际指向可能随版本变化。最稳妥的做法是先readlink -f拿到真实路径再写进/etc/profileexport JAVA_HOME/usr/lib/jvm/java-1.8.0-openjdk export PATH$JAVA_HOME/bin:$PATH配完执行source /etc/profile三台机器都要配。很多Worker节点启动失败最后查来查去就是JAVA_HOME没生效——Spark的start脚本检测不到Java会直接报JAVA_HOME is not set但报错位置非常隐蔽经常被淹没在启动日志里。2.2 SSH免密登录与主机名映射Spark的start-all.sh脚本会通过SSH远程登录到workers文件里列出的节点去逐个启动Worker如果每一次登录都要输密码脚本会直接卡死在半路。所以Master到所有Worker节点包括自己的SSH免密登录是整个集群启动的大前提。配置过程分三步我在每个节点上把~/.ssh/id_rsa生成好ssh-keygen -t rsa -P -f ~/.ssh/id_rsa然后在node101上把公钥分发到所有节点包括node101自身ssh-copy-id node101 ssh-copy-id node102 ssh-copy-id node103这里有一个我当年栽过的跟头如果你规划Master也承担Worker角色一定记得ssh-copy-id node101也要执行。因为start-all.sh在启动本地Master之后还会再SSH连一次本机去启动Worker如果本机免密没配好你会在启动日志里看到Master已经起来了但Worker全部失败。然后是/etc/hosts配置三台机器上都要写同样的内容否则Spark节点之间通过主机名通信会解析失败。我这里用内网示例IP做演示你替换成自己的实际IP即可192.168.1.101 node101 192.168.1.102 node102 192.168.1.103 node103配完hosts之后需要重新登录一次SSH会话否则当前终端的hostname解析缓存不会刷新。2.3 防火墙与SELinux的取舍这节内容在真实场景里争议很大但对我来说结论很明确如果是自己测试的虚拟机环境直接systemctl stop firewalld并禁止开机自启能省掉后面一连串的端口排查时间。systemctl stop firewalld systemctl disable firewalldSELinux同理测试环境建议先setenforce 0临时关闭。我之前遇到过一种极其隐蔽的情况防火墙开着但Spark的8080端口已经手动放行Web UI也能正常访问结果作业提交一直卡在Waiting for resources查了半天才发现是SELinux拦截了Worker和Master之间的7077端口通信日志里却只显示连接超时。说实话这类权限拦截在真实线上环境应该仔细分析再决定但学习阶段完全没必要在这一环浪费时间先关掉等集群跑顺了再回头研究加固也不迟。3. 解压配置Spark 3.2与老版本的一个关键差异3.1 目录结构与spark-env.sh逐项拆解下载spark-3.2.1-bin-hadoop3.2版本的安装包解压到指定目录后建议第一时间把目录改名成简短路径不然$SPARK_HOME环境变量要写很长一串tar -zxvf spark-3.2.1-bin-hadoop3.2.tgz mv spark-3.2.1-bin-hadoop3.2 /opt/spark然后配置SPARK_HOME并加入PATH三台机器同步export SPARK_HOME/opt/spark export PATH$SPARK_HOME/bin:$SPARK_HOME/sbin:$PATH紧接着是全文最关键的一个文件$SPARK_HOME/conf/spark-env.sh。默认只有spark-env.sh.template模板先复制一份再改cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh我最终用的配置如下每项的用途逐个说明export JAVA_HOME/usr/lib/jvm/java-1.8.0-openjdk export SPARK_MASTER_HOSTnode101 export SPARK_MASTER_PORT7077 export SPARK_WORKER_CORES2 export SPARK_WORKER_MEMORY3g export SPARK_WORKER_INSTANCES1SPARK_MASTER_HOSTMaster绑定的主机名必须写能被所有节点解析到的主机名或IP写成域名或localhost会导致Worker无法注册。SPARK_MASTER_PORTMaster与Worker通信的RPC端口默认7077一般不用改。SPARK_WORKER_CORES每个Worker最多可分配给Executor的CPU核数。SPARK_WORKER_MEMORY每个Worker可供Spark作业使用的总内存注意是可供Spark使用不是机器物理内存总额。后面讲资源调优时这里是个大坑。SPARK_WORKER_INSTANCES每台节点上启动的Worker进程实例数量。我固定设成1避免一个节点上多个Worker进程互相抢资源。这里多提醒一句SPARK_WORKER_MEMORY不要贪。我测试机上物理内存4G给Worker分配3g已经算激进了因为你还要给操作系统、Master进程、后续提交作业的当前进程留余量。如果设成4gWorker启动时大概率正常第一个作业提交上来就内存耗尽整个节点卡死。3.2 workers文件别再用Slaves了这是Spark 3.2和老版本最明显的差异之一老版本用来声明Worker节点清单的文件叫slaves但从Spark 3.0起这个文件已经改名为workers老教程里让你改slaves文件的操作在3.2版本下完全不生效。我当时刚从老版本的习惯迁移过来愣是照着一个旧文配了配置结果start-all.sh只起了Master一个Worker都没起来。正确操作是把$SPARK_HOME/conf/workers.template复制为workers然后在里面逐行写节点名node101 node102 node103值得注意的一点是通常教程会建议Master节点不要写入workers文件但我这里由于Master作为Worker参与计算所以三台节点都在清单里。如果你的规划是纯Master节点那就只在workers里写Worker节点清单。3.3 配置文件同步与目录权限配置改完之后最省事的方式是把整个Spark目录同步到其他节点而不是一台一台手动去改scp -r /opt/spark rootnode102:/opt/ scp -r /opt/spark rootnode103:/opt/这里有一个常见问题解压后的Spark目录默认属主是当前用户如果你在node101上用root操作但其他节点的用户权限不一致会导致启动脚本执行时Permission Denied。建议在三台机器上统一指定一个运维用户或者统一用root测试然后chown -R root:root /opt/spark配置文件分发完毕后最好在每台节点上检查一下workers文件有没有被意外覆盖。4. 启动集群确认的不只是进程还有调度4.1 启动顺序先Master再Worker很多人直接跑sbin/start-all.sh一把梭我相信你在很多教程里也看到过这个写法。但这个脚本做的事情是先在当前节点启动Master再根据workers文件去挨个启动Worker。它默认认为你的workers文件已经配好、SSH免密已经生效如果这两点没满足报错会非常迷惑。我的建议是分步启动方便定位问题。先在node101上启动Mastersbin/start-master.sh启动成功后用jps应该能看到Master进程。然后再次在node101上启动所有Workersbin/start-workers.sh看到每个节点上都出现了Worker进程说明注册成功。这里我特意用start-workers.sh而不是start-all.sh逻辑上更清晰把启动Master和启动Worker两个步骤解耦哪一步出了问题一眼就能定位到是Master的配置问题还是Worker节点的通信问题。4.2 Web UI与日志验证集群真正健康进程起来不代表集群真的健康。Spark Standalone提供两个Web UI页面是判断集群状态最直观的手段Master UIhttp://node101:8080Worker UIhttp://node102:8081每个Worker节点独立端口打开Master UI后重点看两个信息第一Workers表格里有没有出现node101、node102、node103三行记录第二每行记录的Alive状态是否为1。如果节点数量对不上或者状态反复变化说明网络通信链路有问题需要回头检查hosts和防火墙。日志的查看位置也值得记一下$SPARK_HOME/logs/目录下Master和Worker的日志文件按日期滚动生成。启动Worker失败时的具体原因比如Unable to connect to Master、Executor registration timed out基本都能在这个目录下找到。4.3 提交第一个正式作业并追踪任务集群起来后用Spark自带的examples包里的Pi计算程序做一次完整的端到端验证$SPARK_HOME/bin/spark-submit \ --master spark://node101:7077 \ --deploy-mode client \ --executor-memory 2g \ --executor-cores 2 \ --total-executor-cores 4 \ --class org.apache.spark.examples.SparkPi \ $SPARK_HOME/examples/jars/spark-examples_2.12-3.2.1.jar 100执行成功后刷新Master UI应该能在Running Applications或Completed Applications区域看到这个作业的记录。点进去还能看到每个Executor分布在哪个Worker节点上、每个Task的执行时间。这个界面值得多研究一下它能帮你直观理解两个Worker节点各分配了一个Executor每个Executor里并行跑着两个Task这件事比任何原理图都有说服力。5. Standalone模式下的资源调优与真实踩坑5.1 Worker内存与Executor内存的关系我先用一个真实场景来说刚开始我把SPARK_WORKER_MEMORY设成3g然后用--executor-memory 3g提交作业结果集群调度作业时直接报错。原因其实很直接——每个Worker的3g内存并不是一个整体Executor在这个上限里面申请资源但并不能一次性把所有内存都占满。Standalone模式下每个Worker节点向Master上报自己能提供的内存资源然后Master把这个资源池按Executor粒度切分。Executor内存由--executor-memory决定但它只是Executor的堆内存实际操作中Spark还会在堆外分配一些内存所以一个Executor实际占用的物理内存会略高于你指定的值。当Worker可分配的总量不足以容纳你申请的所有Executor时作业就永远处于Waiting for resources状态。我在三节点测试环境里总结出一个安全配比Worker总内存3g提交作业时--executor-memory设置在2g左右--executor-cores不超过2这样每个Worker能容纳一个Executor整体集群还能留出一个备用槽位。这套配比的关键是Worker的可用内存必须大于Executors申请总量的峰值否则作业要么排队等到天荒地老要么直接内存溢出。5.2 spark-submit提交参数的正确姿势节省篇幅我直接给出一套跨节点跑分布式作业的标准姿势。注意两个关键词一个是资源相关参数另一个是部署模式--executor-memory单个Executor的堆内存--executor-cores单个Executor使用的CPU核数--total-executor-cores整个作业最多占用的总CPU核数--deploy-mode clientDriver运行在提交作业的客户端我见过很多人在这里犯混淆以为--executor-memory设置得越大越好或者以为--total-executor-cores是每台机器的核数导致任务数量远多于实际可供分配的核数大量Task排队。正确的理解是total-executor-cores是全局上限Master会在所有Worker节点上统筹资源来满足你申请的需求。还有一个细节因为集群里每台Worker报了3g可用内存如果你用--executor-memory 2g --total-executor-cores 4这种组合Master会在两台Worker上各启动一个Executor每个Executor占2g核2资源刚好分配干净。但如果再叠加--executor-memory 2g --total-executor-cores 6按照每个Worker当前的空闲核数Master最多只能按4核调度多出来的2核会一直等待——这不是死机而是正常的资源供需关系在起作用。5.3 一次Executor内存溢出的复盘有一次提交一个稍重的测试作业我直接把--executor-memory提到了3g想着既然SPARK_WORKER_MEMORY给了3g就能用满。跑起来不到一分钟节点上的物理内存直接被打满紧接着Executor进程被操作系统OOM Killer干掉。日志里能看到一行含糊的Killed而不是Java堆内存溢出。复盘后问题有三层第一层Worker总内存3g与Executor申请3g相等看上去够用但Spark Executor除了堆内存还有约300M的堆外开销第二层容器进程不是JVM一个还有代码缓存、线程栈等额外内存第三层机器物理内存4g中Worker占3g后留给操作系统和Master的只剩1g这显然不够。所以Executor内存填满Worker上限是极端危险操作留出至少20%的余量才是正确做法。之后我把SPARK_WORKER_MEMORY改回3g提交参数用--executor-memory 2g一切恢复正常。6. 排障手册Standalone集群最常见三个故障6.1 Worker进程反复消失先看日志再动配置最典型的现象是start-workers.sh执行时Worker进程短暂出现过几秒又从jps里消失了。绝大多数人第一反应是去改spark-env.sh里的一大堆参数但我踩过这个坑之后养成了一个习惯——任何进程存活性问题先打开日志确认原因再决定动哪里。Worker日志路径固定命令是tail -100 $SPARK_HOME/logs/spark-root-worker-*.out日志里如果出现Failed to connect to master大概率是Master地址问题检查SPARK_MASTER_HOST是否在Worker节点的/etc/hosts里能解析如果出现Master has no available resources则是内存分配不够如果出现Lost connection to master很可能是网络不稳定或防火墙中断了定时心跳。每次看到有人从配置文件开始排查我内心都会替那个日志文件觉得委屈——它里面写得清清楚楚。6.2 Web UI打不开 / 任务卡住端口与网络的排查顺序Web UI打不开这个现象我见过三种不同原因本地能开远端开不了、所有节点都开不了、Master能开Worker开不了。排查顺序建议是固定的用ss -tlnp | grep 8080确认Master进程确实在监听端口用telnet node101 8080测试端口连通性检查防火墙是否拦截了8080/8081/7077端口任务卡住不执行的排查则是另一套逻辑打开Master UI先看集群资源是不是已经用满再看作业页面的Executor列表。如果Executor一直处于Launching状态十有八九是Worker节点资源不足或者网络通信有延迟如果Executor正常但Task全在Pending问题通常在CPU核数分配上回到第5节讲的参数关系去调整。6.3 master URL细节提交作业时最常见的拼写坑spark-submit --master spark://node101:7077 ...这里的spark://协议前缀是必须写的省略或写错都会让客户端认不出Master地址。另外--deploy-mode默认是client模式这个模式要求客户端机器能通过网络访问到所有Executor节点——如果你在一个和集群网络隔离的本机提交作业即使URL写对Executor反向连接Driver时也可能失败表现为作业提交后Task全失败。一个容易被忽视的细节node101这个主机名必须在客户端机器上也解析到正确IP否则客户端连不上。跨网络提交时直接写IP更稳妥比如spark://192.168.1.101:7077。这个坑在单机测试时根本暴露不出来一旦脱离集群内部环境立刻就会撞上。最后说点个人体会。Standalone模式虽然最终生产环境可能不会直接用但它给我建立的资源调度直觉在后来排查YARN上各种分配问题时帮了大忙——理解了Executor和Worker的边界、理解了客户端模式的通信路径再看别的资源管理器只是换了一层壳而已。如果你现在也在学习Spark的路上我建议配置一次这个集群然后跑到Web UI里反复观察Task的分布这比直接背十遍调度源码都有用。