Spark standalone模式集群搭建指南:从配置到作业提交与故障排查 1. 为什么我推荐用 standalone 模式搭建第一个 Spark 集群先交代一下背景我接触 Spark 已经有好几年期间从本地模式一路用到生产环境的资源调度框架。如果你现在正准备学习 Spark想在一台或几台服务器上把集群跑起来我第一个建议就是 standalone 模式。它是 Spark 自带的一种集群部署方式不依赖 YARN 或 Kubernetes核心架构就是一个 Master 节点管着若干个 Worker 节点。对初学者来说这是最能直观理解分布式计算运行机制的一条路。standalone 模式能帮你解决什么问题最直接的是你只需要准备几台能互相通信的机器装好 JDK下载 Spark 安装包改几个配置项就能拥有一个真正意义上的多机集群。你可以亲眼看到任务是怎么被拆分成一个个 executor在 Worker 节点上并行执行的。相比 YARN 和 K8s少了那一层额外的资源调度组件排查问题时会省掉很多干扰项。我的建议是第一次搭集群别绕开 standalone 直接上生产调度框架先用它把 Spark 本身的机制摸透后面迁移到其他模式会顺得多。这篇文章适合谁看如果你是刚接触大数据、想在测试环境搭一个多节点 Spark 集群的人或者你已经在用 local 模式想体验分布式任务调度都可以照着本文操作。我会把配置思路、每台机器的分工、启动验证步骤、提交作业的参数含义、常见坑全部展开讲整个过程可以完整复现。2. 搭建前的准备机器规划、JDK 版本与安装包选择2.1 硬件规划两台机器起步怎么分配角色先别急着下载安装包你把机器规划好再动手后面会省很多事。standalone 集群最小的规模是两台节点一台跑 Master一台跑 Worker。我实际测试时经常用三台机器比例是 1 个 Master 加 2 个 Worker。这样做的好处是你能真正验证 Spark 的任务调度是否会在多台 Worker 之间做负载均衡而不是所有任务都堆在同一台机器上。机器配置方面Master 节点对计算资源要求不高内存 2GB 以上即可因为它主要负责接收作业请求、调度任务、维护集群状态真正跑数据计算的是 Worker 节点内存建议至少 4GB核心数越多越好。如果你只有一台物理机也可以用虚拟机拆出多个节点或者用 Docker 起容器效果是等价的。需要注意无论用什么方式节点之间的 hostname 必须能互相解析这是后续所有分布式组件通信的基础。关于操作系统CentOS 7、Ubuntu 20.04、Rocky Linux 这些都行。不用在系统版本上过于纠结但要保证一个前提所有节点的系统时间尽量同步。分布式集群对时间偏差比较敏感误差太大可能导致节点心跳异常。最简单的做法是安装 NTP 服务或者至少手动校准一下。2.2 JDK 版本选型Spark 3.x 与 Java 的兼容性Spark 是跑在 JVM 上的JDK 是刚需。选错版本会出现各种奇怪问题最典型的是启动时直接报 UnsupportedClassVersionError或者 Master 进程刚起来就退出。我的建议是如果使用 Spark 3.2 到 3.5 这个区间JDK 选 8 或 11 都可以生产环境中我用 Java 8 比较多因为生态兼容性最好。如果你用的是 Spark 4.0 预览版才需要考虑 Java 17。这里提醒一句安装 JDK 时不要只配 JAVA_HOME还要把 PATH 加上并且确认java -version能正常输出。很多搭建教程会默认你懂这些但我在帮别人排查问题时发现相当一部分启动失败的原因就是 JAVA_HOME 指向了不存在的目录或者 bin 目录没加进 PATH。集群里每一台机器都要装不要只在 Master 装完就以为万事大吉。2.3 下载 Spark 安装包与 SSH 免密登录配置安装包去哪下去 Spark 官网下载页找spark-3.5.x-bin-hadoop3.2.tgz这一类带有bin-hadoop字样的包。为什么选带 Hadoop 标识的版本因为 Spark 在读写 HDFS 时需要用到对应的 Hadoop 客户端库即使你现在不装 HDFS带上它也不会有副作用以后接存储时省得重装。下载完成后在 Master 节点解压到统一目录比如/opt/spark。但这里有个常见误区很多人只在 Master 解压Worker 节点却什么都没有。standalone 模式启动 Worker 时是通过 SSH 在远程机器上执行命令的如果 Worker 上没有解压好的 Spark 目录启动过程会直接失败。我习惯的做法是先把安装包分发到所有节点统一解压到相同路径比如/opt/spark3.5这样配置脚本里写绝对路径时不会出错。SSH 免密登录是另一个必须提前处理的事。从 Master 节点要能免密 SSH 到所有 Worker 节点执行ssh worker1 hostname不提示输入密码才算配好。具体操作是在 Master 上执行ssh-keygen生成密钥对然后把公钥追加到每个 Worker 的~/.ssh/authorized_keys里最后用chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys收紧权限。如果你用的是 root 用户注意有些系统默认禁用了 root 的 SSH 登录需要提前检查sshd_config。3. 核心配置拆解spark-env.sh 与 workers 文件3.1 配置文件的职责分工Spark 的配置目录在安装包解压根目录下的conf里。启动集群时真正会读到的文件主要有三个spark-env.sh、workers老版本叫slaves、spark-defaults.conf。很多初学者喜欢把一堆参数全塞进spark-defaults.conf其实每个文件有自己明确的定位。workers文件的作用最简单它罗列了所有 Worker 节点的 hostname每行一个。启动集群时Master 会读取这个文件逐个 SSH 到对应机器上启动 Worker 进程。spark-env.sh里放的是环境变量比如 JAVA_HOME、Master 的地址和端口、每个 Worker 能使用的内存和 CPU 核心数。spark-defaults.conf里放的是 Spark 作业运行时的默认参数比如每个执行器的内存、序列化方式、事件日志开关等。三个文件分工明确后面调参时你会体会到这个分层的重要性。有一点值得注意Spark 从 3.0 开始配置文件模板里已经没有slaves这个文件名了取而代之的是workers。网上很多教程还在写slaves照做的话新版 Spark 是识别不了的。最好的做法是进入conf目录直接复制workers.template为workers再编辑内容。3.2 几个最容易配错的细节先说spark-env.sh。我贴一个典型的配置然后逐个解释每行含义。# conf/spark-env.sh export JAVA_HOME/usr/local/jdk1.8.0_202 export SPARK_MASTER_HOSTmaster export SPARK_MASTER_PORT7077 export SPARK_WORKER_CORES4 export SPARK_WORKER_MEMORY4g export SPARK_WORKER_INSTANCES1SPARK_MASTER_HOST指定 Master 绑定的主机名默认是机器的 hostname。这里经常有人踩坑如果机器有多个网卡或者/etc/hosts里映射了多个主机名必须指定一个集群内其他节点能访问到的名字否则 Worker 会连不上 Master。SPARK_MASTER_PORT默认就是 7077这是 Master 对外接收任务提交的端口要和 Web UI 的 8080 端口区分开。SPARK_WORKER_CORES和SPARK_WORKER_MEMORY是控制单个 Worker 节点最多向 Spark 资源池贡献多少 CPU 和内存的关键参数。注意这两个值不是越大越好。我曾经在一台 16 核 64GB 的机器上把SPARK_WORKER_MEMORY配成 64g结果提交任务后频繁出现 executor 进程被系统杀掉的情况。原因是 Worker 本身、操作系统等都要消耗内存Spark 默认会在分配时预留一部分但如果你设置的内存几乎占满物理内存风险就很高。经验值是给系统留 2GB 到 4GB 余量。关于SPARK_WORKER_INSTANCES默认是 1表示一个 Worker 节点只启动一个 Worker 进程。你可以把它设置成大于 1让一台机器上跑多个 Worker这样做的实际效果是会把机器的总资源拆成多份但通常没有必要反而会增大进程管理的开销。我在测试时偶尔会用这个参数模拟多节点效果。3.3 网络与主机名解析集群通信的前置条件配置文件写得再对网络不通也白搭。最容易被忽视的就是/etc/hosts。我在三台机器上分别加入如下映射192.168.1.10 master 192.168.1.11 worker1 192.168.1.12 worker2每台机器的/etc/hosts都要有全部三台机器的条目。为什么要这么做因为 Spark 在启动 Worker 时Worker 会反向解析自己的 hostname然后去连接 Master。如果 Worker 机器的 hostname 解析不到它甚至会尝试解析到一个回环地址结果就是 Worker 一直处于等待状态Master 的 Web UI 上永远看不到这个节点。验证方法很简单在每一台机器上执行ssh master hostname和ssh worker1 hostname输出应该分别是对应的主机名。再执行ping worker1确认 IP 解析正确。如果域名解析用了内网 DNS也可以不配 hosts但要保证所有节点对集群内主机名的解析结果完全一致。防火墙是另一个容易忽略的点。很多 Linux 发行版默认开启了 firewalld 或 ufw会拦截 8080、7077、6066 这些端口。如果你从外部访问不到 Master 的 Web UI先别怀疑配置直接临时关闭防火墙试一下。生产环境不推荐关防火墙但你要知道问题根源针对性地放行端口。4. 启动集群与验证从 Web UI 到日志4.1 首次启动的正确姿势配置写完可以先手动在 Master 节点启动 Master 进程这样能第一时间看到输出方便排查问题cd /opt/spark3.5/sbin ./start-master.sh启动完成后jps命令应该能看到Master进程。如果看不到检查logs目录下的spark-...-org.apache.spark.deploy.master.Master-...out日志。接下来启动所有 Worker。如果你已经配置好 SSH 免密可以直接执行./start-workers.sh这个脚本会读取workers文件逐个 SSH 到对应机器执行 Worker 启动命令。启动完不要急着看结果先等十几秒让 Worker 进程注册到 Master。然后访问http://master:8080如果界面显示了一个 Worker 节点状态是 ALIVE说明集群已经正常起来了。这里有个细节旧版 Spark 里用start-all.sh可以同时启动 Master 和 Worker但新版本已经把它拆分成start-master.sh和start-workers.sh。如果你在 Spark 3.x 的sbin目录里执行start-all.sh会看到一条 deprecated 警告功能还能用但不推荐。4.2 用日志和 Web UI 判断集群是否健康Web UI 是最直观的指标。进入 Master 页面后重点关注几个信息Cluster Summary 区的 Workers 数量、每个 Worker 的 Cores 和 Memory。如果 Worker 状态是 DEAD说明 Worker 启动后连接 Master 失败或者连接后又超时了。这时候去 Worker 节点查看日志通常报的是连接超时、hostname 解析失败这类网络问题。如果 Worker 状态是 ALIVE 但显示的内存或核心数和你预期不一致比如只有默认值那大概率是spark-env.sh里的环境变量没被正确加载。注意Worker 进程是通过 SSH 远程启动的它是一个非交互式 shell~/.bashrc里的 export 不一定生效所以在spark-env.sh里写死的参数要确认语法正确、路径真实存在。我再推荐一个排查奇招直接查看页面里 Driver 和 Executor 的日志链接。Spark 会把每个作业的日志输出到 Master 或 Worker 节点的指定目录点进去能看到完整的异常堆栈。很多新手不知道这个功能遇到问题只会看控制台其实控制台打印的往往只是错误摘要。5. 提交分布作业也是门手艺spark-submit 实战5.1 从官方示例到自定义作业集群起来后第一个要跑的不是你自己的业务代码而是官方自带的示例。Spark 安装包里的examples/jars目录下有现成的示例 JAR用它验证集群最稳妥不会引入代码问题干扰判断。执行命令如下cd /opt/spark3.5/bin ./spark-submit \ --master spark://master:7077 \ --class org.apache.spark.examples.SparkPi \ --executor-memory 2g \ --total-executor-cores 4 \ ../examples/jars/spark-examples_2.12-3.5.0.jar 100几个参数的作用我解释一下--master指定集群入口地址必须和SPARK_MASTER_HOST、SPARK_MASTER_PORT对应--class告诉 Spark 运行哪个主类--executor-memory是每个执行器进程的内存上限--total-executor-cores是你希望整个作业使用多少个 CPU 核心最后的100是传给 SparkPi 的参数表示计算圆周率的迭代次数。运行结束后控制台会输出类似Pi is roughly 3.141...的结果。看到这行字说明整个集群的任务调度链路是通的。接下来你就可以把一些自己写的 JAR 包提交上去格式和上面一样把--class换成你自己的主类即可。5.2 资源分配参数为什么任务卡住或报错提交作业时最常碰到的困惑是作业一直 Pending明明有资源却跑不起来。这通常和 Standalone 模式下 Executor 的分配逻辑有关。默认情况下每个 Worker 只为每个作业启动一个 Executor这个 Executor 能使用的 CPU 核心数由spark.executor.cores控制默认值是 1。如果你提交作业时指定--total-executor-cores 8而集群总共有 8 个核心那么理论上可以分配下去但如果某个 Worker 节点核数少或者内存不够分配任务就会一直等待。举一个真实场景两个 Worker每个 4 核 4GB。我提交作业时指定--executor-memory 4g --total-executor-cores 8。结果作业 Pending 了很久。原因很简单每个 Worker 的可用内存是 4GB但 Spark 在分配 Executor 时还要考虑 overhead 内存默认是 executor 内存的 10%所以实际需要的容器内存超过 4GB。任何一个 Worker 都单独无法满足这个资源需求自然分配不出 Executor。正确的做法有两种一是降低--executor-memory到2g或3g留出 overhead 空间二是通过配置让一个 Worker 可以启动多个 Executor但这需要调整spark.executor.cores和 Worker 的资源预算。我的经验是第一次跑作业先保守一点--executor-memory配成 Worker 内存的一半核心数配成 Worker 核心数的一半跑通了再逐步加大。5.3 查看作业运行状态8080 网页之外的细节提交作业后你会看到 Master 页面出现两个新区域Running Applications 和 Completed Applications。点进去可以看作业的 Executor 列表每个 Executor 使用的是哪个 Worker 的资源。这里有个细节很多人没注意如果作业还没开始页面上会显示 Waiting Executors说明资源不足作业处于排队状态。Standalone 模式在作业运行过程中不会把 stdout 完整地传回提交端。你在 spark-submit 的控制台只看到作业开始和结束的状态想要看日志得通过 Web UI 进入 Executor 的日志页面或者在代码里把日志输出到指定目录。这个特性对调试分布式作业影响很大我见过不少人因为看不到 print 输出误以为作业没执行。6. 高频故障排查与性能调优手册6.1 Worker 连不上 Master 的排查路径Standalone 集群最经典的问题就是 Worker 状态为 DEAD。排查时我习惯按以下顺序来先看 Worker 机器的spark-...-workder-...out日志如果是Connection refused那先检查 Master 是否真的在监听 7077执行netstat -anp | grep 7077如果端口在监听再检查 Master 配置的SPARK_MASTER_HOST是否被 Worker 机器通过 hosts 解析到了正确 IP最后检查防火墙。还有一种隐蔽情况Worker 是通过 SSH 远程启动的但 SSH 登录后 JAVA_HOME 没有传递。这会导致 Worker 启动脚本找不到 Java进程直接退出。你可能会疑惑明明在spark-env.sh里配了JAVA_HOME为什么还报错因为spark-env.sh里的变量在start-workers.sh环境中是存在的但如果你把JAVA_HOME写在~/.bashrc里远程非交互 shell 不会加载它。所以尽量把JAVA_HOME显式写在spark-env.sh中。6.2 Executor 被系统杀掉的常见原因我在前面提到过内存配置过大的问题。这里再展开一下。Executor 进程是独立的 JVM 进程运行在 Worker 节点上。如果你给 Worker 配置了SPARK_WORKER_MEMORY16g然后提交作业时--executor-memory 15g再加上 overhead操作系统很可能会因为物理内存不足而触发 OOM Killer直接把 Executor 进程杀掉。遇到这种问题如何确认是被系统杀的去 Worker 节点执行dmesg -T | grep -i kill如果看到相关记录基本就能确定。解决方法有两个方向一是降低 Executor 内存二是配置spark.executor.memoryOverhead。不过说实话在测试环境最省心的方案还是给 Worker 少分点资源留足操作系统的余量。6.3 作业长时间 Pending资源测算与动态分配Standalone 模式默认不支持 Executor 动态扩缩容。你提交作业时申请了多少资源集群就必须一次性满足否则作业就等着。所以当你看到作业卡住最快的判断方式是打开 Master 页面的 Workers 列表看每个 Worker 的 Used Cores 和 Used Memory。如果已经是满的说明是其他作业占用了资源如果还有剩余但作业仍 Pending那大概率是单个 Executor 的资源请求超过了任何单个 Worker 的剩余容量。是否要启用动态分配如果你在 Standalone 集群上有多个作业交替运行可以考虑在spark-defaults.conf里开启spark.dynamicAllocation.enabledtrue spark.shuffle.service.enabledtrue但注意Spark Standalone 模式对动态分配的支持相比 YARN 要弱一些我遇到的问题是扩容时新 Executor 启动速度偏慢在小集群上反而引入不确定性。两三个节点的测试环境我建议你手动指定资源更可控。6.4 hostname 变更后集群起不来的问题很多场景下你可能会在测试环境克隆虚拟机导致所有节点的 hostname 一样或者在安装完成后改过主机名。此时启动 Master 和 WorkerWeb UI 上显示的是奇怪的名字甚至多个 Worker 显示为同一个名字。这会影响 Spark 的注册逻辑。解决办法是统一规划主机名并同步修改/etc/hostname和/etc/hosts。改完重启机器或者至少执行hostnamectl set-hostname让主机名立刻生效。然后删除 Spark 在/tmp或work目录下缓存的工作目录再把进程全部停掉重启。这个坑看起来蠢但排查起来真的很费时间特别是你没有意识到克隆机器导致主机名冲突的时候。7. 扩展用 ZooKeeper 实现 Master 高可用7.1 单点 Master 的问题前面搭建的集群Master 是单点运行的。它挂了整个集群就不能接受新作业正在运行的作业也会因为无法维持调度而失败。测试环境无所谓但如果你的集群要跑比较长时间的任务或者想模拟生产环境就需要考虑 Master 的高可用。在 Standalone 模式下官方支持的方案是依靠 ZooKeeper 做主备切换。多台 Master 节点同时向 ZooKeeper 注册其中一台是 Active其他是 Standby。Active Master 挂掉后Standby 会自动接管状态。这里要注意这个方案不仅解决 Master 进程的单点问题还依赖 ZooKeeper 来持久化集群的运行状态。7.2 配置步骤与验证思路要启用 HA首先需要一个 ZooKeeper 集群哪怕是最小的 3 节点。在spark-env.sh中额外加以下配置export SPARK_DAEMON_JAVA_OPTS-Dspark.deploy.recoveryModeZOOKEEPER -Dspark.deploy.zookeeper.urlnode1:2181,node2:2181,node3:2181 -Dspark.deploy.zookeeper.dir/spark_status注意spark.deploy.zookeeper.dir是 ZooKeeper 里用于保存 Spark 集群状态信息的根节点要保证所有 Master 节点使用同一个路径。然后在每台要作为 Master 的节点上执行start-master.sh。它们会竞争注册只有一个能成为 Active其余都是 Standby。访问每台 Master 的 8080 端口Active 的那个显示的标题是 ALIVEStandby 显示 STANDBY。验证故障切换时直接 kill 掉 Active Master 进程等十几秒之前 Standby 的节点会自动变成 Active。你会发现原来注册的 Worker 依然存在已提交的作业状态也是保留的。对于测试环境这已经足够让你理解高可用的原理了。不过在生产环境中还需要考虑 ZooKeeper 本身的可用性以及网络分区带来的脑裂问题。这些话题比较大如果你的集群规模不大建议先从单 Master 开始等真正需要连续长时间运行的服务时再引入 HA。8. 配置顺手把 History Server 开了8.1 为什么需要 History Server默认情况下Spark 作业运行完Web UI 上的历史记录就看不到了。有时候你第二天想复盘上一个任务跑了多久、Executor 使用了多少内存、是否有 Stage 失败重试如果没有 History Server这些信息全找不回来。History Server 做的事情很简单把作业运行日志持久化到磁盘或 HDFS然后通过一个独立的 Web 服务提供查询。配置它只需要在spark-defaults.conf里加两行spark.eventLog.enabledtrue spark.eventLog.dir/opt/spark3.5/eventLog然后启动历史服务cd /opt/spark3.5/sbin ./start-history-server.sh注意eventLog.dir指定的目录如果在多节点环境下运行作业每个节点都要能写。如果写到本地路径那么 Executor 日志最终只存在于某台 Worker 上History Server 可能因为读取不到本地文件而无法展示。我的推荐做法是在集群环境接上 HDFS比如hdfs://master:8020/spark-history如果没接 HDFS那就保证所有节点这个路径共享或者干脆用 NFS。测试环境里先把日志落在 Master 本地也能解决大部分需求。8.2 通过 History 界面逆向检查作业性能History Server 的默认端口是 18080。打开以后你能看到所有运行过的 Application。点进任意一个页面结构和运行时 Web UI 几乎一样但多了时间维度的信息。我最常关注的分析点有两个一个是每个 Stage 的 Shuffle Read 和 Shuffle Write 量如果某个 Stage 的 Shuffle 数据量特别大说明存在数据倾斜或分区不合理另一个是 Executor 的时间线如果某个 Executor 运行时间远大于其他 Executor基本就是节点资源不均衡或数据倾斜的体现。这些都是后续调优的重要线索所以我的建议是集群搭好之后第一时间把 History Server 开起来养成观察作业历史记录的习惯。9. 几个我一再强调的实操心得这里的每一条都是我踩坑踩出来的写出来供你参考。第一所有节点的 Spark 安装路径尽量保持一致。如果你在 Master 上把包解压到了/opt/spark3.5在 Worker 上最好也解压到/opt/spark3.5。这样做的好处是配置文件里可以放心地用绝对路径而且排查问题时你在任何一台机器上执行命令路径习惯是一致的。我自己曾试过不同节点装在不同目录后面写自动化脚本时吃了不少亏。第二启动集群后先花五分钟检查 Web UI不要急着提交作业。只要 Web UI 上的 Worker 是 ALIVE资源数值符合预期那么集群基本是健康的。如果在这里数据都不对后面提交作业大概率会失败。检查 Web UI 的另一个好处是能顺便熟悉 Spark 的运维界面这对你后面使用其他部署模式也有帮助。第三提交作业的参数要记录成文档。同一个集群不同作业的资源需求差异很大。我习惯在作业提交脚本里写明这个作业为什么申请这么多内存和核心而不是只写命令。这样过了两周你再看自己的脚本还能回忆起当初的决策依据。特别是多个人共用集群时资源申请是否合理直接影响其他人的作业排队长短。第四不要过早引入外部调度框架。如果你刚接触 Spark直接上 YARN 或 Kubernetes遇到问题时你很难分清是 Spark 的问题还是资源层的问题。Standalone 模式能先把 Spark 自身这套内核搞明白等你理解了 Executor 和 Worker 的关系、任务调度的机制再往上迁移理解成本会低很多。第五多读日志。遇到任何奇怪的问题第一反应不是改配置而是看日志。Spark 的日志体系已经比较完善Master、Worker、Executor 都有独立日志文件。日志里通常直接写了失败原因比你瞎猜高效得多。如果日志不够再去调整日志级别或者通过 Web UI 查看详细堆栈。排查分布式问题路径感比经验更重要。