
简介面向大数据课程实验七的 Spark 初级编程实践完整报告适合正在学习 Hadoop/Spark 环境搭建与 Scala 独立应用开发的本科学生。文档基于 Ubuntu Kylin 16.04 虚拟机、Hadoop 3.1.3、JDK 1.8 和 Spark 环境完整记录从安装启动到编程验证的过程包括通过 spark-shell 读取本地文件与 HDFS 文件并统计行数以及实现 SimpleApp、RemDup、AvgScore 三个 Scala 独立应用覆盖 sbt 打包、spark-submit 提交运行和结果输出。资源包为 1 个 docx 文档大小约 1.9MB正文包含实验环境、代码清单、运行截图及常见问题排查对提交课程实验报告或复习 Spark 入门操作有直接参考价值。已有 8345 人浏览学习适合按步骤对照复现。1. Spark初级编程实践最值钱的不是代码是三条文件路径一份Spark初级编程实践的实验记录通常被当成课程作业看待装好Hadoop和Spark启动spark-shell写几个Scala独立应用跑完交差。但这份实验记录真正值钱的部分藏在出现的问题那一节里——三个报错全部指向同一个大坑文件路径写错。file:///少打一个斜杠、把HDFS根目录当成~、URL前面多了个空格每一个都能让新手卡半小时以上。这篇文章把环境搭建、Spark读取本地与HDFS文件、数据去重、求平均值整条链路拆开讲保留完整可复现的代码和踩坑记录适合正在做类似课程实验、或者刚接触Spark想独立跑通一个Scala项目的开发者。2. 环境安装与Scala项目骨架sbt spark-submit 这套组合为什么绕不开2.1 版本搭配Hadoop 3.1.3 配 JDK 1.8 是最省事的组合先看清楚这份实验的运行底座主机Windows 10虚拟机Ubuntu 16.04ubuntukylin上面装Hadoop 3.1.3和SparkJDK用1.8。这套组合是很多高校大数据课程的标准配置。JDK必须用1.8因为Spark 2.x依赖Java 8的运行时特性装更高版本反而会碰到模块化访问限制之类的兼容问题。装Hadoop和Spark的路径要固定下来后续所有命令都依赖这两个位置。常见布局是# 解压到固定路径 tar -zxvf hadoop-3.1.3.tar.gz -C /usr/local tar -zxvf spark-*.tgz -C /usr/local # 重命名方便管理 mv /usr/local/hadoop-3.1.3 /usr/local/hadoop mv /usr/local/spark-* /usr/local/spark # 配置环境变量写入 ~/.bashrc export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME/usr/local/hadoop export SPARK_HOME/usr/local/spark export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$SPARK_HOME/binsource ~/.bashrc之后用hadoop version和spark-shell --version验证变量是否生效。注意Spark本身不强制依赖Hadoop启动但读写HDFS时必须能找到Hadoop配置。所以实验里Spark的HDFS访问依赖Hadoop的core-site.xml和hdfs-site.xml这两个文件不配好后续sc.textFile(hdfs://...)会报错找不到文件系统。2.2 用 ./bin/spark-shell 验证安装但别只把它当验证工具安装完成后实验记录里的第一个命令是cd /usr/local/spark ./bin/spark-shell看到一堆日志后进入scala提示符就算成功了。spark-shell本质是Spark的交互式客户端它启动的时候会自动创建SparkContext对象sc所以你不需要自己写new SparkContext(conf)就能直接调sc.textFile。新手很容易把spark-shell当成命令行工具来用但它真正的价值是快速验证一段数据处理的逻辑对不对——比如你想确认某个文件路径写法是否合法、某种RDD转换是否报错先在spark-shell里敲一遍比写了完整应用再打包提交快得多。我自己做开发时凡是涉及新路径、新数据源一定先在spark-shell里做冒烟测试。2.3 sbt 最小配置三行必填项之外还有一个关键参数独立应用必须用sbt打包这是Spark官方推荐的Scala构建方式。实验里simple.sbt的内容是最小配置我把它拆开解释name : SimpleProject version : 1.0 scalaVersion : 2.11.8 libraryDependencies org.apache.spark %% spark-core % 2.1.0name决定JAR包文件名前缀scalaVersion必须和Spark编译时用的Scala版本一致比如Spark 2.x通常配Scala 2.11版本不一致会出现NoSuchMethodErrorlibraryDependencies里的%%表示sbt会自动追加Scala版本号到依赖坐标比如实际解析成spark-core_2.11。提示spark-core的版本号应该和你安装的Spark版本严格对应。实验用的Spark如果解压后目录名里带版本号比如spark-2.1.0-bin-hadoop2.7那么这里就写2.1.0。版本对不上打包能过运行时会抛ClassNotFoundException。代码目录结构必须是sbt标准布局项目根目录/ ├── simple.sbt └── src/ └── main/ └── scala/ └── SimpleApp.scala在项目根目录执行/usr/local/sbt/sbt package执行完会在target/scala-2.11/下生成JAR包。sbt首次运行会下载依赖慢是正常的耐心等。如果卡住检查网络或者换成国内镜像仓库。2.4 spark-submit 提交参数--class 后面跟的是全限定类名打包之后用spark-submit提交/usr/local/spark/bin/spark-submit --class SimpleApp \ /usr/local/spark/mycode/HDFStest/target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar--class后面是Scala对象名对应代码里的object SimpleApp注意不带.scala后缀也不带包名时直接写类名。JAR包路径要写成实际生成的文件名如果simple.sbt里的name定义成了a-simple-hdfs-test那么JAR就是a-simple-hdfs-test_2.11-1.0.jar。这里有个容易翻车的地方spark-submit执行时当前Shell会把命令行里的路径做一次转义路径里如果混入空格或者引号没配对提交后报的错误会非常难排查。实验记录里URISyntaxException那条就是这么来的后面避坑章节细说。3. 数据读取与行数统计本地文件和HDFS的两条路径差异3.1 读取本地文件file:/// 三个斜杠一个都不能少打开spark-shell读取Linux本地文件并统计行数正确写法如下val textFile sc.textFile(file:///home/hadoop/test.txt) textFile.count()这里file:///是三个斜杠协议是file后面跟绝对路径。很多人写成file://home/hadoop/test.txt两个斜杠或直接写/home/hadoop/test.txt都会出问题。直接写绝对路径时Spark默认把它当HDFS路径处理本地模式下有时能蒙混过关但如果你同时配了Hadoop集群配置就会被解析成hdfs://localhost:9000/home/hadoop/test.txt文件不存在就报错。实验里还出现了一个特例sc.textFile(file:///usr/local/hadoop/1.txt)这个写法和你当前登录用户无关只要这个文件对当前进程有读权限就能读。用户是hadoop文件在hadoop用户目录下权限天然满足。3.2 读取HDFS文件根目录不是 ~而是 /user/hadoopHDFS的读取先要保证文件已经上传。先在Linux终端准备数据echo 这是第一行 test.txt echo 这是第二行 test.txt hdfs dfs -mkdir -p /user/hadoop hdfs dfs -put test.txt /user/hadoop/test.txt上传后建议先用hdfs dfs -ls /user/hadoop确认文件真的存在再进spark-shellval hdfsFile sc.textFile(/user/hadoop/test.txt) hdfsFile.count()这里的路径为什么不是~因为~是Shell层面的概念展开后是/home/hadoop而HDFS是独立文件系统它的根目录是/。对于Hadoop用户名hadoopHDFS默认用户目录是/user/hadoop。如果直接用sc.textFile(~/test.txt)Spark根本不会做Shell式展开它会原样把~/test.txt当成路径去NameNode查询结果自然是InvalidInputException: Input path does not exist。如果HDFS的默认块大小、副本数都配好了也可以写完整HDFS协议路径val hdfsFile sc.textFile(hdfs://localhost:9000/user/hadoop/test.txt)端口号要和core-site.xml里fs.defaultFS的一致默认单机伪分布式是hdfs://localhost:9000。3.3 独立应用读取HDFSSimpleApp 从源码到运行全流程spark-shell里适合做验证但正式交付要用独立应用。SimpleApp.scala的完整代码如下import org.apache.spark.SparkContext import org.apache.spark.SparkContext._ import org.apache.spark.SparkConf object SimpleApp { def main(args: Array[String]) { val conf new SparkConf().setAppName(Simple Application) val sc new SparkContext(conf) val textFile sc.textFile(hdfs://localhost:9000/user/hadoop/test.txt) println(文件行数 textFile.count()) sc.stop() } }代码逻辑拆解第1步创建SparkConf并设置应用名这个名字会显示在Spark Web UI的Application列表里方便查看运行状态和日志第2步基于conf创建SparkContext它是所有RDD操作的入口第3步用hdfs://localhost:9000前缀显式指定文件系统避免依赖默认配置第4步count()是一个行动算子触发真正的分布式计算。注意sc.stop()必须写在最后。不释放SparkContext任务结束后JVM不会退出spark-submit会一直挂在那里。配套的simple.sbt如下name : a-simple-hdfs-test version : 1.0 scalaVersion : 2.11.8 libraryDependencies org.apache.spark %% spark-core % 2.1.0打包提交命令和上一章一致。运行成功后终端会输出文件行数4这里的4是实验里test.txt的实际行数。3.4 三种路径写法对比到底什么时候用哪种整理成一张表遇到路径问题先查这张表写法适用场景常见错误file:///home/hadoop/test.txt读取Linux本地文件少写一个/报IllegalArgumentException/user/hadoop/test.txt读取HDFS文件走默认配置误用~/报InvalidInputExceptionhdfs://localhost:9000/user/hadoop/test.txt显式指定HDFS协议和端口端口号不匹配报连接失败/home/hadoop/test.txtSpark可同时访问本地和HDFS默认解析逻辑取决于配置配置了HDFS后会被解析到HDFS路径最后一种写法最迷惑人sc.textFile(/home/hadoop/test.txt)在没装Hadoop的机器上读本地文件没问题但一旦Spark知道HDFS地址同样的代码就会去HDFS找路径行为完全变了。所以我的习惯是本地文件永远带file:///前缀HDFS文件永远写/user/开头的完整路径不做任何省略。4. 数据去重与平均值计算两个独立应用带你走通完整开发流程4.1 文件合并去重union 和 distinct 的配合数据去重的需求很典型文件A和文件B各有若干行日期 事件记录合并后去掉完全重复的行。输入样例里A有6行、B有5行合并后共11行其中20170102 y和20170103 x在两文件里都有去重后剩9行。RemDup.scala的完整实现import org.apache.spark.SparkContext import org.apache.spark.SparkContext._ import org.apache.spark.SparkConf object RemDup { def main(args: Array[String]) { val conf new SparkConf().setAppName(Remove Duplication) val sc new SparkContext(conf) val fileA sc.textFile(file:///home/hadoop/A.txt) val fileB sc.textFile(file:///home/hadoop/B.txt) val lines fileA.union(fileB) val distinctLines lines.distinct() distinctLines.saveAsTextFile(file:///home/hadoop/C_result) sc.stop() } }逐行说逻辑textFile读入后每个RDD的元素就是一行字符串union把两个RDD纵向拼接成10个元素的RDD只做合并不做任何去重distinct()对全部行做shuffle去重把完全相同的文本行去掉saveAsTextFile把结果写入目标目录。saveAsTextFile有一个非常隐蔽的坑它要求目标目录在HDFS上不存在或者本地文件系统目录不存在。如果C_result已经存在Spark会直接抛FileAlreadyExistsException。所以每次重跑之前必须先手工删除旧输出目录rm -rf /home/hadoop/C_result如果输出是写到HDFS的/user/hadoop/C_result删除命令就换成hdfs dfs -rm -r /user/hadoop/C_result。提到输出结果的排序问题使用distinct()之后数据的顺序是无保证的。实验样例里的输出看起来是按日期排列的那是因为输入文件本身有序union之后shuffle量小碰巧保留了大致顺序。如果你需要精确的排序结果必须在distinct()之后加一次sortByval sortedLines distinctLines.sortBy(line line.split( )(0))4.2 求平均值groupByKey 和 mapValues 的分组聚合写法平均值案例是三个文件Algorithm、Database、Python每行格式都是姓名 成绩。要求输出每个学生的平均成绩并保留两位小数。先把数据文件放进同一个目录mkdir -p /home/hadoop/datas echo 小明 92 /home/hadoop/datas/Algorithm.txt echo 小红 87 /home/hadoop/datas/Algorithm.txt # 其他成绩行省略AvgScore.scala完整代码import org.apache.spark.SparkContext import org.apache.spark.SparkContext._ import org.apache.spark.SparkConf object AvgScore { def main(args: Array[String]) { val conf new SparkConf().setAppName(Average Score) val sc new SparkContext(conf) val data sc.textFile(file:///home/hadoop/datas) val lines data.map(line line.split( )) val scores lines.map(arr (arr(0), arr(1).toInt)) val avgScores scores.groupByKey().mapValues(iter { val sum iter.sum val count iter.size math.round(sum.toDouble / count * 100.0) / 100.0 }) val result avgScores.map(x ( x._1 , x._2 )) result.saveAsTextFile(file:///home/hadoop/avg_result) sc.stop() } }数据处理链路拆开讲textFile读取目录时Spark会把datas目录下的所有文件作为输入每个文件的行都会进入同一个RDD所以不需要手动union三个文件第一段map用空格split切出两个字段第二段map把成绩字段从字符串转成Int同时构造出(name, score)键值对groupByKey()把相同姓名的所有成绩归并到一个迭代器mapValues对整个成绩序列求和、计数并计算平均值。保留两位小数的写法有讲究。math.round(x * 100.0) / 100.0在Scala里返回的是Double比如83.6666会变成83.67但要注意它做的是四舍五入而不是截断。如果想严格用截断模式可以用BigDecimal的setScale(2, BigDecimal.RoundingMode.HALF_UP)。另一个细节sc.textFile读目录而不指定具体文件时Spark只认这个目录下的文件不递归读子目录。如果你的成绩数据分散在嵌套目录里要用data.map配合通配符或者改用wholeTextFiles。4.3 两个独立应用的共性套路读-转-聚合-写走到这一步能发现Spark独立应用的骨架高度统一创建上下文读入数据用map逐行解析成结构化键值对再用groupByKey或reduceByKey这类聚合算子完成核心逻辑最后saveAsTextFile落盘。这个套路覆盖了大部分课程实验和入门项目。reduceByKey和groupByKey的选择值得多说一句。实验里用groupByKey实现平均值性能上有隐患同一个key的所有value会被全部拉到同一个节点如果某位学生的成绩记录特别多可能出现数据倾斜。对平均值这种可以先局部求和再全局合并的场景combineByKey和reduceByKey是更好的选择。不过课程实验的数据量小groupByKey逻辑直观优先保证跑通优化留到真实场景。5. 避坑手册三个真实异常的排查记录5.1 IllegalArgumentExceptionfile:/// 少打了一个斜杠现象在spark-shell里用sc.textFile(file:///home/hadoop/test.txt)统计行数结果报IllegalArgumentException异常信息指向URI解析失败。原因把file:///写成了file://。file://这种写法在URI标准里表示协议后跟一个空authoritySpark解析时会认为URI格式不合法。实验记录里当事人写的错误路径是file:///usr/local/hadoop/1.txt少了一个斜杠实际正确写法是file:///usr/local/hadoop/1.txt三个斜杠里第三个是根目录/的开始。解决数清楚斜杠数量。file:///home/hadoop/test.txt从右往左拆解file:是协议//是分隔符/home/hadoop/test.txt是绝对路径所以协议后一共是三个斜杠。不放心的话在Spark Shell里先打印路径确认val p new java.net.URI(file:///home/hadoop/test.txt) println(p.getPath)如果输出的是/home/hadoop/test.txt说明解析正确如果报错或者getPath和预期不符就是斜杠数量有问题。5.2 InvalidInputExceptionHDFS 根目录不是 ~现象读取HDFS文件时执行sc.textFile(~/test.txt)或sc.textFile(hdfs://localhost:9000/~/test.txt)报InvalidInputException提示Input path does not exist。原因把Linux Shell里的~符号直接套在了HDFS路径上。~是Bash的展开语法HDFS的路径解析器不认识它。正确路径应该是/user/hadoop/test.txt因为Hadoop用户hadoop在HDFS上的home目录就是/user/hadoop而这个/user/hadoop和Linux的/home/hadoop完全无关它是HDFS命名空间里的一个逻辑目录。解决先确认文件在HDFS的真实位置再写路径。hdfs dfs -ls /user/hadoop/看输出结果里有没有test.txt。同时可以用hdfs dfs -pwd查看当前HDFS路径不过新版Hadoop里这个命令改名了最保险的做法是hdfs dfs -ls /一级一级往里找。找到真实路径后改写Spark代码里的路径参数。5.3 URISyntaxException路径里藏了一个空格现象运行RemDup或AvgScore的JAR包时报java.net.URISyntaxException: Illegal character in scheme name at index 0然后定位到textFile调用那一行。原因这一条最隐蔽。异常信息里的at index 0说明URI字符串的第一个字符就非法——实际原因是路径字符串最前面有一个不可见或可见的空格导致Spark把hdfs://...整体当成一个scheme去解析。这通常发生在复制粘贴代码时引号前不小心带了空格或者命令行传参时URL前多敲了一个空格。解决肉眼检查代码里所有字符串常量前后有没有多余空白特别是在textFile(...)的引号内。用编辑器开启显示空白字符功能最直观。我在自己的开发环境里配置了保存时自动去掉行尾空格这类问题就被直接消灭了。5.4 三类问题背后的通用排查规律这三个异常放在一起看本质都是URI/路径字符串不符合Spark解析器的预期格式。排查顺序总结如下第1步用println把传给textFile的完整字符串打出来肉眼核对有没有空格、斜杠数量、协议前缀第2步确认目标文件在对应文件系统里真实存在本地用ls -l、HDFS用hdfs dfs -ls第3步如果报错信息里有at index N的数字直接数那一位是什么字符往往就是答案。6. 验证结果与提交习惯每次跑完Spark作业后我必做的三件事6.1 用系统命令交叉验证Spark输出Spark的count()结果到底准不准我从第1次实验开始就不敢全信每次都用Linux原生命令交叉验证。本地文件用wc -l /home/hadoop/test.txtHDFS文件用hdfs dfs -cat /user/hadoop/test.txt | wc -l两个数字对得上才敢继续往下走。去重和平均值任务直接查看输出目录里的part文件cat /home/hadoop/C_result/part-00000 cat /home/hadoop/avg_result/part-00000看到的结果如果和预期一致这个作业才算真正跑通。6.2 输出目录的先删后跑习惯saveAsTextFile不能覆盖已存在目录这个限制让很多人第二次运行同一个JAR包时直接翻车。我养成的习惯是在每条spark-submit命令前加一个删除命令rm -rf /home/hadoop/C_result /usr/local/spark/bin/spark-submit --class RemDup \ /home/hadoop/sparkapp2/RemDup/target/scala-2.11/remove-duplication_2.11-1.0.jar这样保证了无论上一次运行结果是成功还是失败输出目录一定是干净的。6.3 提交前先走一遍路径三连查写这份实验记录的人踩了三次路径坑而真实的项目环境里路径问题出现的概率比这还高。从那以后我每次执行spark-submit之前都会强制自己走一遍三连查第一步查输入路径本地文件用ls、HDFS文件用hdfs dfs -ls确认文件在第二步查输出路径确认目录不存在或者已删除第三步查提交命令里的JAR包路径直接复制target/scala-2.11/目录下的实际文件名不手敲。三连查全程不超过30秒但帮我省下过无数次定位为什么找不到文件的远程调试。希望帮到你。本文还有配套的精品资源点击获取