Spark 2.4.0 + Hadoop 3.1.3 环境搭建与路径协议避坑指南
简介:本资源是《大数据技术原理与应用》课程配套的Spark初级编程实践实验报告,面向高校大数据初学者及Hadoop/Spark入门学习者,聚焦分布式计算框架的核心操作训练。内容覆盖Spark环境搭建(Ubuntu虚拟机+Hadoop 3.1.3+Spark+JDK 1.8)、Spark Shell交互式数据读取(本地文件与HDFS双路径统计行数)、三个完整Scala独立应用程序开发(SimpleApp行数统计、RemDup数据去重、AvgScore多文件平均分计算),并附详细排错指南——针对常见IllegalArgumentException、InvalidInputException及URISyntaxException问题,给出路径格式、用户名目录、空格字符等关键修复方案。资源为1个1.9MB的DOCX文档,结构清晰,含实验环境配置、分步截图、代码片段、运行结果与问题分析,便于对照复现与理解原理。目前已有8349人学习下载,是理论结合实操、兼顾部署调试与工程规范的优质入门实践材料。
1. Spark初级编程实践:别被file:///少一个斜杠坑哭,一套能跑通的Ubuntu+Hadoop3.1.3+Spark2.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.8(u181及以上),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上的用户目录,不是文件系统根。
下载解压Hadoop:
wget 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> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> <!-- 关键:协议+host+port,无路径 --> </property> </configuration># 编辑 /usr/local/hadoop/etc/hadoop/hdfs-site.xml <configuration> <property> <name>dfs.replication</name> <value>1</value> <!-- 单机设为1 --> </property> <property> <name>dfs.namenode.name.dir</name> <value>file:/usr/local/hadoop/dfs/name</value> <!-- 绝对路径! --> </property> <property> <name>dfs.datanode.data.dir</name> <value>file:/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.tgz
wget 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/..."(少一个/)→ 解析失败,同URISyntaxException
SimpleApp.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(s"Lines count: ${lines.count()}") spark.stop() } }3.4 常见问题排查:5条真实踩坑记录与速查方案
现象1:spark-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现象2:spark-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路径现象3:sbt 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"现象4:spark-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现象5:RemDup.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是开发调试的黄金选项。若用cluster,driver日志会分散在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失败,则重启HDFS(stop-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: X(X为文件行数),且无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:检查输出行数是否等于A+B去重后总数 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 # 应小于A+B之和 # 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中成绩是否被转为Int("92".toInt),而非字符串;若去重后仍有重复,检查distinct()是否在union()之后调用。
我的习惯:从那以后我每次写RDD转换,必在spark-shell里用小数据集(sc.parallelize(Seq(...)))先试一遍逻辑,再写进工程。
这套清单不是一次性的,而是我每次面对新环境、新同学提问、甚至自己半年后重装时,都会打开终端逐行敲一遍的「后悔药」。它不保证你理解所有原理,但能保证你写的每一行代码,都在一个可验证、可回滚、可复现的轨道上运行。希望帮到你。
本文还有配套的精品资源,点击获取