所谓DStream
的转换其实就是对间隔时间内DStream
数据流的RDD
进行转换操作并返回去一个新的DStream
。
其实DStream
转换语法跟RDD
的转换语法非常类似,但DStream
有它自己的一些特殊的语法,如updateStateByKey()、transform()
、以及各种Window
语法。
转换 | 意思 |
---|---|
map(func) | 将DStream上的每个RDD通过func函数操作并返回一个新的DStream |
flatMap(func) | 和map类似,但是每个输入的元素可以映射成0个或者多个项 |
filter(func) | 过滤出DStream上符合要求的RDD并返回新的DStream |
repartiton(numPartitions) | 对DStream上的RDD进行重新分区,提高并行度 |
union(otherDStream) | 将两个DStream合并到一起 |
count() | 返回DStream中RDD中元素的总个数,返回只包含一个Long类型的DStream |
reduce(func) | 通过func函数聚合DStream中每个RDD中的每个元素,返回新的DStream,该新的DStream中只有一个元素,就是聚合以后的而结果 |
countByValue() | 计算DStream上RDD内元素出现的频次,并返回新的DStream[(K,Long)],K是RDD元素的类型,Long是元素出现的次数 |
reduceByKey(func,[numPartition]) | 聚合DStream上(K,V)类型的RDD里元素的,根据K统计V的个数,返回新的DStream,新的DStream里的RDD元素类型也为(K,V),K为键,V为K对应值的个数 |
join(otherDStream,[numPartition]) | 将两个类型为(K,V)和(K,W)的DStream进行join连接,返回一个类型为(K,(V,W))的新的DStream |
cogroup(otherDStream,[numPartition]) | 对两个(K,V)和(K,W)类型的DStream上调用该函数的时候,返回(K,(Seq [V],Seq [W]))元组的新DStream。 |
transform(func) | 通过转换函数,将DStream上的每个RDD转换成另一种RDD,这种函数的操作基本单位为RDD,所以这个函数中的操作语法就是RDD的操作语法。转换后返回新的DStream |
updateStateByKey
就是随着时间的流逝,在SparkStreaming
中的可以对每一个Key
通过checkPoint
来维护state
状态,通过更新函数对每一个Key
的状态进行更新,在更新的时候,对于每一个批次的数据而言,SparkStreaming
都会通过updateStateByKey
这个函数更新已存在Key对应的State
,但是如果通过更新函数对State
更新以后返回的是None
,那么此刻的Key
对应的State
就会被删除,State
可以是任意类型的数据结构。
package com.lyz.streaming.transformation
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object UpdateStateByKey {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Streamingtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val streaming = new StreamingContext(conf, Seconds(5))
/**
*
* 在HDFS上存储state的检查点,必须设置,否则会报错,因为每次更新state的时候
* 都需要去检查点获取上一次更新后的state
*/
streaming.checkpoint("hdfs://xxxxx:8020/spark/checkpoint")
val stream: InputDStream[(LongWritable, Text)] = streaming.fileStream[LongWritable, Text, TextInputFormat]("hdfs://192.168.101.187:8020/spark/data")
val mapStream: DStream[(String, Int)] = stream.map(x => (x._2.toString, 1))
/**
* 定义更新函数体
* value:就是新一批次的数据经过逻辑处理得到的结果
* res:新一批次的数据进来之前的状态值
*/
def func(value: Seq[Int], res: Option[Int]): Some[Int] = {
val sum: Int = value.sum
val per: Int = res.getOrElse(0)
Some(sum + per)
}
//调用更新函数,传入更新函数体
val res: DStream[(String, Int)] = mapStream.updateStateByKey(func)
res.foreachRDD(rdd => {
rdd.collect().foreach(println(_))
})
streaming.start()
streaming.awaitTermination()
}
}
transform
操作允许DStream
上间隔时间内里的RDD
转换成另个RDD
。它的作用就是应用Dstream
为公开的RDD
操作函数。例如DStream
上一个新的批次RDD
与已存在的RDD
进行join
连接查询的API
是未公开的,所以是不能这么使用的,但是利用transform
函数就可以使用DStream
未公开API
。
package com.lyz.streaming.transformation
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat
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, InputDStream}
object TransformTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Streamingtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val streaming = new StreamingContext(conf, Seconds(5))
/**
*
* 在HDFS上存储state的检查点,必须设置,否则会报错,因为每次更新state的时候
* 都需要去检查点获取上一次更新后的state
*/
streaming.checkpoint("hdfs://xxxx:8020/spark/checkpoint")
val stream: InputDStream[(LongWritable, Text)] = streaming.fileStream[LongWritable, Text, TextInputFormat]("hdfs://192.168.101.187:8020/spark/data")
val mapStream: DStream[(String, Int)] = stream.map(x => (x._2.toString, 1))
//外部的RDD
val rdd: RDD[(String, Int)] = streaming.sparkContext.makeRDD(Array(("aaaa", 1)))
/**
* 调用DStream的transform函数,将DStream里的RDD与外部RDD进行操作,返回新的DStream
* transform强大的原因就是能够使用DStream为公开的一些RDD操作函数
*/
val res: DStream[(String, (Int, Int))] = mapStream.transform(rdd1 => {
rdd1.join(rdd)
})
res.foreachRDD(rdd => {
rdd.collect().foreach(println(_))
})
streaming.start()
streaming.awaitTermination()
}
}
SparkStreaming
提供了基于窗口的操作,你可以在滑动窗口内对数据进行转换。这种窗口操作就是在比SparkStreaming
批处理的时间间隔更长的时间间隔内整合多个批处理的结果。
Window
窗口操作需要两个参数,一个参数是窗口的时长(最近的几个批次),另一个是窗口的滑动步长(多久执行一次计算)。需要特别注意的是这两个参数一定要是批处理间隔时间的整数倍。例如我们有一个以10
秒为间隔的批处理,我们想要没十秒计算一次30
秒内的数据,那么我们就可以把时长窗口设置成30
秒,窗口的滑动步长为10
秒。
package com.lyz.streaming.transformation
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
object WindowTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Streamingtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val streaming = new StreamingContext(conf, Seconds(5))
val stream: InputDStream[(LongWritable, Text)] = streaming.fileStream[LongWritable, Text, TextInputFormat]("hdfs://192.168.101.187:8020/spark/data")
val mapStream: DStream[(String, Int)] = stream.map(x => (x._2.toString, 1))
/**
* 每十秒计算一次前三十秒的数据,其实也就是前6个批次的数据
*/
val res: DStream[(String, Int)] = mapStream.reduceByKeyAndWindow((w: Int, w1: Int) => w + w1, Seconds(30), Seconds(10))
}
}
比较常用的Window窗口函数
函数 | 解释 |
---|---|
window(windowLength,slideInterval) | 创建滑动窗口,并返回一个新的DStream然后自定义函数处理这个滑动窗口的数据 |
reduceByKeyWindow(func,windowLength.slideInterval) | 对每个滑动窗口执行reduceByKey的操作 |
reduceByKeyAndWindow(func,invFunc,windowLength,slideInterval,[numParition]) | 这是一个性能更好的reduceByKeyAndWindow函数, |
countByWindow(windowLength,slideInterval) | 对每一个滑动窗口执行count操作,返回每个窗口里的元素个数 |
countByValueAndWindow(windowLength,slideInterval,[numPatition]) | 在每个滑动窗口内计算reduceByValue操作,统计每个键对应的值在窗口内出现的频次 |
操作DStream的输出结果可以推送到外部数据库中或者外的文件系统中。
函数 | 解释 |
---|---|
print() | 在Driver端打印DStream上的前十个元素 |
saveAsTextFiles(prefix,[suffix]) | 保存DStream内容到文本文件中,每个批次产生的结果的名字为前缀为prefix,后缀为[.suffix],所以全称为"prefix-TIME_IN_MS[.suffix]" |
saveAsObjectFiles(prefix,[suffix]) | 保存DStream内容为Sequence文件,这文件是Java对象序列化后的,每个批次产生的结果的名字为前缀为prefix,后缀为[.suffix],所以全称为"prefix-TIME_IN_MS[.suffix]" |
saveAsHadoopFiles(prefix,[suffix]) | 保存DStream内容到Hadoop文件系统中,每个批次产生的结果的名字为前缀为prefix,后缀为[.suffix],所以全称为"prefix-TIME_IN_MS[.suffix]" |
foreachRDD(func) | 循环DStream上的RDD,并且将函数应用到每个RDD上,并且这个函数可以将RDD推送到外部系统中,注意这个函数时在Driver端运行的。它内部的RDD的操作是在每个Woker分区上执行的 |
foreachRDD
几首一个函数,通过这个函数我可以把RDD
保存到外部系统中,既然要保存到外部系统就需要与外部系统建立连接,我们都知道RDD
的数据是散落在各个woker
上的,处理RDD
的函数也是在各个worker
上进行的,我们需要将数据保存到外部数据,那么我们就必须在每个worker
上都存在一个连接,因为连接是不能被序列化的,所以这个连接肯定是不能由driver
发送到worker
上的,而是在worker
上创建出来。那么怎么样才能在worker
上创建连接呢?那就是在rdd.foreach
代码块里创建连接,而不是在stream.foreachRDD
代码块里创建。
res.foreachRDD(rdd => {
rdd.foreach(r => {
//1、创建连接
//2、保存数据
//3、关闭连接
})
})
我们都知道遍历RDD的时候其实就是在遍历每一条记录,如上代码所示我们为每一条记录都创建了连接,显然这样是非常影响系统性能的,那么我们怎么办呢?既然数据是在各个worker的分区上的,那么我们可以为每个分区创建一个连接。
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
在处理DStream数据流的时候,我们可以利用DataFrame和SQL操作DStream。利用DataFrame和SQL处理DStream的前提是必须利用初始化Streaming的SparkConf来创建SparkSession。例子如下
package com.lyz.streaming.transformation
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat
import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
object DateFrameAndSqlTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Streamingtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val streaming = new StreamingContext(conf, Seconds(5))
/**
*
* 在HDFS上存储state的检查点,必须设置,否则会报错,因为每次更新state的时候
* 都需要去检查点获取上一次更新后的state
*/
streaming.checkpoint("hdfs://192.168.101.187:8020/spark/checkpoint")
val stream: InputDStream[(LongWritable, Text)] = streaming.fileStream[LongWritable, Text, TextInputFormat]("hdfs://192.168.101.187:8020/spark/data")
val mapStream: DStream[(String, Int)] = stream.map(x => (x._2.toString, 1))
mapStream.foreachRDD(rdd => {
//利用SparkConf来初始化SparkSession。
val sparkSession: SparkSession = SparkSession.builder().config(conf).getOrCreate()
//导入隐式转换来将RDD
import sparkSession.implicits._
//将RDD转换成DF
val df: DataFrame = rdd.toDF("word", "count")
df.createOrReplaceTempView("compute")
val computeDF: DataFrame = sparkSession.sql("select * from compute")
computeDF.show()
})
streaming.start()
streaming.awaitTermination()
}
}
与RDD缓存和持久化类似,我们在程序中可以把程序中将要被多次计算的DStream缓存到内存中,也就是对相同数据进行多次计算。调用DStream的persist方法来实现DStream的缓存。对于窗口函数的操作这些底层已经对数据进行缓存了,因此窗口函数生成的DStream已经保存在了内存中,开发人员无需调用persist方法。
对于通过网络传输的的数据流,默认的持久化是将数据复制到两个节点上进行备份实现容错
注意:与RDD不同,DStream的默认持久化级别是将数据持久化保存在内存中。
由于流式处理程序需要全天不间断的运行,运行期间有很大几率会出现程序故障,为了应用程序的故障容错性,Streaming设置了检查点功能,将足够的信息保存到检查点中,以便对系统故障进行恢复。Streaming可以对两种数据进行checkPoint。
有状态操作的转换,如果在应用程序中只用了例如updateStateByKey侯喆是reduceByKeyAndWindow,就必须要设置RDD的检查点。
恢复故障的应用驱动程序 ,应用程序的元数据检查点是保存应用程序当前进度信息的,所以驱动程序故障恢复就是恢复当前应用程序执行的进度。
package com.lyz.streaming.checkpoint
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
object MetaStoreCheckPointTest {
def main(args: Array[String]): Unit = {
//设置检查点目录
val checkPointDir = "hdfs://192.168.101.187:8020/spark/checkpoint"
//从检查点上得到StreamingContext,如果检查点上没有StreamingContext那么就创建一个新的StreamingContext。
val streaming: StreamingContext = StreamingContext.getOrCreate(checkPointDir, () => createContext(checkPointDir))
streaming.start()
streaming.awaitTermination()
}
def createContext(checkPointDir: String): StreamingContext = {
//创建SparkConf
val conf = new SparkConf().setMaster("local[2]").setAppName("Streamingtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
//创建StreamingContext
val streaming = new StreamingContext(conf, Seconds(5))
/**
*
* 在HDFS上存储state的检查点,必须设置,否则会报错,因为每次更新state的时候
* 都需要去检查点获取上一次更新后的state
*/
streaming.checkpoint(checkPointDir)
val stream: InputDStream[(LongWritable, Text)] = streaming.fileStream[LongWritable, Text, TextInputFormat]("hdfs://192.168.101.187:8020/spark/data")
val mapStream: DStream[(String, Int)] = stream.map(x => (x._2.toString, 1))
val res: DStream[(String, Int)] = mapStream.reduceByKeyAndWindow((a: Int, b: Int) => a + b, Seconds(30), Seconds(10))
res.foreachRDD(_.foreach(println(_)))
streaming
}
}