前期准备:
- 搭建好hadoop和spark集群
- 掌握SparkCore知识
- 掌握SparkSQL知识
- 准备好IDEA开发环境
内容概要
本次课是SparkStreaming的第一次课,主要讲解SparkStreaming的运行原理,常用算子的使用。
课程目标
- 掌握SparkStreaming运行原理
- 掌握SparkStreaming基本算子使用
知识要点
1、SparkStreaming简介
- SparkStreaming是对于Spark核心API的拓展,从而支持对于实时数据流的可拓展,高吞吐量和容错性流处理。
- 数据可以由多个源取得,例如:Kafka,Flume,Twitter,ZeroMQ,Kinesis或者TCP接口,同时可以使用由如map,reduce,join和window这样的高层接口描述的复杂算法进行处理。最终,处理过的数据可以被推送到文件系统,数据库和HDFS。
- Spark Streaming 是基于spark的流式批处理引擎,其基本原理是把输入数据以某一时间间隔批量的处理,当批处理间隔缩短到秒级时,便可以用于处理实时数据流。
- 在 Spark Streaming 中,处理数据的单位是一批而不是单条,而数据采集却是逐条进行的,因此 Spark Streaming 系统需要设置间隔使得数据汇总到一定的量后再一并操作,这个间隔就是批处理间隔。批处理间隔是 Spark Streaming 的核心概念和关键参数,它决定了 Spark Streaming 提交作业的频率和数据处理的延迟,同时也影响着数据处理的吞吐量和性能。
2、SparkStreaming架构流程
3、 SparkStreaming程序入口
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
//或者
val ssc = new StreamingContext(new SparkContext(conf), Seconds(1))
4、什么是DStream
- 离散数据流或者DStream是SparkStreaming提供的基本抽象。其表现数据的连续流,这个输入数据流可以来自于源,也可以来自于转换输入流产生的已处理数据流。内部而言,一个DStream以一系列连续的RDDs所展现,这些RDD是Spark对于不变的,分布式数据集的抽象。一个DStream中的每个RDD都包含来自一定间隔的数据,如下图
- 在DStream上使用的任何操作都会转换为针对底层RDD的操作。例如:之前那个将行的流转变为词流的例子中,flatMap操作应用于行DStream的每个RDD上 从而产生words DStream的RDD。如下图:
5、DStream算子操作
5.1 Transformations
- 实现把一个DStream转换生成一个新的DStream,延迟加载不会触发任务的执行
Transformation | Meaning |
---|---|
map(func) | 对DStream中的各个元素进行func函数操作,然后返回一个新的DStream |
flatMap(func) | 与map方法类似,只不过各个输入项可以被输出为零个或多个输出项 |
filter(func) | 过滤出所有函数func返回值为true的DStream元素并返回一个新的DStream |
repartition(numPartitions) | 增加或减少DStream中的分区数,从而改变DStream的并行度union(otherStream) |
count() | 通过对DStream中的各个RDD中的元素进行计数,然后返回只有一个元素的RDD构成的DStream |
reduce(func) | 对源DStream中的各个RDD中的元素利用func进行聚合操作,然后返回只有一个元素的RDD构成的新的DStream. |
countByValue() | 对于元素类型为K的DStream,返回一个元素为(K,Long)键值对形式的新的DStream,Long对应的值为源DStream中各个RDD的key出现的次数 |
reduceByKey(func, [numTasks]) | 利用func函数对源DStream中的key进行聚合操作,然后返回新的(K,V)对构成的DStream |
join(otherStream, [numTasks]) | 输入为(K,V)、(K,W)类型的DStream,返回一个新的(K,(V,W))类型的DStream |
cogroup(otherStream, [numTasks]) | 输入为(K,V)、(K,W)类型的DStream,返回一个新的 (K, Seq[V], Seq[W]) 元组类型的DStream |
transform(func) | 通过RDD-to-RDD函数作用于DStream中的各个RDD,可以是任意的RDD操作,从而返回一个新的RDD |
updateStateByKey(func) | 根据key的之前状态值和key的新值,对key进行更新,返回一个新状态的DStream |
reduceByKeyAndWindow | 窗口函数操作,实现按照window窗口大小来进行计算 |
5.2 Output Operations
- 输出算子操作,触发任务的真正运行
Output Operation | Meaning |
---|---|
print() | 打印到控制台 |
saveAsTextFiles(prefix, [suffix]) | 保存流的内容为文本文件,文件名为"prefix-TIME_IN_MS[.suffix]". |
saveAsObjectFiles(prefix, [suffix]) | 保存流的内容为SequenceFile,文件名为 “prefix-TIME_IN_MS[.suffix]”. |
saveAsHadoopFiles(prefix, [suffix]) | 保存流的内容为hadoop文件,文件名为 “prefix-TIME_IN_MS[.suffix]”. |
foreachRDD(func) | 对Dstream里面的每个RDD执行func |
6、数据源
6.1 socket数据源
-
需求:
sparkStreaming实时接收socket数据,实现单词计数
-
业务处理流程图
-
安装socket服务
首先在linux服务器node01上用yum 安装nc工具,nc命令是netcat命令的简称,它是用来设置路由器。我们可以利用它向某个端口发送数据。
sudo yum -y install nc
执行命令向指定的端口发送数据
nc -lk 9999
- 代码开发
pom.xml配置
<properties>
<scala.version>2.11.8</scala.version>
<spark.version>2.3.3</spark.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-dependencyfile</arg>
<arg>${project.build.directory}/.scala_dependencies</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass></mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 开发sparkStreaming程序
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* sparkStreaming接受socket数据实现单词计数程序
*/
object SocketWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("TcpWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
//todo: 5、打印结果
result.print()
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
6.2 HDFS数据源
-
需求:
通过sparkStreaming监控hdfs上的目录,有新的文件产生,就把数据拉取过来进行处理 -
业务处理流程图
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
/**
* HDFS数据源
*/
object HdfsWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("HdfsWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、监控hdfs目录数据
val textFileStream: DStream[String] = ssc.textFileStream("hdfs://node01:8020/data")
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = textFileStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
//todo: 5、打印结果
result.print()
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
6.3 自定义数据源
/**
* 自定义一个Receiver,这个Receiver从socket中接收数据
* 使用方式:nc -lk 8888
*/
import java.io.{BufferedReader, InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.SparkConf
import org.apache.spark.internal.Logging
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.receiver.Receiver
/**
* 自定义数据源
*/
object CustomReceiver {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf()
.setAppName("CustomReceiver")
.setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、调用 receiverStream api,将自定义的Receiver传进去
val receiverStream = ssc.receiverStream(new CustomReceiver("node01",8888))
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = receiverStream
.flatMap(_.split(" "))
.map((_,1))
.reduceByKey(_+_)
//todo: 5、打印结果
result.print()
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
/**
* 自定义source数据源
* @param host
* @param port
*/
class CustomReceiver(host:String,port:Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK_SER) with Logging{
override def onStart(): Unit ={
//启动一个线程,开始接受数据
new Thread("socket receiver"){
override def run(): Unit = {
receive()
}
}.start()
}
/** Create a socket connection and receive data until receiver is stopped */
private def receive() {
var socket: Socket = null
var userInput: String = null
try {
logInfo("Connecting to " + host + ":" + port)
socket = new Socket(host, port)
logInfo("Connected to " + host + ":" + port)
val reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
userInput = reader.readLine()
while(!isStopped && userInput != null) {
store(userInput)
userInput = reader.readLine()
}
reader.close()
socket.close()
logInfo("Stopped receiving")
restart("Trying to connect again")
} catch {
case e: java.net.ConnectException =>
restart("Error connecting to " + host + ":" + port, e)
case t: Throwable =>
restart("Error receiving data", t)
}
}
override def onStop(): Unit ={
}
}
6.4 Kafka数据源
- 非常重要的数据源,会放在后面重点详细的讲解
7、SparkStreaming 任务提交集群运行
- 开发wordcount程序,然后打包上传到集群,并打开任务运行界面,查看一下任务运行情况。
bin/spark-submit \
--master spark://node01:7077 \
--class com.aaa.streaming.Demo \
--executor-memory 1g \
--total-executor-cores 2 \
original-sparkStreamingStudy-1.0-SNAPSHOT.jar
8、Transformation高级算子
8.1 updateStateByKey
-
需求
sparkStreaming接受socket数据实现所有批次的单词次数累加 -
代码开发
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* todo: 实现把所有批次的单词出现的次数累加
*/
object UpdateStateBykeyWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("TcpWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//需要设置checkpoint目录,用于保存之前批次的结果数据,该目录一般指向hdfs路径
ssc.checkpoint("hdfs://node01:8020/ck")
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val wordAndOneDstream: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1))
val result: DStream[(String, Int)] = wordAndOneDstream.updateStateByKey(updateFunc)
//todo: 5、打印结果
result.print()
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
//currentValue:当前批次中每一个单词出现的所有的1
//historyValues:之前批次中每个单词出现的总次数,Option类型表示存在或者不存在。 Some表示存在有值,None表示没有
def updateFunc(currentValue:Seq[Int], historyValues:Option[Int]):Option[Int] = {
val newValue: Int = currentValue.sum + historyValues.getOrElse(0)
Some(newValue)
}
}
8.2 mapWithState
-
需求
sparkStreaming接受socket数据实现所有批次的单词次数累加 -
代码开发
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, MapWithStateDStream, ReceiverInputDStream}
import org.apache.spark.streaming._
/**
* todo: mapWithState实现把所有批次的单词出现的次数累加
* --性能更好
*/
object MapWithStateWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("MapWithStateWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
val initRDD: RDD[(String, Int)] = ssc.sparkContext.parallelize((List(("hadoop",10),("spark",20))))
//需要设置checkpoint目录,用于保存之前批次的结果数据,该目录一般指向hdfs路径
ssc.checkpoint("hdfs://node01:8020/ck")
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val wordAndOneDstream: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1))
val stateSpec=StateSpec.function((time:Time,key:String,currentValue:Option[Int],historyState:State[Int])=>{
//当前批次结果与历史批次的结果累加
val sumValue: Int = currentValue.getOrElse(0)+ historyState.getOption().getOrElse(0)
val output=(key,sumValue)
if(!historyState.isTimingOut()){
historyState.update(sumValue)
}
Some(output)
//给一个初始的结果initRDD
//timeout: 当一个key超过这个时间没有接收到数据的时候,这个key以及对应的状态会被移除掉
}).initialState(initRDD).timeout(Durations.seconds(5))
//todo: 使用mapWithState方法,实现累加
val result: MapWithStateDStream[String, Int, Int, (String, Int)] = wordAndOneDstream.mapWithState(stateSpec)
//todo: 5、打印所有批次的结果数据
result.stateSnapshots().print()
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
- 小结
- 特点:
(1)若要清除某个key的状态,可在自定义的方法中调用state.remove()
(2)若要设置状态超时时间,可以调用StateSpec.function(mappingFunc).timeout()方法设置
(3)若要添加初始化的状态,可以调用StateSpec.function(mappingFunc).initialState(initialRDD)方法
(4)性能比updateStateByKey好
- 特点:
8.3 transform
- 需求
获取每一个批次中单词出现次数最多的前3位
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
/**
* todo: 获取每一个批次中单词出现次数最多的前3位
*/
object TransformWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("TransformWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
//todo: 5、将Dstream进行transform方法操作
val sortedDstream: DStream[(String, Int)] = result.transform(rdd => {
//对单词出现的次数进行排序
val sortedRDD: RDD[(String, Int)] = rdd.sortBy(_._2, false)
val top3: Array[(String, Int)] = sortedRDD.take(3)
println("------------top3----------start")
top3.foreach(println)
println("------------top3------------end")
sortedRDD
})
//todo: 6、打印该批次中所有单词按照次数降序的结果
sortedDstream.print()
//todo: 7、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
8.4 window
- Window Operations可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Steaming的允许状态。
- 所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。
- 窗口时长:计算内容的时间范围
- 滑动步长:每隔多久触发一次计算
- 注意:这两者都必须为采集周期大小的整数倍。
- 需求:
2秒一个批次,实现每隔4秒统计6秒窗口的数据结果 - 代码开发
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
/**
* todo: 2秒一个批次,实现每隔4秒统计6秒窗口的数据结果
*/
object ReduceByKeyAndWindowWordCount {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("ReduceByKeyAndWindowWordCount").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1))
//todo: 5、每隔4秒统计6秒的数据
/**
* 该方法需要三个参数:
* reduceFunc: (V, V) => V, ----------> 就是一个函数
* windowDuration: Duration, ----------> 窗口的大小(时间单位),该窗口会包含N个批次的数据
* slideDuration: Duration ----------> 滑动窗口的时间间隔,表示每隔多久计算一次
*/
val windowDStream: DStream[(String, Int)] = result.reduceByKeyAndWindow((x:Int,y:Int)=>x+y,Seconds(6),Seconds(4))
//todo: 6、打印该批次中所有单词按照次数降序的结果
windowDStream.print()
//todo: 7、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}
9、Output 算子
-
核心算子foreachRDD实战
-
需求:
sparkStreaming把处理的结果数据写入到mysql表中进行存储 -
代码开发
import java.sql.DriverManager
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
/**
* todo: 将WordCount案例中得到的结果通过foreachRDD保存结果到mysql中
*/
object WordCountForeachRDD {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
// todo: 1、创建SparkConf对象
val sparkConf: SparkConf = new SparkConf().setAppName("WordCountForeachRDD").setMaster("local[2]")
// todo: 2、创建StreamingContext对象
val ssc = new StreamingContext(sparkConf,Seconds(2))
//todo: 3、接受socket数据
val socketTextStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//todo: 4、对数据进行处理
val result: DStream[(String, Int)] = socketTextStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
//todo: 5、保存结果到mysql表中
//todo:方案一(有问题)
result.foreachRDD(rdd =>{
//注意这里创建的对象都是在Driver端
val conn = DriverManager.getConnection("jdbc:mysql://node03:3306/test", "root", "123456")
val statement = conn.prepareStatement(s"insert into wordcount(word, count) values (?, ?)")
rdd.foreach { record =>
//这一块代码的执行是在executor端,需要进行网络传输,会出现task not serializable 异常
statement.setString(1, record._1)
statement.setInt(2, record._2)
statement.execute()
}
statement.close()
conn.close()
})
//todo: 方案二
result.foreachRDD(rdd=>{
//遍历
rdd.foreach { record =>
val conn = DriverManager.getConnection("jdbc:mysql://node03:3306/test", "root", "123456")
val statement = conn.prepareStatement(s"insert into wordcount(word, count) values (?, ?)")
statement.setString(1, record._1)
statement.setInt(2, record._2)
statement.execute()
statement.close()
conn.close()
}
})
//todo: 方案三
result.foreachRDD(rdd=>{
rdd.foreachPartition( iter =>{
val conn = DriverManager.getConnection("jdbc:mysql://node03:3306/test", "root", "123456")
val statement = conn.prepareStatement(s"insert into wordcount(word, count) values (?, ?)")
iter.foreach( record =>{
statement.setString(1, record._1)
statement.setInt(2, record._2)
statement.execute()
})
statement.close()
conn.close()
})
})
//todo: 方案四
result.foreachRDD(rdd=>{
rdd.foreachPartition( iter =>{
val conn = DriverManager.getConnection("jdbc:mysql://node03:3306/test", "root", "123456")
val statement = conn.prepareStatement(s"insert into wordcount(word, count) values (?, ?)")
//关闭自动提交
conn.setAutoCommit(false);
iter.foreach( record =>{
statement.setString(1, record._1)
statement.setInt(2, record._2)
//添加到一个批次
statement.addBatch()
})
//批量提交该分区所有数据
statement.executeBatch()
conn.commit()
// 关闭资源
statement.close()
conn.close()
})
})
//todo: 6、开启流式计算
ssc.start()
ssc.awaitTermination()
}
}