前言
- 这篇文章主要讲述了spark几大核心内置模块中的spark-streaming(流数据的处理),主要围绕Dstream的入门,创建,转化展开,图文详解
- spark其他核心模块的系列文章:
spark(一)–spark-core–RDD入门实战
spark(二)–spark-core—RDD进阶知识(图文详解,基于IDEA开发)
目录
Dstream的创建–各个数据源
Dstream转换–无状态和有状态
Spark Streaming概述
Spark Streaming是什么
- Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。
- 和Spark基于RDD的概念很相似,Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫作DStream。DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而DStream是由这些RDD所组成的序列(因此得名“离散化”)。
SparkStreaming架构
Dstream入门案例
1 WordCount案例实操
1.需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数
window netcat下载教程
2.添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>2.4.5</version>
</dependency>
3代码
def main(args: Array[String]): Unit = {
1.初始化Spark配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
2.初始化SparkStreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(2))
3.通过监控端口创建DStream,读进来的数据为一行行
val lineStreams = ssc.socketTextStream("localhost", 9999)
将每一行数据做切分,形成一个个单词
val wordStreams = lineStreams.flatMap(_.split(" "))
将单词映射成元组(word,1)
val wordAndOneStreams = wordStreams.map((_, 1))
将相同的单词次数做统计
val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)
println("hello")
打印
wordAndCountStreams.print()
继续等待,不能让其停止
ssc.start()
ssc.awaitTermination()
}
4先打开window命令行,再在idea中开启应用程序,让二者建立连接
5在命令行中随意输入
sasdf aaa aaa
--------------
Time: 1589959686000 ms
-------------------------------------------
(aaa,2)
(sasdf,1)
2 WordCount解析
Discretized Stream是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据,如下图:
对数据的操作也是按照RDD为单位来进行的
计算过程由Spark engine来完成
Dstream创建
Spark Streaming原生支持一些不同的数据源。一些“核心”数据源已经被打包到Spark Streaming 的 Maven 工件中,而其他的一些则可以通过 spark-streaming-kafka 等附加工件获取。每个接收器都以 Spark 执行器程序中一个长期运行的任务的形式运行,因此会占据分配给应用的 CPU 核心。此外,我们还需要有可用的 CPU 核心来处理数据。这意味着如果要运行多个接收器,就必须至少有和接收器数目相同的核心数,还要加上用来完成计算所需要的核心数。例如,如果我们想要在流计算应用中运行 10 个接收器,那么至少需要为应用分配 11 个 CPU 核心。所以如果在本地模式运行,不要使用local[1]。
1. hdfs文件系统读取流式文件
文件数据流:能够读取所有HDFS API兼容的文件系统文件,通过fileStream方法进行读取,Spark Streaming 将会监控 dataDirectory 目录并不断处理移动进来的文件,记住目前不支持嵌套目录。
streamingContext.textFileStream(dataDirectory)
注意事项:
1)文件需要有相同的数据格式;
2)文件进入 dataDirectory的方式需要通过移动或者重命名来实现;
3)一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据;
4) 一般不会这么做,而是用flume收集数据,了解一下就好
- 在HDFS上建好目录
hadoop fs -mkdir /fileStream
- 创建三个文件
touch a.tsv
touch b.tsv
touch c.tsv
添加如下数据:
Hello
Hello spark
- 编写代码
def main(args: Array[String]): Unit = {
//1.初始化Spark配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(2))
//3.通过监控端口创建DStream,读进来的数据为一行行
val lineStreams = ssc.textFileStream("hdfs://121.199.16.65:8020/fileSystem")
//将每一行数据做切分,形成一个个单词
val wordStreams = lineStreams.flatMap(_.split("\t"))
//将单词映射成元组(word,1)
val wordAndOneStreams = wordStreams.map((_, 1))
//将相同的单词次数做统计
val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)
println("hello")
//打印
wordAndCountStreams.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
- 启动程序并向fileStream目录上传文件
hadoop fs -put ./a.tsv /fileStream
hadoop fs -put ./b.tsv /fileStream
hadoop fs -put ./c.tsv /fileStream
- 获取计算结果
-------------------------------------------
Time: 1539073815000 ms
-------------------------------------------
(Hello,4)
(spark,2)
-------------------------------------------
Time: 1539073820000 ms
-------------------------------------------
(Hello,2)
(spark,1)
2.RDD队列
用法及说明
测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。
- 案例实操
1)需求:循环创建几个RDD,将RDD放入队列。通过SparkStream创建Dstream,计算WordCount
- 代码
object RDDStream {
def main(args: Array[String]) {
//1.初始化Spark配置信息
val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(conf, Seconds(4))
//3.创建RDD队列
val rddQueue = new mutable.Queue[RDD[Int]]()
//4.创建QueueInputDStream
val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)
//5.处理队列中的RDD数据
val mappedStream = inputStream.map((_,1))
val reducedStream = mappedStream.reduceByKey(_ + _)
//6.打印结果
reducedStream.print()
//7.启动任务
ssc.start()
//8.循环创建并向RDD队列中放入RDD
for (i <- 1 to 5) {
rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
Thread.sleep(2000)
}
ssc.awaitTermination()
}
}
-------------------------------------------
Time: 1539075280000 ms
-------------------------------------------
(4,60)
(0,60)
(6,60)
(8,60)
(2,60)
3 Kafka数据源(重点)
1 用法及说明
在工程中需要引入 Maven 工件 spark- streaming-kafka_2.10 来使用它。包内提供的 KafkaUtils 对象可以在 StreamingContext 和 JavaStreamingContext 中以你的 Kafka 消息创建出 DStream。由于 KafkaUtils 可以订阅多个主题,因此它创建出的 DStream 由成对的主题和消息组成。要创建出一个流数据,需要使用 StreamingContext 实例、一个由逗号隔开的 ZooKeeper 主机列表字符串、消费者组的名字(唯一名字),以及一个从主题到针对这个主题的接收器线程数的映射表来调用 createStream() 方法。
2 案例实操
需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做简单计算(WordCount),最终打印到控制台。
(1)导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.2</version>
</dependency>
(2)
在kafka中创建topic
kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic kafka_spark
生产消息
kafka-console-producer.sh --broker-list localhost:9092 --topic kafka_spark
(3)编写代码
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("DirectKafka")
.setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(5))
val topicsSet = Array("kafka_spark")
val kafkaParams = mutable.HashMap[String, String]()
//必须添加以下参数,否则会报错
kafkaParams.put("bootstrap.servers", "121.199.16.65:9092")
kafkaParams.put("group.id", "0")
kafkaParams.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
kafkaParams.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
val messages = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topicsSet, kafkaParams
)
)
// Get the lines, split them into words, count the words and print
val lines = messages.map(_.value)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1L)).reduceByKey(_ + _)
wordCounts.print()
// Start the computation
ssc.start()
ssc.awaitTermination()
}
(4)
在kafka中输入消息
aa bb cc aa
Time: 1589984005000 ms
-------------------------------------------
(aa,2)
(bb,1)
(cc,1)
4 自定义数据源
1 用法及说明
需要继承Receiver,并实现onStart、onStop方法来自定义数据源采集。
3.3.2 案例实操
1)需求:自定义数据源,实现监控某个端口号,获取该端口号内容。
2)代码实现
自定义采集器:
class CustomerReceiver(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {
最初启动的时候,调用该方法,作用为:读数据并将数据发送给Spark
override def onStart(): Unit = {
new Thread("Socket Receiver") {
override def run() {
receive()
}
}.start()
}
读数据并将数据发送给Spark
def receive(): Unit = {
创建一个Socket
var socket: Socket = new Socket(host, port)
定义一个变量,用来接收端口传过来的数据
var input: String = null
创建一个BufferedReader用于读取端口传来的数据
val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
读取数据
input = reader.readLine()
当receiver没有关闭并且输入数据不为空,则循环发送数据给Spark
while (!isStopped() && input != null) {
store(input)
input = reader.readLine()
}
跳出循环则关闭资源
reader.close()
socket.close()
重启任务
restart("restart")
}
override def onStop(): Unit = {}
}
测试:
def main(args: Array[String]): Unit = {
//上下文
val conf=new SparkConf().setAppName("streamWordCount").setMaster("local");
// var sc=new SparkContext(conf);
//sc.setLogLevel("ERROR")
val ssc=new StreamingContext(conf,Seconds(3));
val lineStream=ssc.receiverStream(new myReceiver("localhost",9999));
//4.将每一行数据做切分,形成一个个单词
val wordStreams = lineStream.flatMap(_.split(" "))
//5.将单词映射成元组(word,1)
val wordAndOneStreams = wordStreams.map((_, 1))
//6.将相同的单词次数做统计
val wordAndCountStreams = wordAndOneStreams.reduceByKey(_ + _)
//7.打印
println("hello")
wordAndCountStreams.print();
//8.启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
DStream转换
1 无状态转化操作
无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。
部分无状态转化操作列在了下表中。
需要记住的是,尽管这些函数看起来像作用在整个流上一样,但事实上每个DStream在内部是由许多RDD(批次)组成,且无状态转化操作是分别应用到每个RDD上的。例如,reduceByKey()会归约每个时间区间中的数据,但不会归约不同区间之间的数据。
举个例子,在之前的wordcount程序中,我们只会统计5秒内接收到的数据的单词个数,而不会累加。
Window Operations
先来看看什么叫窗口滑动
def main(args: Array[String]): Unit = {
val list=Array(1,2,3,4,5,6);
val window=list.sliding(2);
for (r<-window){
println(r.mkString(";"));
}
}
-----------------
1;2
2;3
3;4
4;5
5;6
可以想像,有一个窗口,把两个数包围起来,然后一步一步的往后滑动
window Operations
window Operations可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Steaming的允许状态。基于窗口的操作会在一个比 StreamingContext 的批次间隔更长的时间范围内,通过整合多个批次的结果,计算出整个窗口的结果。
上图可以观察到,窗口的大小为三,每次往后滑动两个步长
- 注意:所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长,两者都必须是 StreamContext 的批次间隔的整数倍。
- 窗口时长控制每次计算最近的多少个批次的数据,其实就是最近的 windowDuration/batchInterval 个批次。如果有一个以 10 秒为批次间隔的源 DStream,要创建一个最近 30 秒的时间窗口(即最近 3 个批次),就应当把 windowDuration 设为 30 秒。而滑动步长的默认值与批次间隔相等,用来控制对新的 DStream 进行计算的间隔。如果源 DStream 批次间隔为 10 秒,并且我们只希望每两个批次计算一次窗口结果, 就应该把滑动步长设置为 20 秒。
- 假设,你想拓展前例从而每隔十秒对持续30秒的数据生成word count。为做到这个,我们需要在持续30秒数据的(word,1)对DStream上应用reduceByKey。使用操作reduceByKeyAndWindow.
WordCount第三版:3秒一个批次,窗口12秒,滑步6秒。
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(3))
ssc.checkpoint(".")
// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)
// Split each line into words
val words = lines.flatMap(_.split(" "))
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(12), Seconds(6))
// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
}
-------------------------------------
结果:
-------------------------------------------
Time: 1589966829000 ms
-------------------------------------------
(bbb,1)
(ccc,1)
(aaa,2)
-------------------------------------------
Time: 1589966835000 ms
-------------------------------------------
(ssss,1)
(bbb,2)
(ddd,1)
(lll,1)
(ccc,1)
(aaa,2)
-------------------------------------------
Time: 1589966841000 ms
-------------------------------------------
(ssss,1)
(bbb,2)
(ddd,1)
(lll,1)
(aaa,1)
-------------------------------------------
Time: 1589966847000 ms
-------------------------------------------
(bbb,1)
(aaa,1)
-------------------------------------------
Time: 1589966853000 ms
-------------------------------------------
Process finished with exit code -1
UpdateStateByKey
updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,你需要做下面两步:
- 定义状态,状态可以是一个任意的数据类型。
- 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
更新版的wordcount:
ef main(args: Array[String]) {
定义更新状态方法,参数values为当前批次单词频度,state为以往批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _)
val previousCount = state.getOrElse(0)
Some(currentCount + previousCount)
}
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("hdfs://localhost:9000/streamCheck")
val lines = ssc.socketTextStream("hadoop102", 9999)
// Split each line into words
val words = lines.flatMap(_.split(" "))
//import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
// 使用updateStateByKey来更新状态,统计从运行开始以来单词总的次数
val stateDstream = pairs.updateStateByKey[Int](updateFunc)
stateDstream.print()
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
}
(2)启动程序并向9999端口发送数据
nc -lk 9999
ni shi shui
ni hao ma
(3)结果展示
-------------------------------------------
Time: 1504685175000 ms
-------------------------------------------
-------------------------------------------
Time: 1504685181000 ms
-------------------------------------------
(shi,1)
(shui,1)
(ni,1)
-------------------------------------------
Time: 1504685187000 ms
-------------------------------------------
(shi,1)
(ma,1)
(hao,1)
(shui,1)
(ni,2)