上篇:第3章 大数据kafka采集数据(Dstream创建)
DStream上的原语与RDD的类似,分为Transformations(转换)和Output
Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。
无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。部分无状态转化操作列在了下表中。注意,针对键值对的DStream转化操作(比如 reduceByKey())要添加import StreamingContext._才能在Scala中使用。
需要记住的是,尽管这些函数看起来像作用在整个流上一样,但事实上每个DStream在内部是由许多RDD(批次)组成,且无状态转化操作是分别应用到每个RDD上的。例如,reduceByKey()会归约每个时间区间中的数据,但不会归约不同区间之间的数据。
举个例子,在之前的wordcount程序中,我们只会统计5秒内接收到的数据的单词个数,而不会累加。
无状态转化操作也能在多个DStream间整合数据,不过也是在各个时间区间内。例如,键 值对DStream拥有和RDD一样的与连接相关的转化操作,也就是cogroup()、join()、leftOuterJoin() 等。我们可以在DStream上使用这些操作,这样就对每个批次分别执行了对应的RDD操作。
我们还可以像在常规的Spark 中一样使用 DStream的union() 操作将它和另一个DStream 的内容合并起来,也可以使用StreamingContext.union()来合并多个流。
UpdateStateByKey
UpdateStateByKey原语用于记录历史记录,有时,我们需要在 DStream 中跨批次维护状态(例如流计算中累加wordcount)。针对这种情况,updateStateByKey() 为我们提供了对一个状态变量的访问,用于键值对形式的 DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件 更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。
updateStateByKey() 的结果会是一个新的 DStream,其内部的 RDD 序列是由每个时间区间对应的(键,状态)对组成的。
updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,你需要做下面两步:
1. 定义状态,状态可以是一个任意的数据类型。
2. 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。 使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
具体代码实现:
package com.study.bigdatabase.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{
Seconds, StreamingContext}
//有状态数据统计
object SparkStreaming05_KafkaState {
def main(args: Array[String]): Unit = {
//使用SparkStreaming完成WordCount
//Sprak配置对象
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming01_WordCount")
//实时数据分析环境对象
//采集周期:以指定的时间为周期采集实时数据
val streamingContext = new StreamingContext(sparkConf,Seconds(5))
//保存数据的状态,需要设定检查点的路径
streamingContext.sparkContext.setCheckpointDir("cp")
//从kafka中采集数据
val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
streamingContext,
"hadoop105:2181",
"bigdata",
Map("bigdata" -> 3)) //分为三个分区
//将采集的数据进行分解(扁平化)
val wordDStream: DStream[String] = kafkaDStream.flatMap(t=>t._2.split(" "))
//将数据进行结构的转换方便统计分析
val mapDStream: DStream[(String, Int)] = wordDStream.map((_, 1))
//将转换结构后的数据进行聚合处理
// val wordToSumDStream: DStream[(String, Int)] = mapDStream.reduceByKey(_ + _)
val stateDStream: DStream[(String, Int)] =mapDStream.updateStateByKey{
case (seq,buffer) =>{
val sum =buffer.getOrElse(0) + seq.sum
Option(sum)
}
}
//将结果 打印出来
stateDStream.print()
//注意:不能停止采集功能
//streamingContext.stop()
//启动采集器
streamingContext.start()
//Drvier等待采集器的执行:
streamingContext.awaitTermination()
}
}
启动程序之前需要把kafka集群启动,在程序启动进行时,kafka需要需要生产数据
shell脚本执行:
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.
关于Window的操作有如下原语:
(1)window(windowLength, slideInterval): 基于对源DStream窗化的批次进行计算返回一个新的Dstream
(2)countByWindow(windowLength, slideInterval):返回一个滑动窗口计数流中的元素。
(3)reduceByWindow(func, windowLength, slideInterval):通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流。
(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]):当在一个(K,V)对的DStream上调用此函数,会返回一个新(K,V)对的DStream,此处通过对滑动窗口中批次数据使用reduce函数来整合每个key的value值。Note:默认情况下,这个操作使用Spark的默认数量并行任务(本地是2),在集群模式中依据配置属性(spark.default.parallelism)来做grouping。你可以通过设置可选参数numTasks来设置不同数量的tasks。
(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]):这个函数是上述函数的更高效版本,每个窗口的reduce值都是通过用前一个窗的reduce值来递增计算。通过reduce进入到滑动窗口数据并”反向reduce”离开窗口的旧数据来实现这个操作。一个例子是随着窗口滑动对keys的“加”“减”计数。通过前边介绍可以想到,这个函数只适用于”可逆的reduce函数”,也就是这些reduce函数有相应的”反reduce”函数(以参数invFunc形式传入)。如前述函数,reduce任务的数量通过可选参数来配置。注意:为了使用这个操作,检查点必须可用。
(6)countByValueAndWindow(windowLength,slideInterval, [numTasks]):对(K,V)对的DStream调用,返回(K,Long)对的新DStream,其中每个key的值是其在滑动窗口中频率。如上,可配置reduce任务数量。
reduceByWindow() 和 reduceByKeyAndWindow() 让我们可以对每个窗口更高效地进行归约操作。它们接收一个归约函数,在整个窗口上执行,比如 +。除此以外,它们还有一种特殊形式,通过只考虑新进入窗口的数据和离开窗口的数据,让 Spark 增量计算归约结果。这种特殊形式需要提供归约函数的一个逆函数,比 如 + 对应的逆函数为 -。对于较大的窗口,提供逆函数可以大大提高执行效率
代码实现:
package com.study.bigdatabase.streaming
object SparkStreaming06_WindowState {
def main(args: Array[String]): Unit = {
val ints = List(1, 2, 3, 4, 5, 6)
滑动窗口函数
val intses: Iterator[List[Int]] = ints.sliding(3)
for (list <- intses ){
println( list.mkString(","))
}
使用kafka采集窗口周期
具体代码:
package com.study.bigdatabase.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{
Seconds, StreamingContext}
object SparkStreaming06_WindowState {
def main(args: Array[String]): Unit = {
//Sprak配置对象
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming01_WordCount")
//采集周期:以指定的时间为周期采集实时数据
val streamingContext = new StreamingContext(sparkConf,Seconds(5))
//从kafka中采集数据
val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
streamingContext,
"hadoop105:2181",
"bigdata",
Map("bigdata" -> 3)) //分为三个分区
//窗口大小为采集周期整数倍,窗口滑动的步长也应该为采集周期的整数倍
val windowDStream: DStream[(String, String)] = kafkaDStream.window(Seconds(9), Seconds(3))
//将采集的数据进行分解(扁平化)
val wordDStream: DStream[String] = kafkaDStream.flatMap(t=>t._2.split(" "))
//将数据进行结构的转换方便统计分析
val mapDStream: DStream[(String, Int)] = wordDStream.map((_, 1))
//将转换结构后的数据进行聚合处理
val wordToSumDStream: DStream[(String, Int)] = mapDStream.reduceByKey(_ + _)
//启动采集器
streamingContext.start()
//Drvier等待采集器的执行:
streamingContext.awaitTermination()
}
}
启动程序需要启动kafka集群,程序启动后,要启动kafka生产数据,输入数据
Transform原语允许DStream上执行任意的RDD-to-RDD函数。即使这些函数并没有在DStream的API中暴露出来,通过该函数可以方便的扩展Spark API。该函数每一批次调度一次。其实也就是对DStream中的RDD应用转换。
比如下面的例子,在进行单词统计的时候,想要过滤掉spam的信息。
代码实现:
package com.study.bigdatabase.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{
Seconds, StreamingContext}
//转换
object SparkStreaming07_transform {
def main(args: Array[String]): Unit = {
//Sprak配置对象
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming01_WordCount")
//采集周期:以指定的时间为周期采集实时数据
val streamingContext = new StreamingContext(sparkConf,Seconds(3))
//从指定的端口采集数据
val socketLineDStream: ReceiverInputDStream[String] = streamingContext.socketTextStream("hadoop105", 9999)
//转换
// TODO 代码(Driver)(1)
//val a =1
socketLineDStream.map{
case x =>{
// TODO 代码(Driver)( n)
//val a =1
x
}
}
// TODO 代码(Driver)(1)
//val a =1
socketLineDStream.transform{
case rdd =>{
// TODO 代码(Driver)( m =采集周期)
//val a =1
rdd.map{
case x =>{
// TODO 代码(Driver)(Execttor)(n)
x
}
}
}
}
socketLineDStream.foreachRDD(rdd=>{
rdd.foreach(println)
})
//启动采集器
streamingContext.start()
//Drvier等待采集器的执行:
streamingContext.awaitTermination()
}
}
输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。与RDD中的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么这些DStream就都不会被求值。如果StreamingContext中没有设定输出操作,整个context就都不会启动。
输出操作如下:
(1)print():在运行流程序的驱动结点上打印DStream中每一批次数据的最开始10个元素。这用于开发和调试。在Python API中,同样的操作叫print()。
(2)saveAsTextFiles(prefix, [suffix]):以text文件形式存储这个DStream的内容。每一批次的存储文件名基于参数中的prefix和suffix。”prefix-Time_IN_MS[.suffix]”.
(3)saveAsObjectFiles(prefix, [suffix]):以Java对象序列化的方式将Stream中的数据保存为 SequenceFiles . 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。
(4)saveAsHadoopFiles(prefix, [suffix]):将Stream中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。
Python API Python中目前不可用。
(5)foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者通过网络将其写入数据库。注意:函数func在运行流应用的驱动中被执行,同时其中一般函数RDD操作从而强制其对于流RDD的运算。
通用的输出操作foreachRDD(),它用来对DStream中的RDD运行任意计算。这和transform() 有些类似,都可以让我们访问任意RDD。在foreachRDD()中,可以重用我们在Spark中实现的所有行动操作。
比如,常见的用例之一是把数据写到诸如MySQL的外部数据库中。 注意:
(1)连接不能写在driver层面;
(2)如果写在foreach则每个RDD都创建,得不偿失;
(3)增加foreachPartition,在分区创建。