分享

Apache Spark源码走读之5-- DStream处理的容错性分析

本帖最后由 pig2 于 2015-1-6 14:10 编辑
问题导读
1、如何理解重启之后数据也会被再次处理?
2、数据处理过程的过程有哪些?
3、checkpoint的数据是通过什么真正写入到hdfs中的?





在流数据的处理过程中,为了保证处理结果的可信度(不能多算,也不能漏算),需要做到对所有的输入数据有且仅有一次处理。在Spark Streaming的处理机制中,不能多算,比较容易理解。那么它又是如何作到即使数据处理结点被重启,在重启之后这些数据也会被再次处理呢?

环境搭建
为了有一个感性的认识,先运行一下简单的Spark Streaming示例。首先确认已经安装了openbsd-netcat。

运行netcat
  1. nc -lk 9999
复制代码


运行spark-shell
  1. SPARK_JAVA_OPTS=-Dspark.cleaner.ttl=10000 MASTER=local-cluster[2,2,1024] bin/spark-shell
复制代码


在spark-shell中输入如下内容
  1. import org.apache.spark.streaming._
  2. import org.apache.spark.streaming.StreamingContext._
  3. val ssc = new StreamingContext(sc, Seconds(3))
  4. val lines = ssc.socketTextStream("localhost", 9999)
  5. val words = lines.flatMap( _.split(" "))
  6. val pairs = words.map(word => (word,1))
  7. val wordCount = pairs.reduceByKey(_ + _)
  8. wordCount.print()
  9. ssc.start()
  10. ssc.awaitTermination()
复制代码

当ssc.start()执行之后,在nc一侧输入一些内容并回车,spark-shell上就会显示出统计的结果。


数据接收过程
来看一下代码实现层面,从两个角度来说,一是控制层面(control panel),另一是数据层面(data panel)。

Spark Streaming的数据接收过程的控制层面大致如下图所示。
1.png



简要讲解一下上图的意思,
  • 数据真正接收到是发生在SocketReceiver.receive函数中,将接收到的数据放入到BlockGenerator.currentBuffer
  • 在BlockGenerator中有一个重复定时器,处理函数为updateCurrentBuffer, updateCurrentBuffer将当前buffer中的数据封装为一个新的Block,放入到blocksForPush队列中
  • 同样是在BlockGenerator中有一个BlockPushingThread,其职责就是不停的将blocksForPush队列中的成员通过pushArrayBuffer函数传递给blockmanager,让BlockManager将数据存储到MemoryStore中
  • pushArrayBuffer还会将已经由BlockManager存储的Block的id号传递给ReceiverTracker,ReceiverTracker会将存储的blockId放到对应StreamId的队列中

  1. socket.receive->receiver.store->pushSingle->blockgenerator.updateCurrentBuffer->blockgenerator.keepPushBlocks->pushArrayBufer
  2. ->ReceiverTracker.addBlocks
复制代码



pushArrayBuffer函数的定义如下
  1.   def pushArrayBuffer(
  2.       arrayBuffer: ArrayBuffer[_],
  3.       optionalMetadata: Option[Any],
  4.       optionalBlockId: Option[StreamBlockId]
  5.     ) {
  6.     val blockId = optionalBlockId.getOrElse(nextBlockId)
  7.     val time = System.currentTimeMillis
  8.     blockManager.put(blockId, arrayBuffer.asInstanceOf[ArrayBuffer[Any]],
  9.       storageLevel, tellMaster = true)
  10.     logDebug("Pushed block " + blockId + " in " + (System.currentTimeMillis - time)  + " ms")
  11.     reportPushedBlock(blockId, arrayBuffer.size, optionalMetadata)
  12.   }
复制代码


数据结构的变化过程
Spark Streaming数据处理高效的原因之一就是批量的进行数据分析,那么这些批量的数据是如何聚集起来的呢?换种方式来表述这个问题,在某一时刻,接收到的数据是单一的,也就是我们最多只能组成<t,data>这种数据元组,而在runJob的时候是批量的提取和分析数据的,这个批量数据的组成是在什么时候完成的呢?

下图大到勾勒出一条新的message被socketreceiver接收之后,是如何通过一系列的处理而放入到BlockManager中,并同时由ReceiverTracker记录下相应的元数据的。
1.png


  • 首先new message被放入到blockManager.currentBuffer
  • 定时器超时处理过程,将整个currentBuffer中的数据打包成一条Block,放入到ArrayBlockingQueue,该数据结构支持FIFO
  • keepPushingBlocks将每一条Block(block中包含时间戳,接收到的原始数据)让BlockManager进行保存,同时通知ReceiverTracker已经将哪些block存储到了blockmanager中
  • ReceiverTracker将每一个stream接收到但还没有进行处理的block放入到receiverBlockInfo,其为一Hashmap. 在后面的generateJobs中会从receiverBlockInfo提取数据以生成相应的RDD


