Spark 3.2.0预编译包部署与Standalone/YARN提交调优实战 简介spark-3.2.0-bin-hadoop3.2.tgz 是面向大数据开发、数据分析与机器学习实践者的 Spark 3.2.0 预编译发行包适配 Hadoop 3.2 环境可用于快速搭建本地或集群实验环境、运行官方示例并查阅源码与配置。压缩包共 1476 个文件约 287.02MB包含大量 py、jar、scala、java 文件分别对应 PySpark 接口、运行依赖库、Scala/Java 源码与示例同时配有 txt、rst 文档、sh/cmd/bat 启动脚本及 parquet、avro、orc 等样例数据便于按模块检索。已有 1126 人学习下载适合需要理解 Spark Core、SQL、Streaming、MLlib 与 GraphX 组件或进行性能调优、Kubernetes 部署验证的开发者参考。解压后可直接体验完整目录结构与提交、Shell、SQL 等命令行工具为环境搭建和二次开发提供较完整的基线材料。1. spark-3.2.0-bin-hadoop3.2.tgz 这个文件名其实说明了全部信息spark-3.2.0-bin-hadoop3.2.tgz 这串字符拆开看就三件事Spark 3.2.0 的发行版、Scala 2.12 编译的二进制、客户端依赖锁定在 Hadoop 3.2 这条线上。它是官方打好的预编译包解压、配两个环境变量、敲一条 spark-submit 就能跑不需要装 Maven、不需要拉 Scala 编译器、不需要对着源码构建脚本折腾。真正需要它的人多半不是来学 Spark 语法的而是要在还能连上老集群的机器上快速立起一套既能跑 Standalone 又能提交 YARN 的运行时做数据清洗、跑批、验证一段 SQL 逻辑或者在离线环境里给团队复现某个报错。它省掉的是编译和版本对齐这两道工序代价是你得接受它锁死的版本范围——JDK 8 或 11、Scala 2.12、Hadoop 3.2 客户端越过这条线一步后面全是需要处理的兼容问题。2. 从解包到第一条命令把 spark-shell 在 local[4] 上跑通2.1 解包后先认清这几个目录各自管什么很多人解压完直接bin/spark-submit一把梭出了错才回头翻目录效率很低。这套包的结构其实非常规整几分钟理清能省掉后面几小时的瞎猜目录里面装的是什么什么时候会碰它bin/spark-submit、spark-shell、pyspark、spark-sql、spark-class每次提交作业sbin/start-master.sh、start-worker.sh、start-history-server.sh、start-thriftserver.sh起 Standalone、起 History Serverconf/一堆.template模板文件第一次部署jars/Spark 自身 jar Hadoop 3.2 客户端 jar排 jar 冲突、查依赖examples/jars/官方示例 jar冒烟测试python/pyspark 的 Python 侧代码和 zip 包指定 PYSPARK_PYTHON 时data/几份小样本数据含 README.md写第一条测试作业关键点在于conf/里全是.template后缀的模板把spark-env.sh.template改名为spark-env.sh、把spark-defaults.conf.template改名为spark-defaults.conf才会被启动脚本读到。没有后者的文件名就是不会生效不报任何错。解包本身没什么技巧但建议养成用软链接指向版本目录的习惯将来升级到 3.3、3.4 时只换软链集群上写死的路径不用动# 解包到统一目录tgz 顶层目录本身就是 spark-3.2.0-bin-hadoop3.2 mkdir -p /opt/spark tar -xzf spark-3.2.0-bin-hadoop3.2.tgz -C /opt/spark # 建软链后续所有配置只认 current ln -sfn /opt/spark/spark-3.2.0-bin-hadoop3.2 /opt/spark/current ls -l /opt/spark/current/bin/spark-submit上面这段里-C是指定解压目标目录-sfn中的n表示把已有软链当普通文件覆盖避免重复执行时报“File exists”。解完之后ls一下bin/确认spark-submit、spark-shell、spark-class都在包就是完整的。2.2 conf 目录下两个模板必须复制环境变量只认 spark-env.shspark-env.sh本质是一个被sbin/*.sh用source加载的 bash 脚本它只负责“进程启动时的环境”不负责 Spark 运行时参数而spark-defaults.conf是 SparkConf 读的配置。这两类东西混写是新手最常见的翻车点把spark.executor.memory写进spark-env.sh会静默失效日志里一句提示都没有。#!/usr/bin/env bash # conf/spark-env.sh —— 只放环境变量别写 spark.xxx 配置项 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export SPARK_HOME/opt/spark/current export SPARK_CONF_DIR/opt/spark/current/conf export HADOOP_CONF_DIR/etc/hadoop/conf # 多网卡机器必须显式指定否则可能注册到 docker0 或虚拟网卡地址 export SPARK_LOCAL_IP192.168.10.11 export SPARK_LOG_DIR/var/log/spark export SPARK_PID_DIR/var/run/spark export PYSPARK_PYTHON/usr/bin/python3.8这段里最值得说的是两个变量。SPARK_LOCAL_IP在多网卡环境下如果不定driver 向 master 注册的地址可能是虚拟网桥的地址表现就是 worker 一直在Launching executor然后超时UI 上 executor 数量永远是 0翻 executor 日志能看到Connection refused连的是个莫名其妙的网段。PYSPARK_PYTHON则是给 pyspark 用的集群上系统自带 Python 版本五花八门不显式钉住driver 和 executor 用的解释器版本不一致pandas_udf 那一层会直接抛类型不匹配。另一个隐蔽的细节spark-env.sh里的变量必须带export。启动脚本source完它之后是用 fork 的方式拉起 JVM 的没 export 的变量只存在于当前 shell子进程读不到表现为“我明明写了 JAVA_HOME还是报找不到 Java”。spark-defaults.conf我一般只放全局默认值作业级参数一律走命令行方便回溯# conf/spark-defaults.conf spark.master spark://192.168.10.11:7077 spark.serializer org.apache.spark.serializer.KryoSerializer spark.eventLog.enabled true spark.eventLog.dir hdfs://ns1/spark/eventlog spark.sql.adaptive.enabled true spark.sql.adaptive.coalescePartitions.enabled true spark.sql.shuffle.partitions 400 spark.yarn.jars hdfs://ns1/spark/jars/*spark.eventLog.enabled建议一上来就开后面所有“当时到底跑了什么”的疑问都靠 event log 加 History Server 来回答这是唯一的追溯手段。spark.yarn.jars指到 HDFS 上是为了避免每次提交都把几百兆 jar 重新上传一遍——第一次跑的时候把jars/*传上去即可。2.3 用 spark-submit 跑通一个能验证链路的 WordCount验证包是否可用不要用spark-shell敲两行sc.parallelize(1 to 10).count()就完事那只验证了 JVM 起来了。要验证的是读文件、shuffle、写结果整条链路。写个最小脚本# wordcount.py —— 放在 $SPARK_HOME 下的任意目录都行 import sys from pyspark.sql import SparkSession from pyspark.sql import functions as F def main(src: str, dst: str) - None: spark (SparkSession.builder .appName(wordcount-smoke) # 本地模式也要限制 shuffle 分区否则 200 个空分区拖慢启动 .config(spark.sql.shuffle.partitions, 8) .getOrCreate()) df spark.read.text(src) out (df.select(F.explode(F.split(F.col(value), r\s)).alias(word)) .where(F.col(word) ! ) .groupBy(word) .count() .orderBy(F.col(count).desc())) # coalesce(1) 只为方便 cat 结果生产上不要这么干 out.coalesce(1).write.mode(overwrite).csv(dst, headerTrue) spark.stop() if __name__ __main__: main(sys.argv[1], sys.argv[2])提交命令用local[4]四个线程模拟四个 slot能真实触发 shuffle 和并发调度cd /opt/spark/current ./bin/spark-submit \ --master local[4] \ --driver-memory 2g \ --conf spark.ui.port4040 \ --conf spark.sql.shuffle.partitions8 \ wordcount.py \ data/README.md \ /tmp/wc_out参数逐个说清楚--master local[4]是本地四线程不用起任何 daemon--driver-memory在 local 模式下就是这唯一的 JVM 堆大小给 2g 足够spark.ui.port4040是 Web UI 端口被占用时 Spark 会自动往后找 4041但如果你在多台机器上同时跑端口漂移会让 History Server 里的链接对不上所以固定下来更省心最后两个位置参数分别传给脚本的src和dst。跑完之后ls /tmp/wc_out应该看到_SUCCESS和一个part-00000-*.csvhead一下能看到词频最高的几行。如果这一步卡住不动、UI 上 stage 一直停在running而 task 数为 0八成是data/README.md的相对路径没对上——spark-submit的工作目录是提交时所在的目录不是$SPARK_HOME用绝对路径最稳。再用--version确认这套包自报的身份顺手把 Java 和 Scala 版本记下来后面排查依赖冲突时全靠它./bin/spark-submit --version # 输出里重点看三行Spark version 3.2.0 / Scala version 2.12.x / Java version 1.8.x3. Standalone 与 on YARN两种 master 的资源配置与提交参数3.1 Standalone 的 master/worker 拆分与核数内存怎么切Standalone 是这套包里最轻的集群形态master 只做资源调度和状态维护几乎不吃资源worker 才是真正跑 executor 的进程。起法就三条命令但要起对得先把conf/workers和spark-env.sh里的资源值捋清楚。# master 节点 ./sbin/start-master.sh # 会监听 7077RPC和 8080Master UI # worker 节点指向 master 的 RPC 地址 ./sbin/start-worker.sh spark://192.168.10.11:7077 # 多台 worker 时把主机名写进 conf/workers然后在 master 上一次性拉起 cat conf/workers # node02 # node03 # node04 ./sbin/start-workers.sh3.2 的sbin/下已经是start-worker.sh老的start-slave.sh还在但已经标了弃用脚本里会打一行 warning不影响运行但别在新脚本里继续用。资源切分的关键在spark-env.sh里的三个变量export SPARK_WORKER_CORES28 # 32 核机器留 4 核给系统和其它服务 export SPARK_WORKER_MEMORY110g # 128G 机器留 18G别顶满 export SPARK_DAEMON_MEMORY2g # master/worker 自身 JVM 的堆SPARK_WORKER_MEMORY上报的是“可分配给 executor 的总内存”worker 自己还要活OS 还要 page cache 给 shuffle 文件读写顶满上报的结果是 executor 分到了内存但机器开始 swap表现是 task 跑得极慢、GC 时间占比飙到 40% 以上。血泪经验是留 15%~20% 的余量。Standalone 下提交作业cluster 模式需要 driver 也跑在 worker 上参数和 client 模式差别不大但--supervise值得加./bin/spark-submit \ --master spark://192.168.10.11:7077 \ --deploy-mode cluster \ --supervise \ --name wc-standalone \ --driver-memory 2g \ --driver-cores 1 \ --executor-memory 8g \ --executor-cores 4 \ --num-executors 6 \ --conf spark.executor.memoryOverhead1g \ wordcount.py hdfs://ns1/input/wc /tmp/wc_out--supervise的作用是 driver 异常退出后由 worker 自动重启适合长跑作业一次性跑批反而不要加因为失败重试会把真正的错误淹没在重启日志里。--num-executors 6在 Standalone 下是硬上限worker 资源不够时会排队等UI 上表现为WAITING状态的 app 一直挂着不是卡死。3.2 提交到 YARNhadoop3.2 预编译包替你省掉的兼容工作bin-hadoop3.2这个后缀的含义是包里那批 Hadoop 客户端 jar 是 3.2 版本的和 Hadoop 3.x 集群的 RPC 协议、文件系统接口对得上。你不需要自己往jars/里塞集群的 hadoop-client也不用操心 Guava、Netty 的版本打架——官方在打包时已经对齐过一轮。这是这个包最大的实用价值。如果集群是 Hadoop 2.7 那条线这个包别硬上会以NoSuchMethodError、NoClassDefFoundError或者文件系统初始化失败的形式还债正确做法是换bin-hadoop2.7那个包。提交到 YARN 之前HADOOP_CONF_DIR必须指到集群配置目录让 Spark 能读到core-site.xml、hdfs-site.xml、yarn-site.xmlexport HADOOP_CONF_DIR/etc/hadoop/conf ./bin/spark-submit \ --master yarn \ --deploy-mode cluster \ --name etl-orders-daily \ --queue root.etl \ --driver-memory 2g \ --driver-cores 1 \ --executor-memory 8g \ --executor-cores 4 \ --conf spark.executor.memoryOverhead2g \ --conf spark.dynamicAllocation.enabledtrue \ --conf spark.dynamicAllocation.initialExecutors6 \ --conf spark.dynamicAllocation.minExecutors4 \ --conf spark.dynamicAllocation.maxExecutors40 \ --conf spark.dynamicAllocation.executorIdleTimeout90s \ --conf spark.shuffle.service.enabledtrue \ --conf spark.eventLog.enabledtrue \ --conf spark.eventLog.dirhdfs://ns1/spark/eventlog \ --conf spark.yarn.historyServer.addresshttp://192.168.10.11:18080 \ hdfs://ns1/jobs/etl.jar \ hdfs://ns1/ods/orders \ hdfs://ns1/dwd/orders几个参数值得单独解释。--queue要和 YARN 调度器里真实存在的队列名一致写错的报错是Queue root.xxx not found但常常被后面的Application submission failed盖住要往上翻。spark.dynamicAllocation.enabledtrue打开之后--num-executors会被忽略实际并发由initialExecutors起步、按 backlog 伸缩这是最容易误解的一点——很多人以为两个都写会取较小值。spark.shuffle.service.enabledtrue是动态分配的前提executor 被回收后它写过的 shuffle 数据如果不在外部 shuffle 服务里下游 stage 重算时会抛FetchFailedException然后整段 lineage 重跑。这个开关要求 NodeManager 上已经配好对应 aux-service集群侧没开的话这个参数就是自欺欺人。3.3 内存模型与参数表容器真实占用是 executor overheadYARN 调度看的是容器总内存等于spark.executor.memory加spark.executor.memoryOverhead不是前者。这个认知差是绝大多数“物理内存没超但容器被杀”的根源。参数默认值常见改法影响面spark.executor.memory1g6g~12gJVM 堆执行内存 缓存内存都从这里切spark.executor.memoryOverheadmax(384m, 10%)堆的 15%~20%堆外Netty direct buffer、Python worker、native 库spark.executor.cores14~5每个 core 一个并发 task超过 5 核写 HDFS 吞吐反降spark.memory.fraction0.60.6执行存储占堆比例调大挤掉用户数据结构spark.memory.storageFraction0.50.4~0.5缓存区不被执行内存挤出的比例spark.sql.shuffle.partitions200总核数 × 2~3shuffle 分区数AQE 开启后会二次合并spark.sql.adaptive.advisoryPartitionSizeInBytes64m64m~128mAQE 合并分区的目标大小算一笔账就明白为什么“核越多越好”不成立。一台 64G 内存、32 核的节点NodeManager 分配 56G 内存、28 核。如果按 8g 2g 10g 一个容器最多 5 个 executor每个 4 核用掉 20 核还剩 8 核闲着——内存先到瓶颈核被浪费。反过来按 4g 1g 5g 一个容器能起 11 个 executor但每个只有 4 核就超了实际要按min(内存能塞几个, 核数/每executor核数)去配让两边同时打满才是合适的。Executor 核数我一般建议 4~5。超过 5 核的单个 executorHDFS 写路径上的并发连接数和 GC 压力都会上升实际吞吐不一定比两个 4 核 executor 好而且一个 executor 挂了损失的 task 更多重算代价更大。4. 避坑清单五个报错从现象到根因的排查路径4.1 报 Unsupported class file major version现象spark-submit起来几秒就退出栈里出现java.lang.UnsupportedClassVersionError或Unsupported class file major version 61。原因系统 PATH 上默认的java是 JDK 17而 3.2 这条线支持的是 JDK 8 和 11。spark-env.sh里的JAVA_HOME没写或者写了但被 shell 里更靠前的 PATH 覆盖。用./bin/spark-submit --version一眼就能看到实际用的是哪个 Java。解决显式钉住JAVA_HOME并且确认--version输出对得上如果用 JDK 11务必通过bin/下的启动脚本启动脚本会自动补上--add-opens那一串参数绕开脚本自己用java -cp拼命令行的会在反射访问上直接失败。4.2 报 Wrong FS路径被当成本地文件现象java.lang.IllegalArgumentException: Wrong FS: hdfs://ns1/input, expected: file:///。原因Spark 进程没读到集群的core-site.xml文件系统实例还是本地默认的file:///。这通常发生在HADOOP_CONF_DIR只在当前 shellexport了但通过sbin/start-*.sh拉起的 daemon 没继承到或者这个变量压根没写进spark-env.sh。解决两条路任选其一并确认生效——把export HADOOP_CONF_DIR...写进spark-env.shdaemon 也能继承或者把core-site.xml、hdfs-site.xml软链到$SPARK_HOME/conf下。改完用spark-shell里spark.read.text(hdfs://...).count()验证别直接又提大作业。4.3 报 Task not serializable现象作业在第一个map/filter就炸org.apache.spark.SparkException: Task not serializable栈顶指向你写的那个匿名函数。原因闭包捕获了 Driver 端不可序列化的对象——最常见的是把SparkSession、数据库连接池、日志实例、外部类的成员变量带进了 lambda。Scala 的闭包是整体序列化的只要引用到就一起打包。// 反例闭包里直接用了外层的 spark 和成员变量 conn class BadJob(spark: SparkSession) { private val conn DriverManager.getConnection(jdbc:...) // 不可序列化 def run(ds: Dataset[String]) ds.map(s conn.query(s).toString) // 提交即炸 } // 正例只捕获可序列化的值重资源在 executor 侧按分区创建 val threshold 100 ds.mapPartitions { it val client new HeavyClient() // 每个分区建一次分区内复用 try it.map(x client.score(x) threshold) finally client.close() }排查手法看异常栈里Serialization trace那一段它会直接列出是哪个字段不可序列化实在找不到就给可疑成员加transient lazy val让它不参与序列化。4.4 容器被杀但 JVM 堆明明没满现象YARN 日志里Container killed by YARN for exceeding memory limits. 12.3 GB of 12 GB physical memory used退出码 143。原因容器上限是executor.memory memoryOverhead而 overhead 默认只有堆的 10%下限 384m。PySpark 的 Python worker 进程、Netty 的 direct buffer、Parquet 解压用的 native 内存全都从这 10% 里出。写 Parquet、跑 pandas_udf 的作业特别容易撞这条线。解决把spark.executor.memoryOverhead提到堆的 15%~20%PySpark 再单独设spark.executor.pyspark.memory给 Python worker 划一块。另外注意spark.memory.offHeap.enabledtrue时off-heap 内存也是从 overhead 里扣的别把 overhead 又按 10% 算。4.5 shuffle 阶段 FetchFailed 或磁盘写满现象org.apache.spark.shuffle.FetchFailedException: Failed to connect to node02:7337或者 executor 日志里java.io.IOException: No space left on device。原因三种情况最常见——shuffle 分区数太多导致每个分区文件都小但文件数爆炸spark.local.dir只挂了一块盘shuffle 写满动态分配把 executor 回收了下游取不到中间数据又没有外部 shuffle 服务兜底。解决先开 AQE 让分区自动合并3.2 里默认已经开别手动关spark.local.dir指向多块盘用逗号分隔Spark 会轮询写确认spark.shuffle.service.enabled与集群 aux-service 都已配置两者必须同时打开才有意义。5. 读写与调优AQE、Kryo 与小文件合并的落地参数5.1 内置 Hive 支持与分区覆盖的正确写法预编译包自带 Hive 支持enableHiveSupport()之后能直接读写 Hive 表元数据默认用内置的 Derby。但要落地到集群必须先把 warehouse 目录指到 HDFS 上否则它会老老实实往提交机器上的file:/user/hive/warehouse写数据散落一地第二天才发现。from pyspark.sql import SparkSession spark (SparkSession.builder .appName(orders-dwd) .config(spark.sql.warehouse.dir, hdfs://ns1/warehouse) # 关键动态分区覆盖只覆盖写入涉及的分区而不是清空整表 .config(spark.sql.sources.partitionOverwriteMode, dynamic) .config(spark.sql.hive.metastore.version, 2.3.9) .config(spark.sql.hive.metastore.jars, maven) .enableHiveSupport() .getOrCreate()) (spark.read.parquet(hdfs://ns1/ods/orders) .where(dt between 20240101 and 20240131) .write.mode(overwrite) .partitionBy(dt) .parquet(hdfs://ns1/dwd/orders))partitionOverwriteModedynamic是这段里最重要的一行。默认的static模式下mode(overwrite)会把整张表的数据清掉再写一旦上游只补了一天数据历史分区全没了这种事故我见过不止一次。改成dynamic后只有本次写入涉及的分区被替换。where(dt between ...)这种字符串条件能触发分区裁剪前提是分区列的类型和字面量对得上如果dt是 int 而写成字符串裁剪会失效plan 里会看到PartitionFilters: []全表扫描。连远端 Hive metastore 时spark.sql.hive.metastore.version要和远端一致metastore.jarsmaven会让 Spark 去拉对应版本的 metastore 客户端 jar离线环境要改成builtin或指向本地 jar 目录否则启动时会卡在下载上报连接超时。5.2 AQE 的三个开关合并分区、倾斜 Join、本地 Shuffle 读取3.2 这条线里spark.sql.adaptive.enabled默认已经是 true很多人以为自己没开 AQE其实一直在本文还有配套的精品资源点击获取