第16课:Spark Streaming源码解读之数据清理内幕彻底解密

原创 2016年05月31日 16:52:56

本讲从二个方面阐述:

数据清理原因和现象

数据清理代码解析

 

Spark Core从技术研究的角度讲对Spark Streaming研究的彻底,没有你搞不定的Spark应用程序。

Spark Streaming一直在运行,不断计算,每一秒中在不断运行都会产生大量的累加器、广播变量,所以需要对对象及元数据需要定期清理。每个batch duration运行时不断触发job后需要清理rdd和元数据。Clinet模式

可以看到打印的日志,从文件日志也可以看到清理日志内容。

现在要看其背后的事情:

Spark运行在jvm上,jvm会产生对象,jvm需要对对象进行回收工作,如果我们不管理gc(对象产生和回收),jvm很快耗尽。现在研究的是SparkStreaming的Spark GC。Spark Streaming对rdd的数据管理、元数据管理相当jvm对gc管理。

数据、元数据是操作DStream时产生的,数据、元数据的回收则需要研究DStream的产生和回收。

 

看下DStream的继承结构:

abstract class DStream[T:ClassTag] (
    @transient private[streaming] var ssc:StreamingContext
  ) extends Serializable with Logging {

接收数据靠InputDStream,数据输入、数据操作、数据输出,整个生命周期都是基于DStream构建的;得出结论:DStream负责rdd的生命周期,rdd是DStream产生的,对rdd的操作也是对DStream的操作,所以不断产生batchDuration的循环,所以研究对rdd的操作也就是研究对DStream的操作。

 

源码分析:

foreachRDD会触发ForEachDStream:

 

物化存储在外部设备上

/**
 * Apply a function to each RDD in thisDStream. This is an output operator, so
 * 'this' DStream will be registered asan output stream and therefore materialized.
 *
 *
@deprecated As of 0.9.0, replaced by `foreachRDD`.
 */
@deprecated("useforeachRDD", "0.9.0")
def foreach(foreachFunc: (RDD[T],Time) => Unit): Unit = ssc.withScope {
  this.foreachRDD(foreachFunc)
}

private def foreachRDD(
    foreachFunc: (RDD[T], Time) => Unit,
    displayInnerRDDOps: Boolean): Unit ={
  new ForEachDStream(this,
   context.sparkContext.clean(foreachFunc, false),displayInnerRDDOps).register()
}

再看DStream源码foreachRDD:

private[streaming]
class ForEachDStream[T: ClassTag] (
    parent: DStream[T],
    foreachFunc: (RDD[T], Time) => Unit,
    displayInnerRDDOps: Boolean
  ) extends DStream[Unit](parent.ssc) {

  override def dependencies: List[DStream[_]] = List(parent)

  override def slideDuration: Duration = parent.slideDuration

  override def compute(validTime: Time): Option[RDD[Unit]] = None

  override def generateJob(time: Time): Option[Job] = {
    parent.getOrCompute(time) match {
      case Some(rdd) =>
        val jobFunc= () => createRDDWithLocalProperties(time, displayInnerRDDOps) {
          foreachFunc(rdd, time)
        }
        Some(new Job(time, jobFunc))
      case None=> None
    }
  }
}

/**
 * Get the RDD corresponding to the giventime; either retrieve it from cache
 * or compute-and-cache it.
 */
//此时put函数中的RDD是最后一个RDD,虽然触发Job是基于时间,但是也是基于DStream的action的。
private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
  // If RDD was already generated, then retrieve it fromHashMap,
  // or else compute the RDD
  //
基于时间生成RDD
 
generatedRDDs
.get(time).orElse {
    // Compute the RDD if time is valid (e.g. correct time ina sliding window)
    // of RDD generation, else generatenothing.
   
if (isTimeValid(time)){

      valrddOption =createRDDWithLocalProperties(time, displayInnerRDDOps = false) {
        // Disable checks for existing output directories in jobslaunched by the streaming
        // scheduler, since we may needto write output to an existing directory during checkpoint
        // recovery; see SPARK-4835 formore details. We need to have this call here because
        // compute() might cause Sparkjobs to be launched.
       
PairRDDFunctions.disableOutputSpecValidation.withValue(true) {
          compute(time)
        }
      }
      //然后对generated RDD进行checkpoint
     
rddOption.foreach { case newRDD=>
        // Register the generated RDD for caching andcheckpointing
       
if (storageLevel != StorageLevel.NONE) {
          newRDD.persist(storageLevel)
          logDebug(s"Persisting RDD ${newRDD.id} for time $time to $storageLevel")
        }
        if (checkpointDuration != null &&(time - zeroTime).isMultipleOf(checkpointDuration)) {
          newRDD.checkpoint()
          logInfo(s"Marking RDD ${newRDD.id} for time $time for checkpointing")
        }
        //以时间为Key,RDD为Value,此时的RDD为最后一个RDD
        generatedRDDs
.put(time, newRDD)
      }
      rddOption
    } else {
      None
    }
  }
}

 