数据处理过程
数据处理中最重要的函数就是generateJobs, generateJobs会引发下述的函数调用过程,具体的代码就不一一罗列了。
  1. jobgenerator.generateJobs->dstreamgraph.generateJobs->dstream.generateJob->getOrCompute->compute 生成RDD
复制代码

job调用job.func
JobGenerator.generateJobs函数定义如下
  1.   private def generateJobs(time: Time) {
  2.     SparkEnv.set(ssc.env)
  3.     Try(graph.generateJobs(time)) match {
  4.       case Success(jobs) =>
  5.         val receivedBlockInfo = graph.getReceiverInputStreams.map { stream =>
  6.           val streamId = stream.id
  7.           val receivedBlockInfo = stream.getReceivedBlockInfo(time)
  8.           (streamId, receivedBlockInfo)
  9.         }.toMap
  10.         jobScheduler.submitJobSet(JobSet(time, jobs, receivedBlockInfo))
  11.       case Failure(e) =>
  12.         jobScheduler.reportError("Error generating jobs for time " + time, e)
  13.     }
  14.     eventActor ! DoCheckpoint(time)
  15.   }
复制代码

我们先来谈一谈数据处理阶段是如何与上述的接收阶段中存储下来的数据挂上钩的。

假设上一次进行RDD处理发生在时间点t1,现在是时间点t2,那么在<t2,t1>之间有哪些blocks没有被处理呢?

想必你已经知道答案了,没有被处理的blocks全部保存在ReceiverTracker的receiverBlockInfo之中

在generateJob时,每一个DStream都会调用getReceivedBlockInfo,你说没有跟ReceiverTracker中的receivedBlockInfo连起来啊,别急!且看数据输入的源头ReceiverInputDStream中的getReceivedBlockInfo是如何定义的。代码列举如下。
  1.   private[streaming] def getReceivedBlockInfo(time: Time) = {
  2.     receivedBlockInfo(time)
  3.   }
复制代码


那么此处的receivedBlockInfo(time)是从何而来的呢,这个要看ReceivedInputDStream中的compute函数实现
  1. override def compute(validTime: Time): Option[RDD[T]] = {
  2.     // If this is called for any time before the start time of the context,
  3.     // then this returns an empty RDD. This may happen when recovering from a
  4.     // master failure
  5.     if (validTime >= graph.startTime) {
  6.       val blockInfo = ssc.scheduler.receiverTracker.getReceivedBlockInfo(id)
  7.       receivedBlockInfo(validTime) = blockInfo
  8.       val blockIds = blockInfo.map(_.blockId.asInstanceOf[BlockId])
  9.       Some(new BlockRDD[T](ssc.sc, blockIds))
  10.     } else {
  11.       Some(new BlockRDD[T](ssc.sc, Array[BlockId]()))
  12.     }
  13.   }
复制代码


至此终于看到了receiverTracker中的getReceivedBlockInfo被调用,也就是说将接收阶段的数据和目前处理阶段的输入通道打通了

函数调用路径,从generateJobs到sparkcontext.submitJobs. 这个时候要注意注册为DStreamGraph中的OutputStream上的操作会引发SparkContext.runJobs被调用。我们以print函数为例看一下调用过程。
  1.   def print() {
  2.     def foreachFunc = (rdd: RDD[T], time: Time) => {
  3.       val first11 = rdd.take(11)
  4.       println ("-------------------------------------------")
  5.       println ("Time: " + time)
  6.       println ("-------------------------------------------")
  7.       first11.take(10).foreach(println)
  8.       if (first11.size > 10) println("...")
  9.       println()
  10.     }
  11.     new ForEachDStream(this, context.sparkContext.clean(foreachFunc)).register()
  12.   }
复制代码

注意rdd.take,这个会引发runJob调用,不信的话,我们可以看一看其定义中调用runJob的片段。
  1.       val left = num - buf.size
  2.       val p = partsScanned until math.min(partsScanned + numPartsToTry, totalParts)
  3.       val res = sc.runJob(this, (it: Iterator[T]) => it.take(left).toArray, p, allowLocal = true)
  4.       res.foreach(buf ++= _.take(num - buf.size))
  5.       partsScanned += numPartsToTry
  6.     }
复制代码



小结一下数据处理过程
用time为关键字去取出在此时间之前加入的所有blockIds
真正提交运行的时候,rdd中的blockfetcher以blockId为关键字去blockmanagermaster获取真正的数据,即从socket上接收到的原始数据

容错处理
JobGenerator.generateJobs函数的最后会发出DoCheckpoint通知,该通知会让相应的actor将DStreamCheckpointData写入到hdfs文件中,我们来看一看为什么需要写入checkpointdata以及哪些东西是包含在checkpointdata之中。
1.png



