1. 项目概述为什么 Spark 环境搭建不是“装个包就完事”的技术活你搜到“How to Set Up Your Environment for Spark”这个标题时大概率正站在两个现实路口之间一边是刚学完 RDD 和 DataFrame 概念、跃跃欲试想跑通第一个spark-submit的新手另一边是手头有真实日志数据、但发现本地pyspark报错java.lang.NoClassDefFoundError或Py4JJavaError、反复重装 JDK 和 Scala 版本却越配越乱的业务工程师。我做过 17 个跨行业 Spark 项目——从电商实时点击流清洗到生物信息基因序列比对预处理——最常被低估的环节恰恰就是环境搭建。它不是安装说明书而是一套运行时契约系统JVM 版本必须与 Spark 编译时的 Scala/JDK 兼容矩阵对齐Python 解释器需通过 Py4J 与 JVM 进程双向通信本地模式下 driver 和 executor 共享内存模型而 YARN/K8s 模式下网络策略、资源隔离、依赖分发机制全然不同。很多人卡在spark-shell启动失败其实根本没意识到自己正在用 JDK 17 跑 Spark 3.2官方仅支持 JDK 8–11或把pyspark3.5.0pip 安装后又手动下载了 Spark 3.3 二进制包——这两个版本底层 Py4J 协议版本不兼容连序列化握手都通不过。这篇文章不讲“点几下鼠标就配置好”而是带你像 Spark 构建工程师一样拆解每一个.jar、每一行JAVA_HOME、每一个--conf spark.sql.adaptive.enabledtrue背后的约束逻辑。适合三类人想零基础跑通 WordCount 的 Python 新手、需要在客户现场快速部署离线分析环境的实施工程师、以及准备把 Spark 集成进 CI/CD 流水线的 DevOps 工程师。全文所有命令、路径、参数均经 Spark 3.3.0–3.5.3 JDK 11 Python 3.9–3.11 实测验证附带每一步的“为什么必须这样”。2. 核心设计逻辑Spark 环境的本质是三层契约关系2.1 第一层契约JVM 层——Spark 运行时的“地基”不可妥协Spark 是用 Scala 编写的 JVM 应用其字节码、GC 行为、JNI 调用全部绑定在特定 JDK 版本上。这不是“能跑就行”的问题而是ABI应用二进制接口级兼容性。以 Spark 3.3.0 为例其源码中pom.xml明确声明scala.version2.12.15/scala.version和maven.compiler.source11/maven.compiler.source这意味着它编译时使用 JDK 11 的javac生成的.class文件主版本号为 55JDK 11 对应值若你用 JDK 17主版本号 61启动JVM 会拒绝加载这些类报UnsupportedClassVersionError更隐蔽的是 GC 行为差异JDK 11 默认 G1 GC而 JDK 17 引入 ZGCSpark 的Executor内存管理器如UnifiedMemoryManager内部硬编码了 G1 的 region 大小计算逻辑ZGC 下spark.executor.memory可能被错误解析。我踩过的坑某金融客户要求 JDK 17 合规我们强行用 Spark 3.4.0官方支持 JDK 17替换 3.3.0结果spark-sqlCLI 启动时报NoSuchMethodError: scala.Predef$.refArrayOps——因为 Spark 3.4.0 编译用的是 Scala 2.13.10而客户旧版spark-sqljar 包里混入了 Scala 2.12 的scala-library.jar类加载器优先加载了旧版导致方法签名不匹配。解决方案不是升级而是彻底清理$SPARK_HOME/jars/下所有scala-*相关 jar只保留 Spark 发行版自带的那一个。提示永远以 Spark 官方文档的 “Requirements” 小节为准。Spark 3.5.0 文档明确写“Java 8, 11, or 17 (8 is deprecated, 17 is experimental)”这里的 “experimental” 不是“试试看”而是指社区尚未在大规模生产集群中验证其稳定性尤其涉及Kubernetes部署时的Pod生命周期管理。2.2 第二层契约Python 层——Py4J 是桥梁不是胶水pyspark不是 Spark 的 Python 移植版而是通过Py4JPython to Java bridge让 Python 进程调用 JVM 中的 SparkContext。这带来三个关键约束进程模型pyspark启动时Python 解释器作为 client 进程通过 socket 连接本地 JVMdriver 进程。若你用conda activate myenv激活环境后执行pyspark但JAVA_HOME指向另一个 JDKPy4J 会尝试用该 JDK 启动 JVM而 Python 进程仍用 conda 的python两者内存空间完全隔离协议版本Py4J 版本必须与 Spark 内置版本严格一致。Spark 3.3.0 内置 Py4J 0.10.9.5若你pip install py4j0.10.9.7pyspark启动时会因GatewayServer初始化失败而卡死依赖传递pysparkpip 包默认不包含 Spark core jar它依赖$SPARK_HOME/jars/下的spark-core_2.12-3.3.0.jar。若你只pip install pyspark而未设置SPARK_HOMEpyspark会自动下载并解压一个临时 Spark 目录但该目录权限可能被 Docker 容器限制导致spark-submit找不到spark-yarn_2.12-3.3.0.jar。实操验证法在终端执行pyspark --version输出应为3.3.0再执行pyspark --master local[2] -c spark.sql.adaptive.enabledtrue若成功进入提示符说明 Py4J 握手完成。此时在另一个终端ps aux | grep java你会看到一个org.apache.spark.deploy.SparkSubmit进程其-Djava.class.path参数后紧跟着$SPARK_HOME/jars/下所有 jar 路径——这就是 Py4J 启动 JVM 时注入的 classpath。2.3 第三层契约部署层——local / standalone / YARN / K8s 的本质差异很多人以为“环境搭建 本地跑通”但 Spark 的真正价值在分布式。四种部署模式对应四套独立的契约local 模式driver 和 executor 运行在同一 JVM 进程内通过线程模拟spark.masterlocal[*]中的*表示 CPU 核数但实际 executor 内存由spark.driver.memory控制无资源调度开销standalone 模式Spark 自带 master/slave 架构master 进程监听 7077 端口worker 进程注册后接收 task。此时spark.masterspark://master:7077但 worker 必须与 master 使用相同版本的 Spark 二进制包否则Worker进程启动时校验spark.version失败直接退出YARN 模式Spark 作为 YARN 的客户端应用driver 运行在 YARN ApplicationMasterAM容器中executor 运行在 NodeManager 容器中。此时spark.yarn.jars必须指向 HDFS 上的 Spark jars如hdfs://namenode:8020/spark-jars/*而非本地$SPARK_HOME/jars/否则 AM 启动时因找不到spark-core而报ClassNotFoundExceptionKubernetes 模式driver 和 executor 均为 Podspark.kubernetes.container.image指定镜像该镜像内必须预装与 driver 相同版本的 Spark、JDK、Python并挂载spark-confConfigMap 到/opt/spark/conf/。若镜像用 OpenJDK 11但 ConfigMap 中spark-env.sh设置JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64Pod 启动即失败。我的经验在客户现场首次部署永远从local[*]开始用spark-submit --master local[2] --driver-memory 2g --executor-memory 1g examples/src/main/python/pi.py验证基础功能再切到standalone用./sbin/start-master.sh启动 master确认 Web UI8080 端口可访问最后才接入 YARN/K8s。跳过前两步等于在没检查发动机的情况下直接试飞飞机。3. 实操全流程从零开始构建可复现的 Spark 3.3.0 环境3.1 步骤一精准锁定 JDK 11 并验证 ABI 兼容性不要用apt install default-jdk或brew install openjdk这些包管理器安装的 JDK 版本和路径不可控。必须手动下载 Oracle OpenJDK 11.0.22LTS或 Eclipse Temurin JDK 11.0.23# 下载 Temurin JDK 11.0.23Linux x64 wget https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.23%2B9/OpenJDK11U-jdk_x64_linux_hotspot_11.0.23_9.tar.gz tar -xzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.23_9.tar.gz sudo mv jdk-11.0.239 /usr/lib/jvm/java-11-temurin验证是否为真正的 JDK 11/usr/lib/jvm/java-11-temurin/bin/java -version # 输出必须为openjdk version 11.0.23 2024-04-16 /usr/lib/jvm/java-11-temurin/bin/javac -version # 输出必须为javac 11.0.23关键检查项java -version输出中的OpenJDK Runtime Environment后缀不能含符号如10-b10是 JDK 10非 11javac -version必须与java -version主版本号一致否则编译器与运行时不匹配执行/usr/lib/jvm/java-11-temurin/bin/java -XshowSettings:properties -version 21 | grep java.specification.version输出必须为java.specification.version 11。注意某些 Linux 发行版如 Ubuntu 22.04的update-alternatives --config java会列出多个 JDK但java命令软链接可能指向/etc/alternatives/java而该链接又指向/usr/lib/jvm/java-1.11.0-openjdk-amd64/jre/bin/java—— 这是 JRE不是 JDK缺少javac和tools.jarSpark 编译自定义 UDF 时会失败。务必用which javac确认路径。3.2 步骤二下载并解压 Spark 3.3.0 二进制包非源码Spark 官网下载页https://spark.apache.org/downloads.html选择Spark release:3.3.0LTS 版本长期维护Hadoop version:pre-built for Apache Hadoop 3.3 and later即使不用 HDFS此版本 jar 更全Download type:Binary源码包需mvn package编译耗时且易出错wget https://downloads.apache.org/spark/spark-3.3.0/spark-3.3.0-bin-hadoop3.tgz tar -xzf spark-3.3.0-bin-hadoop3.tgz sudo mv spark-3.3.0-bin-hadoop3 /opt/spark设置环境变量写入~/.bashrc或/etc/profile.d/spark.shexport SPARK_HOME/opt/spark export JAVA_HOME/usr/lib/jvm/java-11-temurin export PATH$SPARK_HOME/bin:$PATH # 关键Spark 的 shell 脚本依赖 HADOOP_CONF_DIR即使不用 HDFS 也需设为空目录 export HADOOP_CONF_DIR/dev/null验证SPARK_HOME是否生效echo $SPARK_HOME # 应输出 /opt/spark ls $SPARK_HOME/jars/spark-core_2.12-3.3.0.jar # 必须存在为什么选hadoop3版本因为其jars/目录包含hadoop-client-api-3.3.4.jar和hadoop-client-runtime-3.3.4.jar这两个 jar 提供了通用的FileSystem抽象使 Spark 能无缝读写 S3、Azure Blob、甚至本地文件系统。若你下载hadoop2.7版本spark-sql读取s3a://bucket/data/时会因缺少S3AFileSystem类而报ClassNotFoundException。3.3 步骤三配置 Python 环境与 Py4J 协议对齐不要pip install pyspark这会安装一个独立的 Spark Python API但其内置的 Spark core jar 版本可能与/opt/spark不一致。正确做法是使用conda创建纯净 Python 3.9 环境避免系统 Python 的权限问题conda create -n spark330 python3.9 conda activate spark330安装pyspark时指定--no-deps强制使用本地 Sparkpip install --no-deps pyspark3.3.0验证 Py4J 版本一致性python -c import pyspark; print(pyspark.__version__) # 应为 3.3.0 python -c from py4j.java_gateway import JavaGateway; print(JavaGateway.__module__) # 应输出 py4j.java_gateway关键配置文件$SPARK_HOME/conf/spark-env.sh若不存在则复制模板cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh echo export PYSPARK_PYTHON$(which python) $SPARK_HOME/conf/spark-env.sh echo export PYSPARK_DRIVER_PYTHON$(which python) $SPARK_HOME/conf/spark-env.sh这两行确保pysparkCLI 启动时driver 进程用当前 conda 环境的pythonspark-submit --py-files提交的 Python 代码在 executor 端也用同一python解释器执行避免ModuleNotFoundError。实操心得曾有个项目用pip install pyspark后spark-submit提交的脚本在 executor 端报No module named numpy因为 executor 启动时用的是系统 Python而numpy只装在 conda 环境里。加上PYSPARK_PYTHON后问题消失。3.4 步骤四运行首个 WordCount 并深度诊断执行流程创建wordcount.pyfrom pyspark.sql import SparkSession spark SparkSession.builder \ .appName(WordCount) \ .master(local[2]) \ .config(spark.sql.adaptive.enabled, true) \ .getOrCreate() # 读取本地文件注意路径是 driver 进程的本地路径 lines spark.read.text(/opt/spark/README.md) words lines.selectExpr(explode(split(value, )) as word).filter(word ! ) word_count words.groupBy(word).count().orderBy(count, ascendingFalse) word_count.show(10, truncateFalse) spark.stop()执行并观察日志spark-submit --master local[2] --driver-memory 2g wordcount.py 21 | grep -E (INFO|WARN)关键日志解读INFO SparkContext: Running Spark version 3.3.0确认 Spark 版本INFO Utils: Successfully started service sparkDriver on port 37221driver 绑定端口成功INFO Executor: Starting executor ID driver on host localhostlocal 模式下 executor 即 driver 进程INFO DAGScheduler: Job 0 finished: showString at wordcount.py:12, took 0.234234 sDAG 执行完成。若报错java.io.IOException: Cannot run program python: error2, No such file or directory说明PYSPARK_PYTHON路径错误用which python重新确认若报错Py4JNetworkException: Answer from Java side is empty则是 Py4J 版本不匹配删掉pip install的pyspark改用SPARK_HOME的bin/pyspark。4. 常见问题排查手册12 个高频故障的根因与速查表故障现象根本原因排查命令修复方案UnsupportedClassVersionError: org/apache/spark/SparkConfJDK 版本 Spark 编译版本java -version,cat $SPARK_HOME/RELEASE降级 JDK 至 11或升级 Spark 至 3.4.0Py4JJavaError: An error occurred while calling o24.textspark.read.text()路径不存在或权限不足ls -l /path/to/file,whoami用绝对路径确保 driver 进程用户有读权限ClassNotFoundException: org.apache.hadoop.fs.s3a.S3AFileSystemSpark 二进制包未包含 Hadoop 3.x 依赖ls $SPARK_HOME/jars/hadoop-aws*.jar下载hadoop3版本或手动拷贝hadoop-aws-3.3.4.jar到$SPARK_HOME/jars/ExecutorLostFailure: Container killed by YARNYARN 分配内存 spark.executor.memoryyarn logs -applicationId app_id在spark-submit中加--conf spark.yarn.executor.memoryOverhead2048java.lang.OutOfMemoryError: MetaspaceJVM Metaspace 不足常见于大量 UDFjstat -gc pid加--conf spark.driver.extraJavaOptions-XX:MaxMetaspaceSize512mpyspark.sql.utils.AnalysisException: Path does not existspark.read路径是 executor 本地路径非 driverprint(spark.sparkContext.parallelize([1]).collect())改用spark.read.text(file:///absolute/path)强制本地文件系统Connection refused: localhost/127.0.0.1:7077standalone master 未启动或端口被占netstat -tuln | grep 7077./sbin/start-master.sh检查SPARK_MASTER_HOST是否为127.0.0.1ModuleNotFoundError: No module named pandasexecutor 未安装 pandasspark-submit --py-files未打包依赖用--archives打包 conda env--archives /path/to/env.zip#environmentjava.net.UnknownHostException: namenodespark.yarn.jars指向 HDFS但core-site.xml未配置hadoop fs -ls hdfs://namenode:8020/将HADOOP_CONF_DIR指向含core-site.xml的目录spark-shell启动后卡住无提示Py4J gateway server 启动超时jps -l | grep SparkSubmit加--conf spark.network.timeout10000000延长超时java.lang.NoClassDefFoundError: scala/Function1Scala 版本冲突混入旧版scala-library.jarfind $SPARK_HOME/jars -name scala-library*.jar删除所有scala-library-2.11.*.jar只留2.12.*spark-sqlCLI 报Failed to load Hive dependencies未启用 Hive 支持但spark.sql.catalogImplementation为 hivespark-sql --conf spark.sql.catalogImplementationin-memory在spark-defaults.conf中设spark.sql.catalogImplementation hive仅当真用 Hive4.1 深度案例解决spark-submit在 Docker 中的NoClassDefFoundError场景将 Spark 作业打包进 Docker 镜像docker run启动后报Exception in thread main java.lang.NoClassDefFoundError: scala/Product at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala) Caused by: java.lang.ClassNotFoundException: scala.Product排查过程进入容器docker exec -it container_id /bin/bash检查$SPARK_HOME/jars/ls -l $SPARK_HOME/jars/scala-library*.jar发现存在scala-library-2.11.12.jar和scala-library-2.12.15.jar查看spark-submit启动脚本cat $SPARK_HOME/bin/spark-submit找到build_classpath函数其for循环遍历$SPARK_HOME/jars/下所有 jar按字母序加载scala-library-2.11.12.jar字母序在2.12.15之前被优先加入 classpath导致 Spark 3.3.0需 Scala 2.12加载了旧版 Scala 类。修复方案# Dockerfile 中添加清理步骤 RUN rm $SPARK_HOME/jars/scala-library-2.11.*.jar # 并显式设置 CLASSPATH ENV CLASSPATH$SPARK_HOME/jars/scala-library-2.12.15.jar:$SPARK_HOME/jars/*这个案例说明环境搭建不是静态配置而是动态加载顺序的博弈。每个 jar 的文件名、路径、环境变量都在参与 classpath 的构建竞赛。4.2 实操避坑清单5 条血泪教训不要修改$SPARK_HOME/conf/spark-defaults.conf的spark.master该文件是全局默认若设为yarn则pysparkCLI 也会尝试连 YARN导致本地开发失败。正确做法是在代码中builder.master(local[2])或提交时--master local[2]显式指定。spark.driver.memory不是给 Python 用的它分配给 JVM 的 heapPython 对象存储在 JVM 外的 native memorypyspark中rdd.map(lambda x: big_numpy_array)仍会 OOM需用spark.sql.adaptive.enabledtrue启用自适应查询执行。--jars参数的路径必须是 driver 可访问的若传--jars hdfs://.../my-udf.jardriver 会从 HDFS 下载到本地临时目录再分发给 executor若传--jars /tmp/my-udf.jarexecutor 因无/tmp/权限而失败应改用--files。spark.sql.adaptive.enabledtrue在 local 模式下无效该特性依赖AQEShuffleManager仅在 cluster 模式YARN/K8s下激活本地测试时关闭它更稳定。spark-submit的--conf优先级高于spark-defaults.conf若spark-defaults.conf设spark.sql.adaptive.enabledfalse但提交时加--conf spark.sql.adaptive.enabledtrue后者生效。调试时用--conf spark.debug.maxToStringFields100可查看完整异常栈。5. 进阶扩展如何将环境搭建纳入 CI/CD 流水线环境搭建的终极形态是让每次git push都自动验证 Spark 环境的可复现性。以 GitHub Actions 为例.github/workflows/spark-test.ymlname: Spark Environment Test on: [push, pull_request] jobs: test-spark: runs-on: ubuntu-22.04 steps: - uses: actions/checkoutv3 - name: Install JDK 11 uses: actions/setup-javav3 with: java-version: 11 distribution: temurin - name: Download Spark 3.3.0 run: | wget https://downloads.apache.org/spark/spark-3.3.0/spark-3.3.0-bin-hadoop3.tgz tar -xzf spark-3.3.0-bin-hadoop3.tgz echo SPARK_HOME$(pwd)/spark-3.3.0-bin-hadoop3 $GITHUB_ENV - name: Setup Python uses: conda-incubator/setup-minicondav2 with: python-version: 3.9 - name: Install PySpark run: pip install --no-deps pyspark3.3.0 - name: Run WordCount Test run: | export PYSPARK_PYTHON$(which python) $SPARK_HOME/bin/spark-submit \ --master local[2] \ --driver-memory 1g \ examples/src/main/python/pi.py 10这个流水线的价值在于每次 PR 都验证 JDK/Spark/Python 三者能否协同工作若 Spark 官网更新了spark-3.3.0-bin-hadoop3.tgz的 SHA256CI 会因下载内容变化而失败提醒你检查上游变更examples/src/main/python/pi.py是 Spark 源码自带的集成测试比自定义脚本更权威。我个人在实际操作中的体会是环境搭建没有“一劳永逸”只有“持续验证”。客户环境、云厂商镜像、CI runner 的基础镜像都在不断更新。把spark-submit --master local[2] examples/src/main/python/pi.py写成一行 shell 脚本放在项目根目录test-env.sh每次部署前执行它比任何文档都可靠。这个脚本跑通了说明你的环境契约已建立它失败了说明某个环节的版本锁被打破了——这时你要做的不是重装而是打开jps、ps aux、ls $SPARK_HOME/jars/像侦探一样追踪那个被悄悄替换的 jar。