维护的是时间窗口及时间窗口下的RDD

// RDDs generated,marked as private[streaming] so that testsuites can access it
@transient
private[streaming] vargeneratedRDDs = new HashMap[Time,RDD[T]] ()

 

通过对DirectKafkaInputDStream会产生kafkardd:

override def compute(validTime: Time): Option[KafkaRDD[K, V, U, T, R]] = {
  val untilOffsets= clamp(latestLeaderOffsets(maxRetries))
  val rdd = KafkaRDD[K, V, U, T, R](
    context.sparkContext, kafkaParams, currentOffsets, untilOffsets, messageHandler)

  // Report the record number and metadata of this batchinterval to InputInfoTracker.
 
val offsetRanges= currentOffsets.map { case (tp,fo) =>
    val uo =untilOffsets(tp)
    OffsetRange(tp.topic,tp.partition, fo, uo.offset)
  }
  val description= offsetRanges.filter { offsetRange =>
    // Don't display empty ranges.
   
offsetRange.fromOffset != offsetRange.untilOffset
  }.map { offsetRange =>
    s"topic: ${offsetRange.topic}\tpartition: ${offsetRange.partition}\t" +
      s"offsets: ${offsetRange.fromOffset} to ${offsetRange.untilOffset}"
 
}.mkString("\n")
  // Copy offsetRanges to immutable.List to prevent frombeing modified by the user
 
val metadata= Map(
    "offsets" -> offsetRanges.toList,
    StreamInputInfo.METADATA_KEY_DESCRIPTION -> description)
  val inputInfo= StreamInputInfo(id, rdd.count, metadata)
  ssc.scheduler.inputInfoTracker.reportInfo(validTime, inputInfo)

  currentOffsets = untilOffsets.map(kv => kv._1 -> kv._2.offset)
  Some(rdd)
}

override def compute(thePart: Partition, context:TaskContext): Iterator[R] = {
  val part =thePart.asInstanceOf[KafkaRDDPartition]
  assert(part.fromOffset <=part.untilOffset, errBeginAfterEnd(part))
  if (part.fromOffset== part.untilOffset) {
    log.info(s"Beginning offset ${part.fromOffset} is the same as ending offset " +
      s"skipping ${part.topic}${part.partition}")
    Iterator.empty
 
} else {
    new KafkaRDDIterator(part,context)
  }
}

DStream随着时间进行,不断在内存数据结构,generatorRDD中时间窗口和窗口下的rdd实例,

按照batchDuration存储rdd以及删除掉rdd的。有时候会调用DStream的cache操作,cache就是persist操作,其实是对rdd的cache操作。

generatedRDD对象,释放RDD的时候需要考虑三方面:

Rdd本身释放,产生RDD的时候就数据源(数据来源),源数据matadata,释放rdd时三方面都需要考虑。数据周期性产生和周期性释放,需要找到时钟,需要找jobGenerator下的时钟:

