第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
来源:互联网 发布:西方哲学框架 知乎 编辑:程序博客网 时间:2024/05/14 14:15
第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
本节课讲解Spark Streaming中一个非常重要的内容:状态管理。为了说明这个状态管理,我们拿两个具体的方法updateStateByKey和mapWithState来说明Spark Streaming到底如何实现状态管理的。整个Spark Streaming按照Batch Duration划分Job,但是有时候我们想计算过去一小时,或者过去1天,或者过去一周的数据,在这么长的大于Batch Duration的时间实现符合我们业务的操作,不可避免的一定要发生的事情是进行状态维护。
我们的Spark Streaming在每个Batch Duration会产生一个Job,Job里面都是RDD,我们现在面临的一个问题就是:对于每个Batch Duration中的RDD,怎么对它的状态进行维护?例如updateStateByKey 计算一整天的商品的点击量或者一整天的商品排名,这个时候就需要类似updateStateByKey和mapWithState帮助你完成核心的步骤。
Spark本身博大精深,在Spark中可以看到IT界的大多数的内容,例如:通过Spark去研究JVM,通过Spark去研究分布式,通过Spark去研究机器学习、图计算这些内容,通过Spark也可以去研究架构设计,通过Spark也可以研究很多软件工程的内容。所以我们以Spark为载体可以做非常多的事情。
updateStateByKey和mapWithState 可不可以在Dstream中找到这2个方法?在Dstream中找不到。updateStateByKey和mapWithState 都是针对Key-Value的类型的数据进行操作,都是Pair类型的,跟我们前面的RDD是一样的,RDD并不会直接对Key-Value类型进行操作,这个时候要借助Scala的语法进行隐式转换。
这里是DStream的object,最佳实践是将隐式转换放到Object的静态区域,就是伴生对象区域toPairDStreamFunctions,在Spark 1.3版本之前,使用import StreamingContext._的方式。现在不需要import,因为这里是隐式转换,如使用一个方法updateStateByKey在Dstream中找不到,就会进行隐式转换,发现 toPairDStreamFunctions的签名符合DStream,又是implicit级别的,然后就进行隐式转换,转换成PairDStreamFunctions。我们比喻为从地狱中召唤出来的功能,使用过后又回到地狱。
DStream.scala的源代码:
1. object DStream {
2.
3. // `toPairDStreamFunctions` wasin SparkContext before 1.3 and users had to
4. // `import StreamingContext._`to enable it. Now we move it here to make the compiler find
5. // it automatically. However, westill keep the old function in StreamingContext for backward
6. // compatibility and forward tothe following function directly.
7.
8. implicit deftoPairDStreamFunctions[K, V](stream: DStream[(K, V)])
9. (implicit kt: ClassTag[K],vt: ClassTag[V], ord: Ordering[K] = null):
10. PairDStreamFunctions[K, V] = {
11. new PairDStreamFunctions[K,V](stream)
12. }
13.
14. /** Get the creation site of aDStream from the stack trace of when the DStream is created. */
15. private[streaming] defgetCreationSite(): CallSite = {
16. val SPARK_CLASS_REGEX ="""^org\.apache\.spark""".r
17. valSPARK_STREAMING_TESTCLASS_REGEX ="""^org\.apache\.spark\.streaming\.test""".r
18. val SPARK_EXAMPLES_CLASS_REGEX= """^org\.apache\.spark\.examples""".r
19. val SCALA_CLASS_REGEX ="""^scala""".r
20.
21. /** Filtering function thatexcludes non-user classes for a streaming application */
22. def streamingExclustionFunction(className:String): Boolean = {
23. def doesMatch(r: Regex):Boolean = r.findFirstIn(className).isDefined
24. val isSparkClass =doesMatch(SPARK_CLASS_REGEX)
25. val isSparkExampleClass =doesMatch(SPARK_EXAMPLES_CLASS_REGEX)
26. val isSparkStreamingTestClass= doesMatch(SPARK_STREAMING_TESTCLASS_REGEX)
27. val isScalaClass =doesMatch(SCALA_CLASS_REGEX)
28.
29. // If the class is a sparkexample class or a streaming test class then it is considered
30. // as a streamingapplication class and don't exclude. Otherwise, exclude any
31. // non-Spark and non-Scalaclass, as the rest would streaming application classes.
32. (isSparkClass ||isScalaClass) && !isSparkExampleClass &&!isSparkStreamingTestClass
33. }
34. org.apache.spark.util.Utils.getCallSite(streamingExclustionFunction)
35. }
36. }
PairDStreamFunctions中的updateStateByKey有很多重载方法,也有mapWithState。这两个方法使用完毕,要回到Dstream级别的操作。updateStateByKey是在历史已有基础之上使用updateFunc函数对数据进行更新操作,然后返回一个DStream。
PairDStreamFunctions.scala的源代码:
1. class PairDStreamFunctions[K,V](self: DStream[(K, V)])
2. (implicit kt: ClassTag[K], vt:ClassTag[V], ord: Ordering[K])
3. extends Serializable {
4. ……
5. @Experimental
6. def mapWithState[StateType: ClassTag,MappedType: ClassTag](
7. spec: StateSpec[K, V, StateType,MappedType]
8. ): MapWithStateDStream[K, V, StateType,MappedType] = {
9. new MapWithStateDStreamImpl[K, V,StateType, MappedType](
10. self,
11. spec.asInstanceOf[StateSpecImpl[K, V,StateType, MappedType]]
12. )
13. }
14. ……
15. /**
16. 返回一个新的“状态”DStream ,每个 key的状态按给定的函数根据key的前一状态和每个key的新值进行更新。哈希分区是用numPartitions分区数来生成 RDDs。
17. @param updateFunc 状态更新功能。如果这个函数没有返回,那么对应 key-value会被清除掉。
18. @param numPartitions 新的DStream每个RDD分区的numPartitions数。
19. @tparam S 状态类型
20. */
21. def updateStateByKey[S:ClassTag](
22. updateFunc: (Seq[V], Option[S]) =>Option[S]
23. ): DStream[(K, S)] = ssc.withScope {
24. updateStateByKey(updateFunc,defaultPartitioner())
25. }
26. …….
27. def mapWithState[StateType: ClassTag,MappedType: ClassTag](
28. spec: StateSpec[K, V, StateType,MappedType]
29. ): MapWithStateDStream[K, V, StateType,MappedType] = {
30. new MapWithStateDStreamImpl[K, V,StateType, MappedType](
31. self,
32. spec.asInstanceOf[StateSpecImpl[K, V,StateType, MappedType]]
33. )
34. }
在updateFunc中声明类型,Seq[V]是历史数据, Option[S]是Option。无论是基于状态的计算,还是基于BatchDuration的计算都是基于RDD的。RDD中都需要partition。defaultPartitioner默认采用了HashPartitioner,HashPartitioner对应Hash的计算方式,我们采用Shuffle的时候为什么要采用Hash?我们一定要想明白,在性能调优或者负载均衡或数据倾斜的时候,有很好的理念支撑你去做优化。Hash很重要,Hash的一个特点是效率高,Spark 1.2.x之前采用Shuffle的方式就是因为效率高,不需要排序等。这里并行度使用的defaultParallelism,和Spark Core完全是一样的。自己写代码也可以这么使用,操作的时候注意包名的命名。
PairDStreamFunctions.scala的defaultPartitioner源代码:
1. private[streaming] defdefaultPartitioner(numPartitions: Int = self.ssc.sc.defaultParallelism) = {
2. newHashPartitioner(numPartitions)
3. }
回到updateStateByKey,updateStateByKey的传入参数有一个partitioner参数。类似的,SparkSQL如果操作Hive中的表的时候,你自己设置了一个并行度,使用Spark SQL On Hive的方式是否会生效?封装partition基于Hive去控制是否受自定义的控制?不会!这是Spark SQL比较特殊的地方,不会的时候有时会导致结果的并行度太低,后面的RDD继承前面RDD的partition,假如并行度为3,本来可以为300的,就影响数据的计算,造成GC等。这个时候就要使用repartition,而不是使用coalesce的方式。
PairDStreamFunctions.scala的源代码:
1. def updateStateByKey[S:ClassTag](
2. updateFunc: (Seq[V],Option[S]) => Option[S],
3. partitioner: Partitioner,
4. initialRDD: RDD[(K, S)]
5. ): DStream[(K, S)] =ssc.withScope {
6. val cleanedUpdateF =sparkContext.clean(updateFunc)
7. val newUpdateFunc = (iterator:Iterator[(K, Seq[V], Option[S])]) => {
8. iterator.flatMap(t =>cleanedUpdateF(t._2, t._3).map(s => (t._1, s)))
9. }
10. updateStateByKey(newUpdateFunc,partitioner, true, initialRDD)
11. }
这里用了newUpdateFunc,然后将函数newUpdateFunc传给updateStateByKey,其中传进来的参数rememberPartitioner为True。
PairDStreamFunctions.scala的源代码:
1. def updateStateByKey[S:ClassTag](
2. updateFunc: (Iterator[(K,Seq[V], Option[S])]) => Iterator[(K, S)],
3. partitioner: Partitioner,
4. rememberPartitioner:Boolean,
5. initialRDD: RDD[(K, S)]):DStream[(K, S)] = ssc.withScope {
6. val cleanedFunc =ssc.sc.clean(updateFunc)
7. val newUpdateFunc = (_: Time,it: Iterator[(K, Seq[V], Option[S])]) => {
8. cleanedFunc(it)
9. }
10. new StateDStream(self,newUpdateFunc, partitioner, rememberPartitioner, Some(initialRDD))
11. }
关键的地方是new出来一个StateDStream。
StateDStream.scala的源代码:
1. private[streaming]
2. class StateDStream[K: ClassTag, V: ClassTag, S: ClassTag](
3. parent: DStream[(K, V)],
4. updateFunc: (Time,Iterator[(K, Seq[V], Option[S])]) => Iterator[(K, S)],
5. partitioner: Partitioner,
6. preservePartitioning: Boolean,
7. initialRDD: Option[RDD[(K,S)]]
8. ) extends DStream[(K,S)](parent.ssc) {
9. super.persist(StorageLevel.MEMORY_ONLY_SER)
10.
11. override def dependencies: List[DStream[_]] =List(parent)
12.
13. override def slideDuration: Duration =parent.slideDuration
14.
15. override val mustCheckpoint = true
StateDStream本身继承至DStream,例如广告点击系统如果计算一整天的数据,数据都持久化到内存MEMORY_ONLY_SER,如果进行updateStateByKey,将不断的产生StateDStream。
StateDStream.scala的compute源代码:
1. override def compute(validTime:Time): Option[RDD[(K, S)]] = {
2.
3. // Try to get the previousstate RDD
4. getOrCompute(validTime -slideDuration) match {
5.
6. case Some(prevStateRDD)=> // If previous state RDD exists
7. // Try to get the parentRDD
8. parent.getOrCompute(validTime) match {
9. case Some(parentRDD)=> // If parent RDD exists, thencompute as usual
10. computeUsingPreviousRDD (validTime,parentRDD, prevStateRDD)
11. case None => // If parent RDD does not exist
12. // Re-apply the updatefunction to the old state RDD
13. val updateFuncLocal =updateFunc
14. val finalFunc =(iterator: Iterator[(K, S)]) => {
15. val i =iterator.map(t => (t._1, Seq[V](), Option(t._2)))
16. updateFuncLocal(validTime, i)
17. }
18. val stateRDD =prevStateRDD.mapPartitions(finalFunc, preservePartitioning)
19. Some(stateRDD)
20. }
21.
22. case None => // If previous session RDD does not exist(first input data)
23. // Try to get the parentRDD
24. parent.getOrCompute(validTime) match {
25. case Some(parentRDD)=> // If parent RDD exists, thencompute as usual
26. initialRDD match {
27. case None =>
28. // Define thefunction for the mapPartition operation on grouped RDD;
29. // first map thegrouped tuple to tuples of required type,
30. // and then applythe update function
31. valupdateFuncLocal = updateFunc
32. val finalFunc =(iterator: Iterator[(K, Iterable[V])]) => {
33. updateFuncLocal(validTime,
34. iterator.map(tuple => (tuple._1, tuple._2.toSeq, None)))
35. }
36.
37. val groupedRDD =parentRDD.groupByKey(partitioner)
38. val sessionRDD =groupedRDD.mapPartitions(finalFunc, preservePartitioning)
39. //logDebug("Generating state RDD for time " + validTime + "(first)")
40. Some (sessionRDD)
41. case Some(initialStateRDD) =>
42. computeUsingPreviousRDD(validTime,parentRDD, initialStateRDD)
43. }
44. case None => // Ifparent RDD does not exist, then nothing to do!
45. // logDebug("Notgenerating state RDD (no previous state, no parent)")
46. None
47. }
48. }
49. }
compute是StateDStream复写的方法,计算的时候有parentRDD,parentRDD会调用computeUsingPreviousRDD。重磅的地方:从中可以看出updateStateByKey的弱点。在computeUsingPreviousRDD 中通过updateFunc将函数传进来,然后通过val cogroupedRDD =parentRDD.cogroup(prevStateRDD, partitioner)计算,里面的核心逻辑是cogroup,cogroup就是对所有的数据按照key对Value进行聚合,每次计算的时候都要这样做,这样做的好处是对RDD进行计算,RDD怎么计算,cogroup就怎么计算。不好的地方是性能问题:cogroup要对所有的数据进行重新扫描,每一次都要扫描,随着时间的流失,要扫描的规模越来越大,性能也越来越低。
StateDStream.scala的computeUsingPreviousRDD源代码:
1. private [this] def computeUsingPreviousRDD(
2. batchTime: Time,
3. parentRDD: RDD[(K, V)],
4. prevStateRDD: RDD[(K, S)]) ={
5. // Define the function for themapPartition operation on cogrouped RDD;
6. // first map the cogroupedtuple to tuples of required type,
7. // and then apply the updatefunction
8. val updateFuncLocal =updateFunc
9. val finalFunc = (iterator:Iterator[(K, (Iterable[V], Iterable[S]))]) => {
10. val i = iterator.map { t=>
11. val itr = t._2._2.iterator
12. val headOption = if(itr.hasNext) Some(itr.next()) else None
13. (t._1, t._2._1.toSeq, headOption)
14. }
15. updateFuncLocal(batchTime,i)
16. }
17. val cogroupedRDD =parentRDD.cogroup(prevStateRDD, partitioner)
18. val stateRDD =cogroupedRDD.mapPartitions(finalFunc, preservePartitioning)
19. Some(stateRDD)
20. }
cogroup的RDD和另外一个RDD计算的结果是个tuple,value是(Iterable[V], Iterable[W])。 例如一个学生有学号和姓名被RDD封装;也有学号和成绩被RDD封装,两个进行cogroup,它的key就是学号,Value就是姓名和成绩;数据量比较少,或者updateStateByKey的时间比较短,时间如果太长,可以考虑定时,这里是基于磁盘进行持久化的操作,可能还不是太大的关系。但是每次都要进行全部数据的扫描,这是无法让人承受的事情。例如如果计算几天之后,就会发现越算越慢,原先计算1分钟的时候不慢,计算一段时间就变慢了。
PairRDDFunctions.scala的源代码:
1. def cogroup[W](other: RDD[(K,W)], partitioner: Partitioner)
2. : RDD[(K, (Iterable[V],Iterable[W]))] = self.withScope {
3. if(partitioner.isInstanceOf[HashPartitioner] && keyClass.isArray) {
4. throw newSparkException("HashPartitioner cannot partition array keys.")
5. }
6. val cg = newCoGroupedRDD[K](Seq(self, other), partitioner)
7. cg.mapValues { case Array(vs,w1s) =>
8. (vs.asInstanceOf[Iterable[V]],w1s.asInstanceOf[Iterable[W]])
9. }
10. }
mapWithState现在还是实验状态,实验状态的意思是还不稳定。不过我们过去的实验结果表明mapWithState还是可以的。
PairDStreamFunctions.scala的源代码:
1. /**
2. :实验性的::通过应用函数到this Stream的每一个Key-Value元素,返回 [[MapWithStateDStream]] ,同时为每个唯一Key维护一些状态数据。映射函数和其他规范(如分区、超时、初始状态数据等),转换可以使用StateSpec类指定。状态数据在映射函数中作为一个参数类型State访问。 使用mapWithState的例子:
3.
4. {{{ //一个映射函数,它维护一个整数状态并返回一个字符串。
5. def mappingFunction(key: String,value: Option[Int], state: State[Int]): Option[String] = {
6. //使用 state.exists(), state.get(),state.update() 及 state.remove() 管理 state,返回需要的字符串
7. }
8. val spec =StateSpec.function(mappingFunction).numPartitions(10)
9. val mapWithStateDStream= keyValueDStream.mapWithState[StateType, MappedType](spec)
10. }}}
11.
12. @param spec 转换的表示
13. @tparam StateType state 数据类型
14. @tparam MappedType 映射的类型
15. */
16. @Experimental
17. def mapWithState[StateType:ClassTag, MappedType: ClassTag](
18. spec: StateSpec[K, V,StateType, MappedType]
19. ): MapWithStateDStream[K, V,StateType, MappedType] = {
20. new MapWithStateDStreamImpl[K,V, StateType, MappedType](
21. self,
22. spec.asInstanceOf[StateSpecImpl[K, V,StateType, MappedType]]
23. )
24. }
mapWithState方法返回MapWithStateDStream,使用一个函数不断对我们的key-Value类型的元素进行状态维护和更新,这里面有一个历史状态,基于Key进行更新,具体更新的函数由来StateSpec来说明。mapWithState函数接受的参数spec是一个StateSpec类型的参数,StateSpec参数不是一个函数,但是在StateSpec里面封装了一个函数。
state就是历史状态,state相当于就是一个数据库,也可以想象成是一个内存数据表, state.exists(),state.get(), state.update() 及 state.remove() 判断是否存在,获取这个值,更新这个值,删除这个值,其实可以理解为相应的表操作,如删除这张表。state就是一张表,这张表中记录了状态维护中的所有历史状态,mappingFunction提出对这张表中的哪个key进行操作,基于key进行操作输入的value值是什么,通过key可以查询这张表查询值,至于value怎么操作,就是mappingFunction中的业务逻辑,state就相当于key-value中的一张表,包括key,value两列。所有的历史状态都放在这张表中,这张表的名称就叫state。
在进行更新的时候,state可以认为是表的索引,根据key在state的基础上更新它的value。内存表是从抽象的角度考虑的,这里看到state,例如单词计数,不断的累积计数,上面注释例子中的state类型也是State[Int]整数类型。如果从内存数据表的角度讲,这里是状态的标记:删除标记,超时时间等,都在内存中, 就是对一张表的增、删、改。
State.scala的源代码:
1. /**
2. ::实验性的 ::
3. 获取和更新状态映射函数用于mapWithState操作的[[org.apache.spark.streaming.dstream.PairDStreamFunctionspair DStream]](Scala)或者
4. [[org.apache.spark.streaming.api.java.JavaPairDStreamJavaPairDStream]](java)。
5.
6. Scala中使用State的例子:
7. {{{
8. // 维护整数状态并返回字符串的映射函数。
9. def mappingFunction(key:String, value: Option[Int], state: State[Int]): Option[String] = {
10. // 检查状态是否存在
11. if (state.exists) {
12. val existingState =state.get //获取存在的状态
13. val shouldRemove =... // 决定是否删除状态
14. if (shouldRemove) {
15. state.remove() //删除状态
16. } else {
17. val newState = ...
18. state.update(newState) // 设置新的状态
19. }
20. } else {
21. val initialState = ...
22. state.update(initialState) // 设置初始值
23. }
24. ... // 返回值
25. }
26.
27. }}}
28.
29. Java 使用 State的例子:
30. {{{
31. // 维护整数状态并返回字符串的映射函数。
32. Function3<String,Optional<Integer>, State<Integer>, String> mappingFunction =
33. new Function3<String,Optional<Integer>, State<Integer>, String>() {
34.
35. @Override
36. public Stringcall(String key, Optional<Integer> value, State<Integer> state) {
37. if (state.exists()) {
38. int existingState =state.get(); // 获取存在的状态
39. boolean shouldRemove= ...; // 决定是否删除状态
40. if (shouldRemove) {
41. state.remove(); //删除状态
42. } else {
43. int newState =...;
44. state.update(newState); // 设置新的状态
45. }
46. } else {
47. int initialState =...; // 设置初始状态
48. state.update(initialState);
49. }
50. //返回值
51. }
52. };
53. }}}
54. **/
55. * @tparam S 状态类
56. @Experimental
57. sealed abstract class State[S]{
58. ……..
59. private[streaming] classStateImpl[S] extends State[S] {
60.
61. private var state: S = null.asInstanceOf[S]
62. private var defined: Boolean = false
63. private var timingOut: Boolean = false
64. private var updated: Boolean = false
65. private var removed: Boolean = false
mapWithState方法中的StateSpecImpl将传进来的数据进行封装,这里面有key-Value,StateType, MappedType。StateSpecImpl是一个case class,里面的参数是函数function, StateSpecImpl中有一个很重要的方法getFunction,将函数Function返回。使用一个数据结构封装了函数的内容。这里还有getPartitioner、getInitialStateRDD、getTimeoutInterval,这也是一个很好的编程的技巧,从框架的角度封装成一个数据结构。
StateSpecImpl.scala的源代码
1. private[streaming]
2. case class StateSpecImpl[K, V, S, T](
3. function: (Time, K, Option[V],State[S]) => Option[T]) extends StateSpec[K, V, S, T] {
4.
5. require(function != null)
6.
7. @volatile private varpartitioner: Partitioner = null
8. @volatile private varinitialStateRDD: RDD[(K, S)] = null
9. @volatile private vartimeoutInterval: Duration = null
10.
11. override def initialState(rdd:RDD[(K, S)]): this.type = {
12. this.initialStateRDD = rdd
13. this
14. }
15. …..
16. private[streaming] defgetFunction(): (Time, K, Option[V], State[S]) => Option[T] = function
17.
18. private[streaming] def getInitialStateRDD():Option[RDD[(K, S)]] = Option(initialStateRDD)
19.
20. private[streaming] def getPartitioner():Option[Partitioner] = Option(partitioner)
21.
22. private[streaming] def getTimeoutInterval():Option[Duration] = Option(timeoutInterval)
MapWithStateDStreamImpl传入dataStream、StateSpecImpl。在compute方法中将业务逻辑交给internalStream,InternalMapWithStateDStream是一个内部类。MapWithStateDStreamImpl.scala的源代码:
1. private[streaming] classMapWithStateDStreamImpl[
2. KeyType: ClassTag, ValueType:ClassTag, StateType: ClassTag, MappedType: ClassTag](
3. dataStream: DStream[(KeyType,ValueType)],
4. spec: StateSpecImpl[KeyType,ValueType, StateType, MappedType])
5. extendsMapWithStateDStream[KeyType, ValueType, StateType,MappedType](dataStream.context) {
6. ……
7. private val internalStream =
8. new InternalMapWithStateDStream[KeyType,ValueType, StateType, MappedType](dataStream, spec)
9.
10. override def slideDuration: Duration =internalStream.slideDuration
11.
12. override def dependencies: List[DStream[_]] =List(internalStream)
13.
14. override def compute(validTime: Time):Option[RDD[MappedType]] = {
15. internalStream.getOrCompute(validTime).map{ _.flatMap[MappedType] { _.mappedData } }
16. }
InternalMapWithStateDStream类在历史的基础上进行更新, persist是MEMORY_ONLY的方式,不断的更新内部数据结构,而不是创建一个新的数据结构对象。
MapWithStateDStream.scala的源代码:
1. private[streaming]
2. class InternalMapWithStateDStream[K: ClassTag, V: ClassTag, S: ClassTag,E: ClassTag](
3. parent: DStream[(K, V)], spec:StateSpecImpl[K, V, S, E])
4. extendsDStream[MapWithStateRDDRecord[K, S, E]](parent.context) {
5. persist(StorageLevel.MEMORY_ONLY)
6.
7. private val partitioner =spec.getPartitioner().getOrElse(
8. newHashPartitioner(ssc.sc.defaultParallelism))
9.
10. private val mappingFunction =spec.getFunction()
11.
12. override def slideDuration: Duration =parent.slideDuration
MapWithStateDStream的compute创建一个新的RDD,新的RDD基于BatchDuration时间窗口,当前传进来一个时间,数据可能为空。如果时间里面没有数据,就获取emptyRDD。关键的一行代码是Some(new MapWithStateRDD( prevStateRDD, partitionedDataRDD,mappingFunction, validTime, timeoutThresholdTime)), 这里有prevStateRDD,partitionedDataRDD,没有看到历史数据,可以看到mappingFunction, 在伴生对象InternalMapWithStateDStream中有CHECKPOINT的时间。
MapWithStateDStream.scala的源代码:
1. override def compute(validTime:Time): Option[RDD[MapWithStateRDDRecord[K, S, E]]] = {
2. // Get the previous state orcreate a new empty state RDD
3. val prevStateRDD =getOrCompute(validTime - slideDuration) match {
4. case Some(rdd) =>
5. if (rdd.partitioner !=Some(partitioner)) {
6. // If the RDD is notpartitioned the right way, let us repartition it using the
7. // partition index asthe key. This is to ensure that state RDD is always partitioned
8. // before creatinganother state RDD using it
9. MapWithStateRDD.createFromRDD[K, V,S, E](
10. rdd.flatMap {_.stateMap.getAll() }, partitioner, validTime)
11. } else {
12. rdd
13. }
14. case None =>
15. MapWithStateRDD.createFromPairRDD[K, V,S, E](
16. spec.getInitialStateRDD().getOrElse(newEmptyRDD[(K, S)](ssc.sparkContext)),
17. partitioner,
18. validTime
19. )
20. }
21.
22.
23. // Compute the new state RDDwith previous state RDD and partitioned data RDD
24. // Even if there is no dataRDD, use an empty one to create a new state RDD
25. val dataRDD =parent.getOrCompute(validTime).getOrElse {
26. context.sparkContext.emptyRDD[(K, V)]
27. }
28. val partitionedDataRDD =dataRDD.partitionBy(partitioner)
29. val timeoutThresholdTime =spec.getTimeoutInterval().map { interval =>
30. (validTime -interval).milliseconds
31. }
32. Some(new MapWithStateRDD(
33. prevStateRDD,partitionedDataRDD, mappingFunction, validTime, timeoutThresholdTime))
34. }
35. }
36. …..
37. private[streaming] objectInternalMapWithStateDStream {
38. private valDEFAULT_CHECKPOINT_DURATION_MULTIPLIER = 10
39. }
所有精彩的内容从MapWithStateRDD开始,MapWithStateRDD是一个RDD,作为一个RDD,包含了MapWithState中的数据及具体怎么操作,每个分区被MapWithStateRDDRecord代表,这里面有数据结构StateMap,维护的是数据的状态。
MapWithStateRDD.scala的源代码:
1. /** RDD存储mapWithState的Key的状态和相应的映射数据。RDD每个分区具有[[MapWithStateRDDRecord]]数据类型的记录。这包含了StateMap(包含key的状态)和记录的顺序,通过mapWithState函数返回。
2. @param prevStateRDD 以前的MapWithStateRDD on whose StateMap data`this` RDD
3. will becreated
4. @param partitionedDataRDD Thepartitioned data RDD which is used update the previous StateMaps
5. in the`prevStateRDD` to create `this` RDD
6. @param mappingFunction The function that will be used to updatestate and return new data
7. @param batchTime The time of the batch to which this RDDbelongs to. Use to update
8. @param timeoutThresholdTime Thetime to indicate which keys are timeout
9. **/
10. private[streaming] classMapWithStateRDD[K: ClassTag, V: ClassTag, S: ClassTag, E: ClassTag](
11. private var prevStateRDD:RDD[MapWithStateRDDRecord[K, S, E]],
12. private varpartitionedDataRDD: RDD[(K, V)],
13. mappingFunction: (Time, K,Option[V], State[S]) => Option[E],
14. batchTime: Time,
15. timeoutThresholdTime:Option[Long]
16. ) extendsRDD[MapWithStateRDDRecord[K, S, E]](
17. partitionedDataRDD.sparkContext,
18. List(
19. newOneToOneDependency[MapWithStateRDDRecord[K, S, E]](prevStateRDD),
20. newOneToOneDependency(partitionedDataRDD))
21. ) {
MapWithStateRDD中的重点是compute,获取RDD的迭代器prevStateRDDIterator、dataIterator,最后返回一个 Iterator(newRecord)
MapWithStateRDD.scala的源代码:
1. override def compute(
2. partition: Partition,context: TaskContext): Iterator[MapWithStateRDDRecord[K, S, E]] = {
3.
4. val stateRDDPartition =partition.asInstanceOf[MapWithStateRDDPartition]
5. val prevStateRDDIterator =prevStateRDD.iterator(
6. stateRDDPartition.previousSessionRDDPartition,context)
7. val dataIterator =partitionedDataRDD.iterator(
8. stateRDDPartition.partitionedDataRDDPartition,context)
9.
10. val prevRecord = if(prevStateRDDIterator.hasNext) Some(prevStateRDDIterator.next()) else None
11. val newRecord =MapWithStateRDDRecord.updateRecordWithData(
12. prevRecord,
13. dataIterator,
14. mappingFunction,
15. batchTime,
16. timeoutThresholdTime,
17. removeTimedoutData =doFullScan // remove timedout data only when full scan is enabled
18. )
19. Iterator(newRecord)
20. }
MapWithStateRDDRecord中有2个关键的数据结构:mappedData、wrappedState。newStateMap的数据结构中先对StateMap进行copy,这里copy还是很高效的,然后是dataIterator.foreach 循环遍历,不断的给wrappedState赋值,mappedData是最后返回的值,每次操作之后,判断是否要删除,进行删除操作。对当前的Batch的数据进行计算,对newStateMap进行更新,newStateMap的数据结构保存了整个历史数据,可进行删除、更新操作,有没有对历史数据重新计算或者遍历?没有,没有cogroup的操作,对当前数据进行操作,只在内存中更新数据结构。效率会高很多。
MapWithStateRDD.scala的源代码:
1. private[streaming] objectMapWithStateRDDRecord {
2. def updateRecordWithData[K: ClassTag, V:ClassTag, S: ClassTag, E: ClassTag](
3. prevRecord: Option[MapWithStateRDDRecord[K,S, E]],
4. dataIterator: Iterator[(K, V)],
5. mappingFunction: (Time, K, Option[V],State[S]) => Option[E],
6. batchTime: Time,
7. timeoutThresholdTime: Option[Long],
8. removeTimedoutData: Boolean
9. ): MapWithStateRDDRecord[K, S, E] = {
10. // Create a new state map by cloning theprevious one (if it exists) or by creating an empty one
11. val newStateMap = prevRecord.map {_.stateMap.copy() }. getOrElse { new EmptyStateMap[K, S]() }
12.
13. val mappedData = new ArrayBuffer[E]
14. val wrappedState = new StateImpl[S]()
15.
16. // Call the mapping function on each recordin the data iterator, and accordingly
17. // update the states touched, and collectthe data returned by the mapping function
18. dataIterator.foreach { case (key, value)=>
19. wrappedState.wrap(newStateMap.get(key))
20. val returned = mappingFunction(batchTime,key, Some(value), wrappedState)
21. if (wrappedState.isRemoved) {
22. newStateMap.remove(key)
23. } else if (wrappedState.isUpdated
24. || (wrappedState.exists &&timeoutThresholdTime.isDefined)) {
25. newStateMap.put(key,wrappedState.get(), batchTime.milliseconds)
26. }
27. mappedData ++= returned
28. }
29.
30. // Get the timed out state records, callthe mapping function on each and collect the
31. // data returned
32. if (removeTimedoutData &&timeoutThresholdTime.isDefined) {
33. newStateMap.getByTime(timeoutThresholdTime.get).foreach{ case (key, state, _) =>
34. wrappedState.wrapTimingOutState(state)
35. val returned =mappingFunction(batchTime, key, None, wrappedState)
36. mappedData ++= returned
37. newStateMap.remove(key)
38. }
39. }
40.
41. MapWithStateRDDRecord(newStateMap,mappedData)
42. }
43. }
最终返回MapWithStateRDDRecord。从RDD的角度讲,partition没有变,但是内部变了。原来的RDD直接指向一条数据,数据不可以修改。现在也是指向一条数据,但是数据进行了封装,可以改变里面的内容,但是从RDD的角度讲,这里的数据并没有变,这里的设计很巧妙的。MapWithStateRDDRecord就代表了当前的partition,Dstream操作是RDD,MapWithStateRDDRecord没变,但里面的内容变了就管不了,借助了RDD的不变性,又整合MapWithStateRDDRecord的可变性,高效的完成了整个过程。
一个额外的结论:RDD本身不可以变,不可变的RDD也可以处理变化的数据,自定义的RDD的数据结构要注意一下。RDD是不可变的,这是对的;RDD处理的数据也不可变,当然是错的!通过MapWithStateRDDRecord非常清楚的看见这一点。RDD是不可变是没有问题的,但RDD只能处理数据源不变的数据呢?当然不是,数据源可以变化,在RDD里面你自己负责这里的变化并维护里面的数据,这是一个非常重要的结论。
- 第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- 第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- 第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- 第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Spark定制班第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- 14:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Spark 定制版:014~Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Spark Streaming源码解读之State管理之UpdataStateByKey和MapWithState解密
- Spark Streaming之updateStateByKey和mapWithState比较
- Spark学习笔记(14)State管理之updateStateByKey解密
- 第93课:Spark Streaming updateStateByKey案例实战和内幕源码解密
- Spark updateStateByKey和mapWithState
- Spark定制班第16课:Spark Streaming源码解读之数据清理内幕彻底解密
- 第129课:Spark streaming源码经典解读系统之四:GobGenerator工作内幕源码解密
- 第131课: Spark Streaming源码经典解读系列之六:ReceiverTracker工作内幕源码解密
- 第16课:Spark Streaming源码解读之数据清理内幕彻底解密
- 【bzoj2330】【P3275 】【SCOI2011】糖果
- 约瑟夫环报数,直到剩下k-1人停止
- word2010中设置页码起始页从任意一页开始
- MySQL添加用户、删除用户与授权
- 【jzoj5368】【NOIP2017提高A组模拟9.16】【为逝去的公主献上的七重樱】【单调队列】
- 第14课:Spark Streaming源码解读之State管理之updateStateByKey和mapWithState解密
- Win10各个版本免费激活密钥
- 3dMax第一章:界面操作
- 用微信公众号理解观察者模式
- WEB0911-0913
- 相同的和
- markdown学习
- 软第一期项目心得与总结
- Geoffrey Hinton:放弃反向传播,我们的人工智能需要重头再来