Spark Streaming中数据是以微批的形式进来的,如果要基于之前的批处理的处理结果进行计算的话,就必须对之前计算的结果进行管理。可以通过外部存储实现,也可以通过Spark Streaming提供的状态管理来实现。
Spark Streaming提供了两个算子来实现流状态管理:一个是updateStateByKey,另一个是mapWithState。
updateStateByKey
updateStateByKey允许维护任意的状态,并通过新进来的数据更新状态。
在数据流中,通过将给定函数应用于键的前一个状态和该键的新值来更新每个键的状态。
示例
我们以WordCount为例,要统计所有批次总的WordCount,就可以通过updateStateByKey
来实现。
由于是测试,我使用的是socketTextStream
接受数据,通过netcat
来发送数据。
通过nc -l -p 端口号
来创建某个端口的连接,当我们的流程序跑起来后,就可以通过命令行发送word到spark程序。
val sparkConf = new SparkConf()
.setMaster("local[2]")
.setAppName("updateByKeyDemo")
val sc = new SparkContext(sparkConf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))
// 有状态计算必须要指定checkpoint目录
ssc.checkpoint("file:///D:/checkpoint/")
// 监听本地的1234端口
val stream = ssc.socketTextStream("localhost", 1234)
// 对输入进行切换,然后转换为(word, 1)的元组
val wordPairStream = stream
.flatMap(_.split(" "))
.map((_, 1))
// 执行有状态的计算,并输出
wordPairStream.updateStateByKey(updateFunc _)
.print()
ssc.start()
ssc.awaitTermination()
// ---------------------------------------------------
// 定义更新函数
def updateFunc(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
var count = runningCount.getOrElse(0)
for (value <- newValues) {
count += value
}
Some(count)
}
说明
由以上demo可知,将自定义的状态更新函数作为参数传给updateStateByKey
算子即可实现状态计算。
接下来看一下updateStateByKey
算子的一些参数:
参数名 | 说明 |
---|---|
updateFunc | 状态更新函数。根据“之前批次的状态值“和”当前批次的值“来更新状态值 |
numPartitions | 新的Dstream中每个RDD的分区数。 |
partitioner | 指定分区器 |
rememberPartitioner | 是否记住生成的RDD中的分区对象 |
initialRDD | 每个key的初始状态值对应的RDD |
根据参数的不同,updateStateByKey
有7个重载函数,但都是大同小异,主要说一下updateFunc
。粗略有以下两种updateFunc
:
- updateFunc: (Seq[V], Option[S]) => Option[S]
参数:Seq[V]代表本批次K对应的Values,Option[S]代表之前批次的状态值
返回值:Option[S]表示当前批次计算后的状态值
// wordcount demo
def updateFunc(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
var count = runningCount.getOrElse(0)
for (value <- newValues) {
count += value
}
Some(count)
}
- updateFunc: (Iterator[(K, Seq[V], Option[S])]) => Iterator[(K, S)]
参数:iterator是(K, Seq[V], Option[S])
类型的Iterator,K,是当前key,Seq[V]是本批次K对应的Values,Option[S]是历史状态值
返回值:Iterator[(K, S)]是返回的新状态值
// wordcount demo
def updateFunc2(iterator: Iterator[(String, Seq[Int], Option[Int])]): Iterator[(String, Int)] = {
iterator.map(it => {
var count = it._3.getOrElse(0)
for (v <- it._2) {
count += v
}
(it._1, count)
})
}
mapWithState
mapWithState和updateStateByKey类似,也可以实现key的状态管理。
实例
也是一个wordcount demo,和上面实现的是一样的功能
val sparkConf = new SparkConf()
.setMaster("local[2]")
.setAppName("updateByKeyDemo")
val sc = new SparkContext(sparkConf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))
ssc.checkpoint("file:///D:/checkpoint/")
val stream = ssc.socketTextStream("localhost", 1234)
val wordPairStream = stream
.flatMap(_.split(" "))
.map((_, 1))
// 使用mapWithState算子来实现状态管理
wordPairStream
.mapWithState(
StateSpec.function(mappingFunc _)
)
.print()
ssc.start()
ssc.awaitTermination()
// ------------------------------------------------
// 自定义的状态更新函数
def mappingFunc(word: String, value: Option[Int], state: State[Int]): (String, Int) = {
val count = value.getOrElse(0) + state.getOption().getOrElse(0)
state.update(count)
(word, count)
}
说明
将我们自定义的函数包装成StateSpec
作为mapWithState
的参数来实现状态计算。
说一说我们自定义的函数,相关的信息如下:
mappingFunction: (KeyType, Option[ValueType], State[StateType]) => MappedType
参数:KeyType代表key,Option[ValueType]代表当前批次的值,State[StateType]表示状态值
返回值:MappedType代表返回值,就是我们要输出的值.
// wprdcpunt demo
def mappingFunc(word: String, value: Option[Int], state: State[Int]): (String, Int) = {
val count = value.getOrElse(0) + state.getOption().getOrElse(0)
state.update(count)
(word, count)
}
总结
updateStateByKey和mapWithState都可以实现基于状态的计算,都需要设置checkpoint。
updateStateByKey不管状态是否更新,都会输出所有的状态值;mapWithState只会输出状态有更新的状态值,但是也会保留所有的状态值。
理论上是mapWithState性能更好,但是还是要看具体的场景。
更多相关内容可阅读下面的参考链接。