5.1 DStreamGraph对象分析
在Spark Streaming中,DStreamGraph是一个非常重要的组件,主要用来:
1. 通过成员inputStreams持有Spark Streaming输入源及接收数据的方式
2. 通过成员outputStreams持有Streaming app的output操作,并记录DStream依赖关系
3. 生成每个batch对应的jobs
下面,通过分析一个简单的例子,结合源码分析来说明DStreamGraph是如何发挥作用的。案例如下:
val sparkConf = new SparkConf().setAppName("HdfsWordCount") val ssc = new StreamingContext(sparkConf, Seconds(2)) val lines = ssc.textFileStream(args(0)) val words = lines.flatMap(_.split(" ")) val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _) wordCounts.print() ssc.start() ssc.awaitTermination()
创建DStreamGraph实例
代码 val ssc = new StreamingContext(sparkConf, Seconds(2)) 创建了StreamingContext实例,StreamingContext包含了DStreamGraph类型的成员graph,graph在StreamingContext主构造函数中被创建,如下
private[streaming] val graph: DStreamGraph = { if (isCheckpointPresent) { cp_.graph.setContext(this) cp_.graph.restoreCheckpointData() cp_.graph } else { require(batchDur_ != null, "Batch duration for StreamingContext cannot be null") val newGraph = new DStreamGraph() newGraph.setBatchDuration(batchDur_) newGraph } }
可以看到,若当前checkpoint可用,会优先从checkpoint恢复graph,否则新建一个。还可以从这里知道的一点是:graph是运行在driver上的
DStreamGraph记录输入源及如何接收数据
DStreamGraph有和application输入数据相关的成员和方法,如下:
private val inputStreams = new ArrayBuffer[InputDStream[_]]() def addInputStream(inputStream: InputDStream[_]) { this.synchronized { inputStream.setGraph(this) inputStreams += inputStream } }
成员inputStreams为InputDStream类型的数组,InputDStream是所有input streams(数据输入流)的虚基类。该类提供了start()和stop()方法供streaming系统来开始和停止接收数据。那些只需要在driver端接收数据并转成RDD的input streams可以直接继承InputDStream,例如FileInputDStream是InputDStream的子类,它监控一个HDFS目录并将新文件转成RDDs。而那些需要在workers上运行receiver来接收数据的Input DStream,需要继承ReceiverInputDStream,比如KafkaReceiver
来看一下 val lines = ssc.textFileStream(args(0)) 调用,调用流程如下
从上面的调用流程图我们可以知道:
1. ssc.textFileStream会触发新建一个FileInputDStream。FileInputDStream继承于InputDStream,其start()方法定义了数据源及如何接收数据
2. 在FileInputDStream构造函数中,会调用ssc.graph.addInputStream(this),将自身添加到DStreamGraph的inputStreams: ArrayBuffer[InputDStream[_]] 中,这样DStreamGraph就知道了这个Streaming App的输入源及如何接收数据。可能你会奇怪为什么inputStreams是数组类型,举个例子,这里再来一个 val lines1 = ssc.textFileStream(args(0)),那么又将生成一个FileInputStream实例添加到inputStreams,所以这里需要集合类型
3. 生成FileInputDStream调用其map方法,将以FileInputDStream本身作为partent来构造新的MappedDStream。对于DStream的transform操作,都将生成一个新的DStream,和RDD transform生成新的RDD类似与MappedDStream不同,所有继承了InputDStream的定义了输入源及接收数据方式的sreams都没有parent,因为它们就是最初的streams
DStream的依赖链
每个DStream的子类都会继承def dependencies: List[DStream[_]] = List()方法,该方法用来返回自己的依赖的父DStream列表。比如,没有父DStream的 InputDStream的dependencies方法返回List()
MappedDStream的实现如下:
class MappedDStream[T: ClassTag, U: ClassTag] ( parent: DStream[T], mapFunc: T => U ) extends DStream[U](parent.ssc) { override def dependencies: List[DStream[_]] = List(parent) ... }
在上例中,构造函数参数列表中的parent即在ssc.textFileStream中new的定义了输入源及数据接收方式的最初的FileInputDStream实例,这里的dependencies方法将返回该FileInputDStream实例,这就构成了第一条依赖。可用如下图表示,这里特地将input streams用蓝色表示,以强调其与普通由transform产生的DStream的不同:
继续来看val words = lines.flatMap(_.split(" ")),flatMap如下:
def flatMap[U: ClassTag](flatMapFunc: T => Traversable[U]): DStream[U] = ssc.withScope { new FlatMappedDStream(this, context.sparkContext.clean(flatMapFunc)) }
每一个transform操作都将创建一个新的DStream,flatMap操作也不例外,它会创建一个FlatMappedDStream,FlatMappedDStream的实现如下:
class FlatMappedDStream[T: ClassTag, U: ClassTag]( parent: DStream[T], flatMapFunc: T => Traversable[U] ) extends DStream[U](parent.ssc) { override def dependencies: List[DStream[_]] = List(parent) ... }
与MappedDStream相同,FlatMappedDStream#dependencies也返回其依赖的父DStream,及lines,到这里,依赖链就变成了下图:
之后的几步操作不再这样具体分析,到生成wordCounts时,依赖图将变成下面这样:
在DStream中,与transform相对应的是output操作,包括print, saveAsTextFiles,saveAsObjectFiles,saveAsHadoopFiles,foreachRDD。output操作中,会创建 ForEachDStream实例并调用register方法将自身添加到DStreamGraph.outputStreams成员中,该ForEachDStream实例也会持有是调用的哪个output操作。本例的代码调用如下,只需看箭头所指几行代码
与DStreamtransform操作返回一个新的DStream不同,output操作不会返回任何东西,只会创建一个ForEachDStream作为依赖链的终结
至此,生成了完成的依赖链,也就是DAG,如下图(这里将ForEachDStream标为黄色以显示其与众不同):
5.2 ReceiverTracker与数据导入
Spark Streaming在数据接收与导入方面需要满足有以下三个特点:
1. 兼容众多输入源,包括HDFS, Flume, Kafka, Twitter and ZeroMQ。还可以自定义数据源
2. 要能为每个batch的RDD提供相应的输入数据
3. 为适应 7*24h 不间断运行,要有接收数据挂掉的容错机制
有容乃大,兼容众多数据源
InputDStream是所有input streams(数据输入流)的虚基类。该类提供了start()和stop()方法供streaming系统来开始和停止接收数据。那些只需要在driver端接收数据并转成RDD的input streams可以直接继承InputDStream,例如FileInputDStream是InputDStream的子类,它监控一个HDFS目录并将新文件转成RDDs。而那些需要在 workers上运行receiver来接收数据的Input DStream,需要继承ReceiverInputDStream,比如KafkaReceiver
只需在driver端接收数据的input stream一般比较简单且在生产环境中使用的比较少,本文不作分析,只分析继承了ReceiverInputDStream的input stream是如何导入数据的
ReceiverInputDStream有一个def getReceiver(): Receiver[T]方法,每个继承了ReceiverInputDStream的 input stream都必须实现这个方法。该方法用来获取将要分发到各个worker节点上用来接收数据的receiver(接收器)。不同的ReceiverInputDStream子类都有它们对应的不同的receiver,如KafkaInputDStream对应KafkaReceiver,FlumeInputDStream对应FlumeReceiver,TwitterInputDStream对应TwitterReceiver,如果你要实现自己的数据源,也需要定义相应的receiver
继承ReceiverInputDStream并定义相应的receiver,就是SparkStreaming能兼容众多数据源的原因
为每个batch的RDD提供输入数据
在StreamingContext中,有一个重要的组件叫做 ReceiverTracker,它是Spark Streaming作业调度器JobScheduler的成员,负责启动、管理各个receiver及管理各个receiver接收到的数据
确定receiver要分发到哪些executors上执行
创建ReceiverTracker实例
我们来看StreamingContext#start()方法部分调用实现,如下:
ReceiverTracker#start()
继续跟进ReceiverTracker#start(),如下图,它主要做了两件事:
1. 初始化一个 endpoint: ReceiverTrackerEndpoint,用来接收和处理来自ReceiverTracker 和 receivers 发送的消息
2. 调用launchReceivers来自将各个receivers分发到executors上
ReceiverTracker#launchReceivers()
继续跟进launchReceivers,它也主要干了两件事:
1. 获取 DStreamGraph.inputStreams 中继承了 ReceiverInputDStream 的input streams 的 receivers。也就是数据接收器
2. 给消息接收处理器endpoint发送StartAllReceivers(receivers)消息。直接返回,不等待消息被处理
处理StartAllReceivers消息
endpoint在接收到消息后,会先判断消息类型,对不同的消息做不同处理。对于StartAllReceivers消息,处理流程如下:
计算每个receiver要分发的目的executors。遵循两条原则:
1. 将 receiver 分布的尽量均匀
2. 如果receiver的preferredLocation本身不均匀,以preferredLocation为准
遍历每个receiver,根据第 1 步中得到的目的executors调用startReceiver方法
到这里,已经确定了每个receiver要分发到哪些executors上
启动receivers
接上,通过ReceiverTracker#startReceiver(receiver: Receiver[_],scheduledExecutors: Seq[String])来启动receivers,我们来看具体流程:
如上流程图所述,分发和启动receiver的方式不可谓不精彩。其中,startReceiverFunc函数主要实现如下:
val supervisor = new ReceiverSupervisorImpl(receiver, SparkEnv.get, serializableHadoopConf.value, checkpointDirOption) supervisor.start() supervisor.awaitTermination()
supervisor.start()中会调用receiver#onStart后立即返回。receiver#onStart一般自行新建线程或线程池来接收数据,比如在KafkaReceiver中,就新建了线程池,在线程池中接收topics的数据
supervisor.start()返回后,由supervisor.awaitTermination()阻塞住线程,以让这个task一直不退出,从而可以源源不断接收数据
数据流转
上图为receiver接收到的数据的流转过程,分析如下:
Step1: Receiver -> ReceiverSupervisor
这一步中,Receiver将接收到的数据源源不断地传给ReceiverSupervisor。Receiver调用其store(...)方法,store方法中继续调用supervisor.pushSingle或 supervisor.pushArrayBuffer等方法来传递数据。Receiver#store有多重形式,ReceiverSupervisor也有pushSingle、pushArrayBuffer、pushIterator、pushBytes方法与不同的 store对应
1. pushSingle: 对应单条小数据
2. pushArrayBuffer: 对应数组形式的数据
3. pushIterator: 对应iterator形式数据
4. pushBytes: 对应ByteBuffer形式的块数据
对于细小的数据,存储时需要BlockGenerator聚集多条数据成一块,然后再成块存储;反之就不用聚集,直接成块存储。当然,存储操作并不在Step1中执行,只为说明之后不同的操作逻辑
Step2.1: ReceiverSupervisor -> BlockManager -> disk/memory
在这一步中,主要将从receiver收到的数据以block(数据块)的形式存储存储block的是receivedBlockHandler: ReceivedBlockHandler,根据参数spark.streaming.receiver.writeAheadLog.enable配置的不同,默认为false,receivedBlockHandler对象对应的类也不同,如下:
private val receivedBlockHandler: ReceivedBlockHandler = { if (WriteAheadLogUtils.enableReceiverLog(env.conf)) { //< 先写 WAL,再存储到 executor 的内存或硬盘 new WriteAheadLogBasedBlockHandler(env.blockManager, receiver.streamId, receiver.storageLevel, env.conf, hadoopConf, checkpointDirOption.get) } else { //< 直接存到 executor 的内存或硬盘 new BlockManagerBasedBlockHandler(env.blockManager, receiver.storageLevel) } } // 启动 WAL 的好处就是在 application 挂掉之后,可以恢复数据。 // < 调用 receivedBlockHandler.storeBlock 方法存储 block,并得到一个 blockStoreResult val blockStoreResult = receivedBlockHandler.storeBlock(blockId, receivedBlock) //< 使用 blockStoreResult 初始化一个 ReceivedBlockInfo 实例 val blockInfo = ReceivedBlockInfo(streamId, numRecords, metadataOption, blockStoreResult) //< 发送消息通知 ReceiverTracker 新增并存储了 block trackerEndpoint.askWithRetry[Boolean](AddBlock(blockInfo))
不管是WriteAheadLogBasedBlockHandler还是BlockManagerBasedBlockHandler最终都是通过BlockManager将block数据存储execuor内存或磁盘或还有 WAL方式存入
这里需要说明的是streamId,每个InputDStream都有它自己唯一的id,即streamId,blockInfo包含streamId是为了区分block是哪个InputDStream的数据。之后为batch分配blocks时,需要知道每个InputDStream都有哪些未分配的blocks
Step2.2: ReceiverSupervisor -> ReceiverTracker
将block存储之后,获得block描述信息blockInfo: ReceivedBlockInfo,这里面包含:streamId、数据位置、数据条数、数据size等信息。之后,封装以block作为参数的AddBlock(blockInfo)消息并发送给ReceiverTracker以通知其有新增block数据块
Step3: ReceiverTracker -> ReceivedBlockTracker
ReceiverTracker收到ReceiverSupervisor发来的AddBlock(blockInfo)消息后,直接调用以下代码将block信息传给ReceivedBlockTracker:
receivedBlockTracker.addBlock中,如果启用了WAL,会将新增的block信息以WAL方式保存
无论WAL是否启用,都会将新增的block信息保存到streamIdToUnallocatedBlockQueues: mutable.HashMap[Int, ReceivedBlockQueue]中,该变量key为 InputDStream的唯一id,value为已存储未分配的block信息。之后为batch分配blocks,会访问该结构来获取每个InputDStream对应的未消费的blocks
5.3 动态生成JOB
JobScheduler有两个重要成员,一是ReceiverTracker,负责分发receivers及源源不断地接收数据;二是JobGenerator,负责定时的生成jobs并checkpoint
定时逻辑
在JobScheduler的主构造函数中,会创建JobGenerator对象。在JobGenerator的主构造函数中,会创建一个定时器:
private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds, longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")
该定时器每隔ssc.graph.batchDuration.milliseconds会执行一次eventLoop.post(GenerateJobs(new Time(longTime)))向eventLoop发送GenerateJobs(new Time(longTime))消息,eventLoop收到消息后会进行这个batch对应的jobs的生成及提交执行,eventLoop是一个消息接收处理器
需要注意的是,timer在创建之后并不会马上启动,将在StreamingContext#start()启动Streaming Application时间接调用到
timer.start(restartTime.milliseconds)才启动
为batch生成jobs
eventLoop在接收到GenerateJobs(new Time(longTime))消息后的主要处理流程有以上图中三步:
1. 将已接收到的blocks分配给batch
2. 生成该batch对应的jobs
3. 将jobs封装成JobSet并提交执行
将这三步展开进行分析
上图是根据源码画出的为batch分配blocks的流程图,这里对『获得batchTime各个InputDStream未分配的blocks』作进一步说明:
我们知道了各个ReceiverInputDStream对应的receivers接收并保存的blocks信息会保存在ReceivedBlockTracker#streamIdToUnallocatedBlockQueues,该成员key为streamId,value为该streamId对应的InputDStream已接收保存但尚未分配的 blocks 信息。
所以获取某 InputDStream 未分配的 blocks 只要以该 InputDStream 的streamId 来从 streamIdToUnallocatedBlockQueues 来 get 就好。获取之后, 会清楚该 streamId 对应的 value,以保证 block 不会被重复分配。
在实际调用中,为batchTime分配blocks时,会从streamIdToUnallocatedBlockQueues取出未分配的blocks塞进timeToAllocatedBlocks: mutable.HashMap[Time, AllocatedBlocks] 中,以在之后作为该batchTime对应的RDD的输入数据。
通过以上步骤,就可以为batch的所有InputDStream分配blocks。也就是为batch分配了blocks
生成该batch对应的jobs
eventLoop在接收到GenerateJobs(new Time(longTime))消息后的主要处理流程有以上图中三步:
1. 将已接收到的blocks分配给batch
2. 生成该batch对应的jobs
3. 将jobs封装成JobSet并提交执行
将这三步进行分析
将已接收到的blocks分配给batch
上图是根据源码画出的为batch分配blocks的流程图,这里对『获得batchTime各个InputDStream未分配的blocks』作进一步说明:
我们知道了各个ReceiverInputDStream对应的receivers接收并保存的blocks信息会保存在ReceivedBlockTracker#streamIdToUnallocatedBlockQueues,该成员key为streamId,value为该streamId对应的InputDStream已接收保存但尚未分配的blocks信息。所以获取某InputDStream未分配的blocks只要以该InputDStream的streamId来从streamIdToUnallocatedBlockQueues来get就好。获取之后,会清楚该streamId对应的value,以保证block不会被重复分配。在实际调用中,为batchTime分配 blocks时,会从streamIdToUnallocatedBlockQueues取出未分配的blocks塞进timeToAllocatedBlocks: mutable.HashMap[Time, AllocatedBlocks]中,以在之后作为该 batchTime对应的RDD的输入数据。通过以上步骤,就可以为batch的所有InputDStream分配blocks。也就是为batch分配了blocks
生成该 batch 对应的 jobs
为指定batchTime生成jobs的逻辑如上图所示。你可能会疑惑,为什么DStreamGraph#generateJobs(time: Time)为什么返回Seq[Job],而不是单个job。这是因为,在一个batch内,可能会有多个OutputStream执行了多次output操作,每次output操作都将产生一个Job,最终就会产生多个Jobs
我们结合上图对执行流程进一步分析:
在DStreamGraph#generateJobs(time: Time)中,对于DStreamGraph成员ArrayBuffer[DStream[_]]的每一项,调用DStream#generateJob(time: Time)来生成这个outputStream在该batchTime的job。该生成过程主要有三步:
Step1: 获取该outputStream在该batchTime对应的RDD
每个DStream实例都有一个generatedRDDs: HashMap[Time, RDD[T]]成员,用来保存该DStream在每个batchTime生成的RDD,当DStream#getOrCompute(time: Time)调用时
首先会查看generatedRDDs中是否已经有该time对应的RDD,若有则直接返回
若无,则调用compute(validTime: Time)来生成RDD,这一步根据每个InputDStream继承compute的实现不同而不同。例如,对于FileInputDStream,其compute实现逻辑如下:
1. 先通过一个findNewFiles()方法,找到多个新file
2. 对每个新file,都将其作为参数调用sc.newAPIHadoopFile(file),生成一个RDD实例
3. 将2中的多个新file对应的多个RDD实例进行union,返回一个union后的UnionRDD
Step2: 根据Step1中得到的RDD生成最终job要执行的函数jobFunc
jobFunc 定义如下: val jobFunc = () => { val emptyFunc = { (iterator: Iterator[T]) => {} } context.sparkContext.runJob(rdd, emptyFunc) }
可以看到,每个outputStream的output操作生成的Job其实与RDD action一样,最终调用SparkContext#runJob来提交RDD DAG定义的任务
Step3: 根据Step2中得到的jobFunc生成最终要执行的Job并返回
Step2 中得到了定义 Job 要干嘛的函数-jobFunc,这里便以 jobFunc 及batchTime 生成 Job 实例:
Some(new Job(time, jobFunc))
该Job实例将最终封装在JobHandler中被执行
至此,我们搞明白了JobScheduler是如何通过一步步调用来动态生成每个batchTime的jobs。下文我们将分析这些动态生成的jobs如何被分发及如何执行
5.4 job的提交与执行
我们分析了JobScheduler是如何动态为每个batch生成jobs,那么生成的jobs是如何被提交的
在JobScheduler生成某个batch对应的Seq[Job]之后,会将batch及Seq[Job]封装成一个JobSet对象,JobSet持有某个batch内所有的jobs,并记录各个job的运行状态
之后,调用JobScheduler#submitJobSet(jobSet: JobSet)来提交jobs,在该函数中,除了一些状态更新,主要任务就是执行
jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job)))
即,对于jobSet中的每一个job,执行jobExecutor.execute(new JobHandler(job)),要搞懂这行代码干了什么,就必须了解JobHandler及jobExecutor
JobHandler
JobHandler继承了Runnable,为了说明与job的关系,其精简后的实现如下:
private class JobHandler(job: Job) extends Runnable with Logging { import JobScheduler._ def run() { _eventLoop.post(JobStarted(job)) PairRDDFunctions.disableOutputSpecValidation.withValue(true) { job.run() } _eventLoop = eventLoop if (_eventLoop != null) { _eventLoop.post(JobCompleted(job)) } } }
JobHandler#run方法主要执行了job.run(),该方法最终将调用到『生成该batch对应的jobs的Step2定义的jobFunc』, jonFunc将提交对应RDD DAG定义的job
JobExecutor
知道了JobHandler是用来执行job的,那么JobHandler将在哪里执行job 呢?答案是
jobExecutor,jobExecutor为JobScheduler成员,是一个线程池,在JobScheduler主构造函数中创建,如下:
private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1) private val jobExecutor = ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job- executor")
JobHandler将最终在线程池jobExecutor的线程中被调用,jobExecutor的线程数可通过spark.streaming.concurrentJobs配置,默认为1。若配置多个线程,就能让多个job同时运行,若只有一个线程,那么同一时刻只能有一 个job运行
以上,即jobs被执行的逻辑
5.5 Block的生成与存储
ReceiverSupervisorImpl共提供了4个将从receiver传递过来的数据转换成block并存储的方法,分别是:
pushSingle: 处理单条数据
pushArrayBuffer: 处理数组形式数据
pushIterator: 处理iterator形式处理
pushBytes: 处理ByteBuffer形式数据
其中,pushArrayBuffer、pushIterator、pushBytes最终调用pushAndReportBlock;而pushSingle将调用defaultBlockGenerator.addData(data),我分别就这两种形式做说明
pushAndReportBlock
我针对存储block简化pushAndReportBlock后的代码如下:
def pushAndReportBlock( receivedBlock: ReceivedBlock, metadataOption: Option[Any], blockIdOption: Option[StreamBlockId] ){ ... val blockId = blockIdOption.getOrElse(nextBlockId) receivedBlockHandler.storeBlock(blockId, receivedBlock) ... }
首先获取一个新的blockId,之后调用receivedBlockHandler.storeBlock, receivedBlockHandler在ReceiverSupervisorImpl构造函数中初始化。当启用了checkpoint且 spark.streaming.receiver.writeAheadLog.enable为true时,receivedBlockHandler被初始化为WriteAheadLogBasedBlockHandler类型;否则将初始化为BlockManagerBasedBlockHandler 类型
WriteAheadLogBasedBlockHandler#storeBlock将ArrayBuffer, iterator, bytes类型的数据序列化后得到的serializedBlock
1. 交由BlockManager根据设置的StorageLevel存入executor的内存或磁盘中
2. 通过WAL再存储一份
而BlockManagerBasedBlockHandler#storeBlock将ArrayBuffer, iterator, bytes类型的数据交由BlockManager根据设置的StorageLevel存入executor的内存或磁盘中,并不再通过WAL存储一份pushSingle
pushSingle将调用BlockGenerator#addData(data: Any)通过积攒的方式来存储数据。接下来对BlockGenerator是如何积攒一条一条数据最后写入block的逻辑
上图为BlockGenerator的各个成员,首选对各个成员做介绍:
currentBuffer
变长数组,当receiver接收的一条一条的数据将会添加到该变长数组的尾部
可能会有一个receiver的多个线程同时进行添加数据,这里是同步操作
添加前,会由rateLimiter检查一下速率,是否加入的速度过快。如果过快的话就需要block住,等到下一秒再开始添加。最高频率由spark.streaming.receiver.maxRate控制,默认值为Long.MaxValue,具体含义是单个Receiver每秒钟允许添加的条数
blockIntervalTimer&blockIntervalMs
分别是定时器和时间间隔。blockIntervalTimer中有一个线程,每隔blockIntervalMs会执行以下操作:
将currentBuffer赋值给newBlockBuffer
将currentBuffer指向新的空的ArrayBuffer对象
将newBlockBuffer封装成newBlock
将newBlock添加到blocksForPushing队列中blockIntervalMs由spark.streaming.blockInterval控制,默认是200ms
blockPushingThread&blocksForPushing&blockQueueSize
blocksForPushing是一个定长数组,长度由blockQueueSize决定,默认为10,可通过spark.streaming.blockQueueSize改变。上面分析到,blockIntervalTimer中的线程会定时将block塞入该队列
还有另一条线程不断送该队列中取出block,然后调用ReceiverSupervisorImpl.pushArrayBuffer(...)来将 block 存储,这条线程就是blockPushingThread
PS: blocksForPushing为ArrayBlockingQueue类型。ArrayBlockingQueue是一个阻塞队列,能够自定义队列大小,当插入时,如果队列已经没有空闲位置,那么新的插入线程将阻塞到该队列,一旦该队列有空闲位置,那么阻塞的线程将执行插入
以上,通过分析各个成员,也说明了BlockGenerator是如何存储单条数据的