在数据处理一节,我们已经分析到在generateJobs的时候会生成多个jobs,它们会通过sparkcontext.runJob接口而发送到cluster中被真正执行。

假设在t2,worker挂掉了,挂掉的worker直到t3才完全恢复。由于挂掉的原因,上一次generateJobs生成的job不一定被完全处理了(也许有些已经处理了,有些还没有处理),所以需要重新再提交一次。这里有一个问题,那就是可能导致针对同一批数据有重复处理的情况发生,从而无法达到exactly-once的语义效果。

问题2: 在<t2,t3>这一段挂掉的时间之内,没有新的数据被接收,所以Spark Streaming的SocketReceiver适合用来充当client侧而不是server侧。SocketReceiver读取到的数据应该存在一个具有冗余备份机制的内存数据库或缓存队列里,如kafaka. 对问题2, Spark Streaming本身是解决不了的。当然这里再往下细究的话,会牵出负载均衡的问题。

checkpointData
checkpoint的成员变量有哪些呢,我们看一看其结构定义就清楚了。
  1.   val master = ssc.sc.master
  2.   val framework = ssc.sc.appName
  3.   val sparkHome = ssc.sc.getSparkHome.getOrElse(null)
  4.   val jars = ssc.sc.jars
  5.   val graph = ssc.graph
  6.   val checkpointDir = ssc.checkpointDir
  7.   val checkpointDuration = ssc.checkpointDuration
  8.   val pendingTimes = ssc.scheduler.getPendingTimes().toArray
  9.   val delaySeconds = MetadataCleaner.getDelaySeconds(ssc.conf)
  10.   val sparkConfPairs = ssc.conf.getAll
复制代码


generatedRDDs是被包含在graph里面。所以不要突然之间惊惶失措,发觉没有将generatedRDDs保存起来。

checkpoint的数据是通过CheckpointwriteHandler真正的写入到hdfs,通过CheckPiontReader而读入。CheckpointReade在重启的时候会被使用到,判断是第一次干净的启动还是因错误而重启,判断的依据全部在cp这个变量。

为了达到重启之后而自动的检查并载入相应的checkpoint数据,那么在创建StreamingContext的时候就不能简单的通过调用new StreamingContext来完成,而是利用getOrCreate函数,代码示例如下。
  1. // Function to create and setup a new StreamingContext
  2. def functionToCreateContext(): StreamingContext = {
  3.     val ssc = new StreamingContext(...)   // new context
  4.     val lines = ssc.socketTextStream(...) // create DStreams
  5.     ...
  6.     ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
  7.     ssc
  8. }
  9. // Get StreaminContext from checkpoint data or create a new one
  10. val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
  11. // Do additional setup on context that needs to be done,
  12. // irrespective of whether it is being started or restarted
  13. context. ...
  14. // Start the context
  15. context.start()
  16. context.awaitTermination()
复制代码


小结
本文中讲述数据接收过程中所使用的两幅图使用tikz完成,里面包含的信息很丰富,有志于了解清楚Spark Streaming内部处理机制的同仁,不妨以此为参考进行详细的代码走读。



相关内容


Apache Spark源码走读之1 -- Spark论文阅读笔记

Apache Spark源码走读之2 -- Job的提交与运行

Apache Spark源码走读之3-- Task运行期之函数调用关系分析

Apache Spark源码走读之4 -- DStream实时流数据处理


Apache Spark源码走读之6-- 存储子系统分析

Apache Spark源码走读之7 -- Standalone部署方式分析

Apache Spark源码走读之8 -- Spark on Yarn

Apache Spark源码走读之9 -- Spark源码编译

Apache Spark源码走读之10 -- 在YARN上运行SparkPi

Apache Spark源码走读之11 -- sql的解析与执行

Apache Spark源码走读之12 -- Hive on Spark运行环境搭建

Apache Spark源码走读之13 -- hiveql on spark实现详解

Apache Spark源码走读之14 -- Graphx实现剖析

Apache Spark源码走读之15 -- Standalone部署模式下的容错性分析

Apache Spark源码走读之16 -- spark repl实现详解

Apache Spark源码走读之17 -- 如何进行代码跟读

Apache Spark源码走读之18 -- 使用Intellij idea调试Spark源码

Apache Spark源码走读之19 -- standalone cluster模式下资源的申请与释放

Apache Spark源码走读之20 -- ShuffleMapTask计算结果的保存与读取

Apache Spark源码走读之21 -- WEB UI和Metrics初始化及数据更新过程分析

Apache Spark源码走读之22 -- 浅谈mllib中线性回归的算法实现

Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

Apache Spark源码走读之24 -- Sort-based Shuffle的设计与实现


http://www.cnblogs.com/hseagle/p/3673139.html

已有(1)人评论

跳转到指定楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条