一、环境
Spark、Hadoop环境搭建可参看之前文章。
开发环境:
系统:Win10
开发工具:scala-eclipse-IDE
项目管理工具:Maven 3.6.0
JDK 1.8
Scala 2.11.11
Spark 2.4.3
Spark运行环境:
系统:Linux CentOS7(两台机:主从节点)
master : 192.168.190.200
slave1 : 192.168.190.201
JDK 1.8
Hadoop 2.9.2
Scala 2.11.11
Spark 2.4.3
二、案例简介
1. 两个记录文件:name_addr.txt 和 name_phone.txt (每条记录的两个字段用空格分隔)
name_addr.txt 中的字段:name addr
name_phone.txt 中的字段:name phone
2. 实现功能:
1)两个文件记录按name字段实现 Join 连接,(name, addr, phone);
2)对name_addr.txt文件记录实现 聚合,统计每个地址所在邮编的总人数。
三、流程图
依赖解析:
1. 由于Spark是惰性的,其代码的执行顺序并非我们常规的顺序执行,而是在Action操作处开始:
1)Action操作处提交Job作业(此处属于finalStage,即整个作业的最后一个阶段)前,若果有上级Stage,先提交上级Stage
2)如此向上递归,直到没有所有上级Stage全都提交之后,最后提交finalStage,开始执行整个作业。
2. 在本例中,按照Spark Standalone模式的资源调度模式(先进先出FIFO):
先由saveAsTextFile(即join的这个流程)操作提交Job作业,执行完毕;
再由collect(即reduceByKey这个聚合流程)操作提交Job作业,执行。
3. 两个Job流程各阶段RDD间依赖关系(向上依赖):
1)Join流程的依赖关系:2个RDD读入 <- RDD1+RDD2 <- RDD3 <- RDD4 <- saveAsTextFile
2)聚合流程的依赖关系:1个RDD读入 <- RDD1 <- RDD5 <- RDD6 <- collect
5. 从中可看出,RDD读入name_addr.txt <- RDD1 这个转换(transformation)操作在两个流程中都执行了一遍,即重复执行了;
解决方式:对RDD1 使用cache持久化到内存,切断依赖链,再次使用时,无需重新计算。
四、代码(Maven项目:rddOperation)
1. 配置 pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com</groupId><!-- 组织名 -->
<artifactId>rddOperation</artifactId><!-- 项目名 -->
<version>0.1</version><!-- 版本号 -->
<dependencies>
<dependency><!-- Spark依赖包 -->
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.4.3</version>
<scope>provided</scope><!-- 运行时提供,打包不添加,Spark集群已自带 -->
</dependency>
<dependency><!-- Log 日志依赖包 -->
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency><!-- 日志依赖接口 -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 混合scala/java编译 -->
<plugin><!-- scala编译插件 -->
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<phase>compile</phase>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>testCompile</goal>
</goals>
<phase>test-compile</phase>
</execution>
<execution>
<phase>process-resources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source><!-- 设置Java源 -->
<target>1.8</target>
</configuration>
</plugin>
<!-- for fatjar -->
<plugin><!-- 将所有依赖包打入同一个jar包中 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<!-- 添加类路径 -->
<addClasspath>true</addClasspath>
<!-- 设置程序的入口类 -->
<mainClass>sparkstreaming_action.rdd.operation.RDDOperation</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
2. 主程序:
1)输入输出路径采用HDFS分布式文件系统的路径:"hdfs://master:9000/user/spark/rddoperation/"
package sparkstreaming_action.rdd.operation
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
//单例对象RDDOperation混入App特质,可直接运行
object RDDOperation extends App{
//Spark配置项
val conf = new SparkConf()
.setAppName("rddOperation")
.setMaster("spark://master:7077")
//创建Spark上下文
val sc = new SparkContext(conf)
//HDFS文件路径:存放输入输出文件的根目录
val IO_PATH_HDFS = "hdfs://master:9000/user/spark/rddoperation/"
//文件名
val txtNameAddr = IO_PATH_HDFS + "input/name_addr.txt"
val txtNamePhone = IO_PATH_HDFS + "input/name_phone.txt"
//读入用户地址文件,并按照格式切分,得到对应的RDD
val rddNameAddr = sc.textFile(txtNameAddr).map(record => {
//实现每条记录的处理函数
val tokens = record.split(" ") //按空格分隔
(tokens(0), tokens(1)) //返回格式:(name, addr)
}) //RDD1
rddNameAddr.cache()
//读入用户电话文件,切分的后得到对应的RDD
val rddNamePhone = sc.textFile(txtNamePhone).map(record => {
val tokens = record.split(" ")
(tokens(0), tokens(1))
}) //RDD2
//rddNamePhone.cache()
//以用户名的key进行join操作(默认按key)
val rddNameAddrPhone = rddNameAddr.join(rddNamePhone) //RDD3
//对用户电话RDD进行HTML格式变化,产生新的RDD
val rddHtml = rddNameAddrPhone.map(record => {
//join后的数据集格式为:(name, (addr, phone))
val name = record._1
val addr = record._2._1
val phone = record._2._2
s"<h2>姓名:${name}</h2><p>地址:${addr}</p><p>电话:${phone}</p>"
}) //RDD4
//输出操作
val rddOutPut = rddHtml.saveAsTextFile(IO_PATH_HDFS + "output/UserInfo")
//根据地址格式得到邮编RDD
val rddPostcode = rddNameAddr.map(record => {
val postcode = record._2.split("#")(1)
(postcode, 1)
}) //RDD5
//汇总邮编出现次数
val rddPostcodeCount = rddPostcode.reduceByKey(_ + _) //RDD6
//汇总结果到Driver节点,打印结果
rddPostcodeCount.collect().foreach(println)
sc.stop
}
五、打包运行
1.在项目的根目录下运行命令行窗口(在目录下 "shift+右键",选择命令行窗口 Power Shell)
执行如下命令:(编译代码)
> mvn install
编译成功后,会在当前目录的 ".\target\" 下产生两个jar包;
其中的 rddOperation-0.1-jar-with-dependencies.jar 用来提交给Spaek集群
2.上传输入文件至HDFS(默认开启Hadoop集群)
创建输入文件,单词用空格分隔:
$ mkdir /opt/rddoperation
$ vi /opt/rddoperation/name_addr.txt
bob shanghai#200000
amy beijing#100000
alice shanghai#200000
tom beijing#100000
lulu hangzhou#310000
nick shanghai#200000
lily nanjing#210000
$ vi /opt/rddoperation/name_phone.txt
bob 15700079421
amy 18700079458
alice 17730079427
tom 16700379451
lulu 18800074423
nick 14400033426
lucy 18900045781
上传至HDFS目录下:
$ hadoop fs -mkdir /user/spark
$ hadoop fs -mkdir /user/spark/rddoperation
$ hadoop fs -mkdir /user/spark/rddoperation/input
$ hadoop fs -put /opt/rddoperation/* /user/spark/rddoperation/input
3.将Jar包提交至主节点上,执行Spark作业:
提交Spark作业:(需先配置Spark_HOME环境变量)
$ spark-submit \
--class sparkstreaming_action.rdd.operation.RDDOperation \
/opt/rddOperation-0.1-jar-with-dependencies.jar
注1:其中每行的末尾 "\" 代表不换行,命令需在一行上输入,此处只为方便观看
注2:提交的Jar包放在 /opt/ 目录下
运行成功后,屏幕输出如下邮编地区人数统计结果:
(310000,1)
(200000,3)
(210000,1)
(100000,2)
4.查看Join结果:
查看join结果输出目录:
$ hadoop fs -ls /user/spark/rddoperation/output/UserInfo
/user/spark/rddoperation/output/UserInfo/_SUCCESS --代表作业成功(标识文件)
/user/spark/rddoperation/output/UserInfo/part-00000 --结果文件1
/user/spark/rddoperation/output/UserInfo/part-00001 --结果文件2
查看join结果(使用/*查看UserInfo/下所有文件):
$ hadoop fs -cat /user/spark/rddoperation/output/UserInfo/*
<h2>姓名:alice</h2><p>地址:shanghai#200000</p><p>电话:17730079427</p>
<h2>姓名:tom</h2><p>地址:beijing#100000</p><p>电话:16700379451</p>
<h2>姓名:lulu</h2><p>地址:hangzhou#310000</p><p>电话:18800074423</p>
<h2>姓名:nick</h2><p>地址:shanghai#200000</p><p>电话:14400033426</p>
<h2>姓名:amy</h2><p>地址:beijing#100000</p><p>电话:18700079458</p>
<h2>姓名:bob</h2><p>地址:shanghai#200000</p><p>电话:15700079421</p>
5.成功运行结束
六、参考文章
1. 《Spark Streaming 实时流式大数据处理实战》第三章 Spark编程模型
2. 《Spark 最佳实践》4.3 DAG调度
3. 《深入理解Spark》5.4 任务提交