//在jobGenerator中,匿名函数会随着时间不断的推移反复被调用。
//这里的timer是RecurringTimer。RecurringTimer的start方法会启动内置线程thread.
private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
  //匿名函数,复制给callback

//根据时间发给eventloop,这边receive的时候不断的有generatorjobs产生:
 
longTime => eventLoop.post(GenerateJobs(newTime(longTime))), "JobGenerator")

//匿名内部类
eventLoop
= new EventLoop[JobGeneratorEvent]("JobGenerator") {
  override protected def onReceive(event: JobGeneratorEvent): Unit = processEvent(event)

/** Processes allevents */
private def processEvent(event: JobGeneratorEvent) {
  logDebug("Got event " + event)
  event match {
    case GenerateJobs(time)=> generateJobs(time)
    case ClearMetadata(time)=> clearMetadata(time)
    case DoCheckpoint(time,clearCheckpointDataLater) =>
      doCheckpoint(time,clearCheckpointDataLater)
    case ClearCheckpointData(time)=> clearCheckpointData(time)
  }
}

/** Clear DStreammetadata for the given `time`. */
private def clearMetadata(time: Time) {
  ssc.graph.clearMetadata(time)

  // If checkpointing is enabled, then checkpoint,
  // else mark batch to be fullyprocessed
 
if (shouldCheckpoint) {
    eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = true))
  } else {
    // If checkpointing is not enabled, then delete metadatainformation about
    // received blocks (block data notsaved in any case). Otherwise, wait for
    // checkpointing of this batch tocomplete.
   
val maxRememberDuration= graph.getMaxInputStreamRememberDuration()
    jobScheduler.receiverTracker.cleanupOldBlocksAndBatches(time - maxRememberDuration)
    jobScheduler.inputInfoTracker.cleanup(time - maxRememberDuration)
    markBatchFullyProcessed(time)
  }
}

def clearMetadata(time: Time) {
  logDebug("Clearing metadata for time " + time)
  this.synchronized{
    outputStreams.foreach(_.clearMetadata(time))
  }
  logDebug("Cleared old metadata for time " + time)
}

/**
 * Clear metadata that are older than
`rememberDuration` ofthis DStream.
 * This is an internal method that shouldnot be called directly. This default
 * implementation clears the oldgenerated RDDs. Subclasses of DStream may override
 * this to clear their own metadata alongwith the generated RDDs.
 */
private[streaming] def clearMetadata(time:Time) {
  val unpersistData= ssc.conf.getBoolean("spark.streaming.unpersist", true)
  val oldRDDs= generatedRDDs.filter(_._1 <= (time - rememberDuration))
  logDebug("Clearing references to old RDDs: [" +
    oldRDDs.map(x => s"${x._1} -> ${x._2.id}").mkString(", ") + "]")
  generatedRDDs --= oldRDDs.keys
  if (unpersistData){
    logDebug("Unpersisting old RDDs: " + oldRDDs.values.map(_.id).mkString(", "))
    oldRDDs.values.foreach { rdd =>
      rdd.unpersist(false)
      // Explicitly remove blocks of BlockRDD
     
rdd match {
        case b:BlockRDD[_] =>
          logInfo("Removing blocks of RDD " + b + " of time " + time)
          b.removeBlocks()
        case _=>
      }
    }
  }
  logDebug("Cleared " + oldRDDs.size + " RDDs that were older than " +
    (time - rememberDuration) + ":" +oldRDDs.keys.mkString(","))
 dependencies.foreach(_.clearMetadata(time))
}

 

