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_HOST=master export SPARK_MASTER_PORT=7077 export SPARK_WORKER_CORES=4 export SPARK_WORKER_MEMORY=4g export SPARK_WORKER_INSTANCES=1SPARK_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_MEMORY=16g,然后提交作业时--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.enabled=true spark.shuffle.service.enabled=true但注意,Spark Standalone 模式对动态分配的支持相比 YARN 要弱一些,我遇到的问题是扩容时新 Executor 启动速度偏慢,在小集群上反而引入不确定性。两三个节点的测试环境,我建议你手动指定资源更可控。
6.4 hostname 变更后集群起不来的问题
很多场景下,你可能会在测试环境克隆虚拟机,导致所有节点的 hostname 一样,或者在安装完成后改过主机名。此时启动 Master 和 Worker,Web 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.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=node1: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 的那个显示的标题是 ALIVE,Standby 显示 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.enabled=true 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 查看详细堆栈。排查分布式问题,路径感比经验更重要。