Spark 2.4.0 + Hadoop 3.1.3 环境搭建与路径协议避坑指南

发布时间:2026/10/31 17:51:42
Spark 2.4.0 + Hadoop 3.1.3 环境搭建与路径协议避坑指南 简介本资源是《大数据技术原理与应用》课程配套的Spark初级编程实践实验报告面向高校大数据初学者及Hadoop/Spark入门学习者聚焦分布式计算框架的核心操作训练。内容覆盖Spark环境搭建Ubuntu虚拟机Hadoop 3.1.3SparkJDK 1.8、Spark Shell交互式数据读取本地文件与HDFS双路径统计行数、三个完整Scala独立应用程序开发SimpleApp行数统计、RemDup数据去重、AvgScore多文件平均分计算并附详细排错指南——针对常见IllegalArgumentException、InvalidInputException及URISyntaxException问题给出路径格式、用户名目录、空格字符等关键修复方案。资源为1个1.9MB的DOCX文档结构清晰含实验环境配置、分步截图、代码片段、运行结果与问题分析便于对照复现与理解原理。目前已有8349人学习下载是理论结合实操、兼顾部署调试与工程规范的优质入门实践材料。1. Spark初级编程实践别被file:///少一个斜杠坑哭一套能跑通的UbuntuHadoop3.1.3Spark2.4.0环境实录你是不是也经历过明明照着教程敲完sc.textFile(/home/hadoop/test.txt)一回车就弹出IllegalArgumentException: Path does not exist或者spark-submit死活找不到JAR包报错ClassNotFoundException翻遍日志却只看到一堆Scala版本号在打架这不是你手残——这是Spark初学者最典型的「路径玄学」和「版本黑匣子」现场。本篇记录的是某高校《大数据技术原理与应用》课程实验七的真实落地过程在Windows 10宿主机上用VirtualBox跑Ubuntu Kylin 16.04虚拟机完整复现Hadoop 3.1.3 Spark 2.4.0注意不是3.x JDK 1.8 sbt 1.2.8的闭环环境。这套组合不是随便选的——Hadoop 3.1.3默认关闭了对Java 9的支持而Spark 2.4.0是最后一个原生兼容Scala 2.11且对Hadoop 3.1.x做过充分适配的稳定版用错任何一个版本spark-shell能启动但sbt package打包后spark-submit必翻车。适合正在写实验报告、卡在InvalidInputException报错、或刚配好Hadoop却连本地文件都读不了的本科生和转行新人。下面所有命令、路径、配置项全部来自真实终端逐行验证不加一句虚的。2. 环境筑基Hadoop 3.1.3 Spark 2.4.0 JDK 1.8 三件套精准安装与路径固化提示本节所有操作均在Ubuntu Kylin 16.04虚拟机中以hadoop普通用户身份执行严禁使用root用户解压或配置。路径统一固化为/usr/local/这是避免后续spark-submit找不到Hadoop配置的核心前提。2.1 JDK 1.8 安装与JAVA_HOME硬编码校验Spark 2.4.0要求JDK 1.8u181及以上JDK 9会导致scala.reflect.internal.Symbols$CyclicReference类加载异常。先确认系统无冲突JDK# 检查已安装JDK卸载openjdk等干扰项 dpkg -l | grep jdk sudo apt-get purge openjdk-* # 下载官方tar.gz非.deb地址https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html # 假设下载到~/Downloads/jdk-8u202-linux-x64.tar.gz sudo mkdir -p /usr/lib/jvm sudo tar -zxvf ~/Downloads/jdk-8u202-linux-x64.tar.gz -C /usr/lib/jvm/ sudo chown -R root:root /usr/lib/jvm/jdk1.8.0_202关键一步环境变量必须写死绝对路径不能用~或$HOME。编辑~/.bashrc# 在文件末尾追加注意PATH顺序很重要java必须在最前 export JAVA_HOME/usr/lib/jvm/jdk1.8.0_202 export JRE_HOME${JAVA_HOME}/jre export CLASSPATH.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH${JAVA_HOME}/bin:$PATH立即生效并校验source ~/.bashrc java -version # 必须输出 java version 1.8.0_202 echo $JAVA_HOME # 必须输出 /usr/lib/jvm/jdk1.8.0_202无空格、无波浪线为什么强调$JAVA_HOME不能带~因为sbt在编译Scala项目时会调用javac若JAVA_HOME含~它会解析成字面量~/...而非真实路径导致编译生成的class文件无法被SparkClassLoader加载——这是ClassNotFoundException的根源之一。2.2 Hadoop 3.1.3 单机伪分布式部署与HDFS根路径确认Hadoop 3.1.3的core-site.xml中fs.defaultFS值决定了Spark读取HDFS的默认协议头。很多教程写hdfs://localhost:9000但在单机伪分布式下必须用hdfs://localhost:9000且HDFS根目录实际挂载在/usr/local/hadoop/dfs/name而非/user/hadoop——后者是HDFS上的用户目录不是文件系统根。下载解压Hadoopwget https://archive.apache.org/dist/hadoop/core/hadoop-3.1.3/hadoop-3.1.3.tar.gz sudo tar -zxvf hadoop-3.1.3.tar.gz -C /usr/local/ sudo mv /usr/local/hadoop-3.1.3 /usr/local/hadoop sudo chown -R hadoop:hadoop /usr/local/hadoop配置四文件路径全用绝对路径# 编辑 /usr/local/hadoop/etc/hadoop/core-site.xml configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value !-- 关键协议hostport无路径 -- /property /configuration# 编辑 /usr/local/hadoop/etc/hadoop/hdfs-site.xml configuration property namedfs.replication/name value1/value !-- 单机设为1 -- /property property namedfs.namenode.name.dir/name valuefile:/usr/local/hadoop/dfs/name/value !-- 绝对路径 -- /property property namedfs.datanode.data.dir/name valuefile:/usr/local/hadoop/dfs/data/value !-- 绝对路径 -- /property /configuration格式化NameNode并启动# 创建目录 mkdir -p /usr/local/hadoop/dfs/{name,data} # 格式化仅首次 /usr/local/hadoop/bin/hdfs namenode -format # 启动HDFS /usr/local/hadoop/sbin/start-dfs.sh # 验证jps应看到NameNode和DataNode进程 jps此时HDFS根路径是hdfs://localhost:9000/而/user/hadoop只是其下的一个子目录。Spark读取HDFS文件时路径必须以hdfs://开头且不能漏掉/。例如正确写法hdfs://localhost:9000/user/hadoop/test.txt错误写法/user/hadoop/test.txt会被Spark当作本地路径或hdfs://localhost:9000user/hadoop/test.txt少/。2.3 Spark 2.4.0 安装与Hadoop兼容性绑定Spark 2.4.0有多个Hadoop兼容版本必须选hadoop3.1后缀版。下载地址https://archive.apache.org/dist/spark/spark-2.4.0/spark-2.4.0-bin-hadoop3.1.tgzwget https://archive.apache.org/dist/spark/spark-2.4.0/spark-2.4.0-bin-hadoop3.1.tgz sudo tar -zxvf spark-2.4.0-bin-hadoop3.1.tgz -C /usr/local/ sudo mv /usr/local/spark-2.4.0-bin-hadoop3.1 /usr/local/spark sudo chown -R hadoop:hadoop /usr/local/spark关键配置让Spark知道Hadoop在哪。编辑/usr/local/spark/conf/spark-env.sh如不存在则复制模板cp /usr/local/spark/conf/spark-env.sh.template /usr/local/spark/conf/spark-env.sh # 编辑该文件添加 export JAVA_HOME/usr/lib/jvm/jdk1.8.0_202 export HADOOP_HOME/usr/local/hadoop export HADOOP_CONF_DIR$HADOOP_HOME/etc/hadoop export SPARK_DIST_CLASSPATH$($HADOOP_HOME/bin/hadoop classpath)SPARK_DIST_CLASSPATH这行是灵魂它把Hadoop的所有JAR包路径注入Spark ClassLoader。没有它spark-shell能启动但一读HDFS就报InvalidInputException: Filesystem hdfs://localhost:9000 not supported——因为Spark根本不知道Hadoop的FileSystem实现类在哪。验证Spark Shell/usr/local/spark/bin/spark-shell --master local[2] # 进入后执行 scala sc.version res0: String 2.4.0 // 确认版本 scala sc.parallelize(1 to 10).count() res1: Long 10 // 确认计算引擎正常3. 路径与协议避坑file:///、hdfs://、/user/hadoop三者关系彻底讲清3.1 本地文件读取file:///必须是三个斜杠且路径必须绝对Spark读本地文件的URI格式是file:///absolute/path/to/file。注意file://是协议/是根目录所以file:///file:// /。常见错误❌file:/home/hadoop/test.txt→ 少一个/Spark解析为相对路径报IllegalArgumentException❌/home/hadoop/test.txt→ 无协议头Spark默认当HDFS路径处理报InvalidInputException✅file:///home/hadoop/test.txt→ 正确三个/在spark-shell中验证// 创建测试文件 echo -e line1\nline2\nline3 /home/hadoop/test.txt // 读取并统计行数 val textFile sc.textFile(file:///home/hadoop/test.txt) textFile.count() // 输出 3血泪经验Ubuntu下/home/hadoop/是用户家目录但~在Spark URI中不展开。永远用file:///home/hadoop/...不要用file://~/...。3.2 HDFS文件读取hdfs://协议 完整路径 用户目录权限HDFS路径必须以hdfs://开头且包含完整的host:port和路径。/user/hadoop是HDFS上的标准用户目录由hdfs dfs -mkdir -p /user/hadoop创建不是Linux系统路径。上传文件到HDFS# 先确保HDFS已启动见2.2节 # 创建用户目录若不存在 /usr/local/hadoop/bin/hdfs dfs -mkdir -p /user/hadoop # 上传本地文件 /usr/local/hadoop/bin/hdfs dfs -put /home/hadoop/test.txt /user/hadoop/ # 查看是否上传成功 /usr/local/hadoop/bin/hdfs dfs -ls /user/hadoop/在spark-shell中读取// 注意必须写全hdfs://协议和端口 val hdfsFile sc.textFile(hdfs://localhost:9000/user/hadoop/test.txt) hdfsFile.count() // 输出 3为什么不是hdfs://localhost:9000//user/hadoop/test.txt因为HDFS路径规则和Linux一致//会被自动合并为/但多写易错官方文档明确要求单/。3.3 独立应用程序中的路径陷阱sbt编译时的URI解析与运行时差异Scala独立应用如SimpleApp.scala在sbt compile阶段不解析文件路径但spark-submit运行时会严格校验URI。常见坑空格陷阱val path hdfs://localhost:9000/user/hadoop/test.txt开头有空格→ 报URISyntaxException: Illegal character in scheme name at index 0拼写陷阱hdfs://localhsot:9000/...localhost拼错→ 连接超时报java.net.UnknownHostException协议陷阱hdfs:/localhost:9000/...少一个/→ 解析失败同URISyntaxExceptionSimpleApp.scala正确写法import org.apache.spark.sql.SparkSession object SimpleApp { def main(args: Array[String]) { val spark SparkSession.builder() .appName(Simple App) .master(local[2]) .getOrCreate() // 关键字符串字面量无空格、协议完整、路径准确 val inputPath hdfs://localhost:9000/user/hadoop/test.txt val lines spark.sparkContext.textFile(inputPath) println(sLines count: ${lines.count()}) spark.stop() } }3.4 常见问题排查5条真实踩坑记录与速查方案现象1spark-shell中sc.textFile(file:///home/hadoop/test.txt)报IllegalArgumentException: Path does not exist原因Linux路径权限问题。hadoop用户对/home/hadoop/test.txt有读权限但Spark运行时可能以其他用户身份访问尤其当spark-env.sh中JAVA_HOME指向root权限目录时。解决chmod 644 /home/hadoop/test.txt # 确保所有用户可读 ls -l /home/hadoop/test.txt # 确认属主是hadoop:hadoop现象2spark-submit运行JAR包报ClassNotFoundException: org.apache.hadoop.fs.FileSystem原因SPARK_DIST_CLASSPATH未正确设置或hadoop classpath命令输出为空。解决# 手动检查Hadoop classpath /usr/local/hadoop/bin/hadoop classpath # 若输出为空检查HADOOP_HOME是否指向正确目录且$HADOOP_HOME/etc/hadoop存在 # 重新source spark-env.sh source /usr/local/spark/conf/spark-env.sh echo $SPARK_DIST_CLASSPATH # 应包含大量hadoop-*.jar路径现象3sbt package成功但spark-submit报java.lang.NoClassDefFoundError: scala/Function1原因Scala版本不匹配。Spark 2.4.0编译于Scala 2.11而sbt默认可能用2.12。解决在simple.sbt中强制指定Scala版本name : SimpleApp version : 1.0 scalaVersion : 2.11.12 // 必须与Spark二进制包匹配 libraryDependencies org.apache.spark %% spark-sql % 2.4.0现象4spark-submit提交后任务卡在ACCEPTED状态YARN UI显示AM Container is launched, waiting for AM container to Register with RM原因本实验是单机伪分布式但spark-submit误用了YARN模式。解决强制指定--master local[2]/usr/local/spark/bin/spark-submit \ --class SimpleApp \ --master local[2] \ # 关键不是yarn /usr/local/spark/mycode/HDFStest/target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar现象5RemDup.scala合并文件后输出重复去重失效原因union()后未调用distinct()或map()转换时引入了不可见字符如Windows换行符\r\n。解决// 读取时过滤\r val rddA sc.textFile(file:///home/hadoop/A.txt).map(_.replaceAll(\r, )) val rddB sc.textFile(file:///home/hadoop/B.txt).map(_.replaceAll(\r, )) val result rddA.union(rddB).distinct() // distinct()必须显式调用4. 独立应用全流程从sbt打包到spark-submit运行的六步闭环4.1 工程目录结构标准化避免路径混乱的根基Spark独立应用必须遵循Maven/SBT标准布局否则sbt package无法识别源码。以RemDup为例正确结构/home/hadoop/sparkapp2/RemDup/ ├── src/ │ └── main/ │ ├── scala/ │ │ └── RemDup.scala # 主程序 │ └── resources/ # 配置文件本例无需 ├── simple.sbt # sbt构建配置 └── datas/ # 数据文件目录非src下 ├── A.txt └── B.txt为什么datas/不能放在src/main/resources/因为sbt package只打包src/main/scala下的代码resources用于配置文件。数据文件需在运行时通过绝对路径读取放src下会导致打包后路径错乱。4.2 simple.sbt配置详解依赖、Scala版本、打包插件三要素simple.sbt是sbt的构建脚本必须精确控制依赖版本。RemDup的完整配置name : RemoveDuplication version : 1.0 scalaVersion : 2.11.12 // 与Spark 2.4.0捆绑的Scala版本 // Spark核心依赖%表示只用于编译和测试不打包进JAR libraryDependencies Seq( org.apache.spark %% spark-core % 2.4.0 % provided, org.apache.spark %% spark-sql % 2.4.0 % provided ) // 打包插件确保生成的JAR包含所有依赖本例不需要因spark-submit会提供 // 但为防万一显式排除provided依赖 assemblyOption in assembly : (assemblyOption in assembly).value.copy(includeScala false)provided标记至关重要它告诉sbt这些依赖由运行环境即Spark提供编译时需要但不要打进JAR。否则会因版本冲突导致NoSuchMethodError。4.3 sbt打包命令与输出验证看懂成功标志进入工程根目录执行cd /home/hadoop/sparkapp2/RemDup /usr/local/sbt/sbt clean compile package成功标志注意关键词[info] Packaging /home/hadoop/sparkapp2/RemDup/target/scala-2.11/remove-duplication_2.11-1.0.jar ... [info] Done packaging.验证JAR内容确认无Spark类jar -tf /home/hadoop/sparkapp2/RemDup/target/scala-2.11/remove-duplication_2.11-1.0.jar | grep spark-core # 应无输出若有则sbt配置错误需检查provided标记4.4 spark-submit参数精解class、master、deploy-mode的取舍逻辑spark-submit是Spark应用的发布入口参数顺序和含义直接影响成败/usr/local/spark/bin/spark-submit \ --class RemDup \ # 主类名必须与Scala文件名、object名完全一致区分大小写 --master local[2] \ # 本地模式2个线程生产环境才用yarn或spark://... --deploy-mode client \ # client模式driver在提交节点运行便于调试日志 --name Remove Duplication Job \ # 任务名YARN UI中可见 /home/hadoop/sparkapp2/RemDup/target/scala-2.11/remove-duplication_2.11-1.0.jar--deploy-mode client是开发调试的黄金选项。若用clusterdriver日志会分散在YARN NodeManager上新手根本找不到报错位置。4.5 数据文件预处理Linux换行符与空格清洗影响去重结果输入文件A和B若从Windows复制会含\r\n导致20170101 x\r和20170101 x被视为不同行。清洗命令# 批量转换Windows换行符为Linux sed -i s/\r$// /home/hadoop/sparkapp2/RemDup/datas/A.txt sed -i s/\r$// /home/hadoop/sparkapp2/RemDup/datas/B.txt # 删除每行首尾空格 sed -i s/^[[:space:]]*//; s/[[:space:]]*$// /home/hadoop/sparkapp2/RemDup/datas/A.txt sed -i s/^[[:space:]]*//; s/[[:space:]]*$// /home/hadoop/sparkapp2/RemDup/datas/B.txt4.6 运行结果验证与输出重定向确保结果可复现spark-submit默认将println输出到终端但生产中需保存。修改RemDup.scala// 在result.saveAsTextFile(...)后添加 result.coalesce(1).saveAsTextFile(file:///home/hadoop/sparkapp2/RemDup/output/C.txt) // 注意saveAsTextFile必须用file://协议且路径存在创建输出目录并运行mkdir -p /home/hadoop/sparkapp2/RemDup/output /usr/local/spark/bin/spark-submit --class RemDup --master local[2] \ /home/hadoop/sparkapp2/RemDup/target/scala-2.11/remove-duplication_2.11-1.0.jar # 查看结果 cat /home/hadoop/sparkapp2/RemDup/output/C.txt/part-000005. 验证闭环四大场景全链路跑通与我的强制检查清单装完环境不是终点而是验证的开始。我给自己定了一套「Spark初级编程实践」强制检查清单每次新配环境或帮同学debug都逐项过一遍。这套清单覆盖了实验七全部四个核心场景且每个步骤都有明确的成功标志和失败回滚点。希望帮到你。5.1 场景一Spark Shell本地文件读取基础生存测试目标验证Spark计算引擎和本地文件系统打通。操作/usr/local/spark/bin/spark-shell --master local[2]进入后执行val test sc.parallelize(Array(a,b,c)) test.count() // 必须返回3 val localFile sc.textFile(file:///home/hadoop/test.txt) localFile.count() // 必须返回文件实际行数如3成功标志两个count()均返回预期数字无异常堆栈。失败回滚若localFile.count()报错立即检查/home/hadoop/test.txt权限ls -l和路径拼写file:///三个斜杠。我的习惯从那以后我每次配完Spark第一件事就是echo test /tmp/spark-test.txt sc.textFile(file:///tmp/spark-test.txt).count()5秒内出结果才算过关。5.2 场景二Spark Shell读取HDFS文件分布式存储验证目标验证Spark与Hadoop的网络和协议层联通。操作# 确保HDFS已启动jps看NameNode/DataNode /usr/local/hadoop/bin/hdfs dfs -ls /user/hadoop/ # 确认test.txt存在 /usr/local/spark/bin/spark-shell --master local[2]进入后执行val hdfsFile sc.textFile(hdfs://localhost:9000/user/hadoop/test.txt) hdfsFile.count() // 必须返回与本地文件相同的行数成功标志count()返回数字且与file:///方式结果一致。失败回滚若报InvalidInputException立即执行/usr/local/hadoop/bin/hdfs dfs -ls hdfs://localhost:9000/user/hadoop/确认HDFS服务可达且路径存在若ls失败则重启HDFSstop-dfs.sh start-dfs.sh。我的习惯从那以后我每次改core-site.xml必先用hdfs dfs -ls验证绝不直接跳到Spark。5.3 场景三独立应用SimpleApp打包与提交工程化能力验证目标验证sbt编译、JAR生成、spark-submit执行的全链路。操作cd /usr/local/spark/mycode/HDFStest /usr/local/sbt/sbt clean package # 检查JAR是否生成且无spark类 jar -tf target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar | head -5 # 提交 /usr/local/spark/bin/spark-submit \ --class SimpleApp \ --master local[2] \ target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar成功标志终端输出Lines count: XX为文件行数且无ClassNotFoundException。失败回滚若报NoClassDefFoundError立即检查sbt输出的Scala版本是否为2.11.12若报File not found检查SimpleApp.scala中路径是否为hdfs://localhost:9000/user/hadoop/test.txt。我的习惯从那以后我每次写完Scala代码必先sbt compile看是否报错再sbt package看JAR大小正常应100KB最后才spark-submit。5.4 场景四数据去重与平均值计算业务逻辑验证目标验证RDD转换union,distinct,map,reduceByKey的正确性。操作对RemDup和AvgScore分别执行# RemDup检查输出行数是否等于AB去重后总数 wc -l datas/A.txt datas/B.txt # 记录原始行数 /usr/local/spark/bin/spark-submit --class RemDup --master local[2] target/scala-2.11/remove-duplication_2.11-1.0.jar wc -l output/C.txt/part-00000 # 应小于AB之和 # AvgScore检查输出是否为浮点数且四舍五入到小数点后2位 /usr/local/spark/bin/spark-submit --class AvgScore --master local[2] target/scala-2.11/average-score_2.11-1.0.jar cat output/avg/part-00000 | head -5 # 应看到(小明,89.67)等格式成功标志C.txt行数符合数学去重预期avg输出为(姓名,XX.XX)格式无NaN或Infinity。失败回滚若AvgScore结果为0.0检查map中成绩是否被转为Int92.toInt而非字符串若去重后仍有重复检查distinct()是否在union()之后调用。我的习惯从那以后我每次写RDD转换必在spark-shell里用小数据集sc.parallelize(Seq(...))先试一遍逻辑再写进工程。这套清单不是一次性的而是我每次面对新环境、新同学提问、甚至自己半年后重装时都会打开终端逐行敲一遍的「后悔药」。它不保证你理解所有原理但能保证你写的每一行代码都在一个可验证、可回滚、可复现的轨道上运行。希望帮到你。本文还有配套的精品资源点击获取