private def handleJobCompletion(job: Job, completedTime:Long) {
  val jobSet= jobSets.get(job.time)
  jobSet.handleJobCompletion(job)
  job.setEndTime(completedTime)
  listenerBus.post(StreamingListenerOutputOperationCompleted(job.toOutputOperationInfo))
  logInfo("Finished job " + job.id + " from job set of time " + jobSet.time)
  if (jobSet.hasCompleted){
    jobSets.remove(jobSet.time)
    jobGenerator.onBatchCompletion(jobSet.time)
    logInfo("Total delay: %.3f s for time %s (execution: %.3fs)".format(
      jobSet.totalDelay / 1000.0, jobSet.time.toString,
      jobSet.processingDelay / 1000.0
   
))
   listenerBus.post(StreamingListenerBatchCompleted(jobSet.toBatchInfo))
  }
  job.result match {
    case Failure(e)=>
      reportError("Error running job " + job, e)
    case _=>
  }
}

 

总结:

Spark Streaming在batchDuration处理完成后都会对产生的信息做清理,对输出DStream清理、依赖关系进行清理、清理默认也会清理rdd数据信息、元数据清理。

内存溢出和内存泄漏的区别、产生原因以及解决方案

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。 ...
  • guojunke579642
  • guojunke579642
  • 2017年12月24日 09:53
  • 957

Spark Streaming 流计算优化记录(5)-分区与内存的优化

关于Spark Streaming优化的各种叽里呱啦记录, 这一次是从跑不动, 一直优化到能每秒解决6万条输入消息以及3G数据的Inner Join. 大数据,流计算,spark,kafka,hado...
  • butterluo
  • butterluo
  • 2015年07月27日 14:38
  • 2036

Linux下几款C++程序中的内存泄露检查工具

Linux下编写C或者C++程序,有很多工具,但是主要编译器仍然是gcc和g++。最近用到STL中的List编程,为了检测写的代码是否会发现内存泄漏,了解了一下相关的知识。所有使用动态内存分配(dyn...
  • gatieme
  • gatieme
  • 2016年07月19日 21:56
  • 11465

分析和解决JAVA 内存泄露的实战例子

这几天,一直在为Java的“内存泄露”问题纠结。Java应用程序占用的内存在不断的、有规律的上涨,最终超过了监控阈值。福尔摩 斯不得不出手了! 分析内存泄露的一般步骤      ...
  • bigtree_3721
  • bigtree_3721
  • 2016年01月20日 18:21
  • 31010

JAVA 内存泄露详解(原因、例子及解决)

Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。 Java中的内存泄露与C++中的表现有所不同。 在C++中,所有被分配了内存的对象,不再...
  • anxpp
  • anxpp
  • 2016年05月05日 20:24
  • 37911

如何检查内存泄露并进行定位

1. 定义:应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费2. 对于C和C++这种没有Garbage Collection 的语言来讲,我们主要关注两种类型的内存泄...
  • patkritLee
  • patkritLee
  • 2016年06月27日 22:50
  • 966

Android 内存泄露总结

Java 中的内存分配简介 Java 中的内存分配 主要是分三块: 静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量。栈区:当方法执行时,会在栈区内...
  • osle123
  • osle123
  • 2016年10月08日 12:17
  • 860

C++内存泄露的定位与解决

PCIe应用程序调试时,发现程序出现内存泄露,经过自己的摸索,以及向软件同学请教,最终解决了此问题。 1. 现象描述 应用程序开发环境为VC++,运用其debug功能进行单步调试时,程序总...
  • asr9k
  • asr9k
  • 2016年11月21日 12:44
  • 431

C++内存泄露和内存管理

一直没有找到系统的讲解C++内存管理的文章,所以结合自己的工作经验,以及网友的一些总结,分析了内存泄露检测的方法,一般原则,最后还补充了内存溢出...
  • swjtuwyp
  • swjtuwyp
  • 2016年05月24日 22:26
  • 826

java中内存泄露有几种?如何分析泄露原因

一、Java内存回收机制 不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(He...
  • zhousenshan
  • zhousenshan
  • 2016年10月19日 22:34
  • 2121
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:第16课:Spark Streaming源码解读之数据清理内幕彻底解密
举报原因:
原因补充:

(最多只允许输入30个字)