Spark Streaming 中的DStream

Discretized Stream 也叫 DStream) 是 Spark Streaming 对于持续数据流的一种基本抽象,在内部实现上,DStream 会被表示成一系列连续的 RDD(弹性分布式数据集),每一个 RDD 都代表一定时间间隔内到达的数据。(并不是说DStream的元素是RDD,而是表现和方法都和RDD类似,DStream可以说约等于RDD,因此在操作中,很多人都把DStream当做RDD,甚至喜欢直接命名为RDD。如果打开DStream.scala和RDD.scala,可以发现几乎RDD上的所有operation在DStream中都有相应的定义。) (有些操作也可以把DStream的元素封装成RDD处理,如foreachRDD把批次内的数据当做一个RDD。但是从DStream的类型可以看到第一种情况更普遍,DStream是RDD的模板,如:DStream[(String,Double)],而非DStream[RDD(String,Double)])所以在对 DStream 进行操作时,会被 Spark Stream 引擎转化成对底层 RDD 的操作。对 Dstream 的操作类型有:

  • Transformations: 类似于对 RDD 的操作,Spark Streaming 提供了一系列的转换操作去支持对 DStream 的修改。如 map,union,filter,transform 等
  • Window Operations: 窗口操作支持通过设置窗口长度和滑动间隔的方式操作数据。常用的操作有 reduceByWindow,reduceByKeyAndWindow,window 等
  • Output Operations: 输出操作允许将 DStream 数据推送到其他外部系统或存储平台, 如 HDFS, Database 等,类似于 RDD 的 Action 操作,Output 操作也会实际上触发对 DStream 的转换操作。常用的操作有 print,saveAsTextFiles,saveAsHadoopFiles, foreachRDD 等。

关于 DStream Operations 的更多信息,请参考 Spark 官网的 Spark Streaming Programing Guide。

DStream是抽象类,它把连续的数据流拆成很多的小RDD数据块, 这叫做“微批次”, spark的流式处理, 都是“微批次处理”。 DStream内部实现上有批次处理时间间隔,滑动窗口等机制来保证每个微批次的时间间隔里, 数据流以RDD的形式发送给spark做进一步处理。因此, 在一个为批次的处理时间间隔里(BatchDuration), DStream只产生一个RDD。

可以这么理解, DStream.foreachRDD 是一个输出操作符,它操作的不是RDD里的一行数据, 而是操作DStream后面的RDD,在一个时间间隔里, 只返回一个RDD的“微批次”, 为了访问这个“微批次”RDD里的数据, 我们还需要在RDD数据对象上做进一步操作.

重点:Spark Streaming的foreachRDD运行在Driver端,而rdd的foreach和foreachPartion运行在Worker节点。

 备注:对数据的向外输出,还是用foreach**算子好,不要用Map**算子,因为Map还要返回一个RDD。

 

RecivieerInputDStream继承自InputDStream继承自DStream,前两个没有定义几个自己的方法, RecivieerInputDStream.map返回的是DStream类型。

同PairRDDFunctions一样PairDStreamFunctions也提供了很多处理pair类型DSTream的方法,如常用的updateStateByKey,reduceByKeyAndWindow,join等,详见:

https://github.com/apache/spark/blob/master/streaming/src/main/scala/org/apache/spark/streaming/dstream/PairDStreamFunctions.scala

 

目前项目中的spark版本是1.5的,据说1.6版本中的mapWithState 性能较updateStateByKey提升10倍。有机会了解了解

http://technicaltidbit.blogspot.com/2015/11/spark-streaming-16-stop-using.html

 

网上很多用updateStateByKey的例程用:

这里实际返回的是Iterator[Some((K,V))],不知他们是否会跑出正确的结果,因为这个函数源码中要求为Iterator[(K,V)]:

猜测网上产生这种错误的原因可能是受重载函数的影响:

 

 

 

DStreams上的输出操作

本节摘自:https://endymecy.gitbooks.io/spark-programming-guide-zh-cn/content/spark-streaming/basic-concepts/output-operations-on-DStreams.html

输出操作允许DStream的操作推到如数据库、文件系统等外部系统中。因为输出操作实际上是允许外部系统消费转换后的数据,它们触发的实际操作是DStream转换。目前,定义了下面几种输出操作:

Output Operation Meaning
print() 在DStream的每个批数据中打印前10条元素,这个操作在开发和调试中都非常有用。在Python API中调用pprint()
saveAsObjectFiles(prefix, [suffix]) 保存DStream的内容为一个序列化的文件SequenceFile。每一个批间隔的文件的文件名基于prefixsuffix生成。"prefix-TIME_IN_MS[.suffix]",在Python API中不可用。
saveAsTextFiles(prefix, [suffix]) 保存DStream的内容为一个文本文件。每一个批间隔的文件的文件名基于prefixsuffix生成。"prefix-TIME_IN_MS[.suffix]"
saveAsHadoopFiles(prefix, [suffix]) 保存DStream的内容为一个hadoop文件。每一个批间隔的文件的文件名基于prefixsuffix生成。"prefix-TIME_IN_MS[.suffix]",在Python API中不可用。
foreachRDD(func) 在从流中生成的每个RDD上应用函数func的最通用的输出操作。这个函数应该推送每个RDD的数据到外部系统,例如保存RDD到文件或者通过网络写到数据库中。需要注意的是,func函数在驱动程序中执行,并且通常都有RDD action在里面推动RDD流的计算。

利用foreachRDD的设计模式

dstream.foreachRDD是一个强大的原语,发送数据到外部系统中。然而,明白怎样正确地、有效地用这个原语是非常重要的。下面几点介绍了如何避免一般错误。

  • 经常写数据到外部系统需要建一个连接对象(例如到远程服务器的TCP连接),用它发送数据到远程系统。为了达到这个目的,开发人员可能不经意的在Spark驱动中创建一个连接对象,但是在Spark worker中 尝试调用这个连接对象保存记录到RDD中,如下:
  dstream.foreachRDD(rdd => {
      val connection = createNewConnection()  // executed at the driver
      rdd.foreach(record => {
          connection.send(record) // executed at the worker
      })
  })

这是不正确的,因为这需要先序列化连接对象,然后将它从driver发送到worker中。这样的连接对象在机器之间不能传送。它可能表现为序列化错误(连接对象不可序列化)或者初始化错误(连接对象应该 在worker中初始化)等等。正确的解决办法是在worker中创建连接对象。

  • 然而,这会造成另外一个常见的错误-为每一个记录创建了一个连接对象。例如:
  dstream.foreachRDD(rdd => {
      rdd.foreach(record => {
          val connection = createNewConnection()
          connection.send(record)
          connection.close()
      })
  })

通常,创建一个连接对象有资源和时间的开支。因此,为每个记录创建和销毁连接对象会导致非常高的开支,明显的减少系统的整体吞吐量。一个更好的解决办法是利用rdd.foreachPartition方法。 为RDD的partition创建一个连接对象,用这个两件对象发送partition中的所有记录。

 dstream.foreachRDD(rdd => {
      rdd.foreachPartition(partitionOfRecords => {
          val connection = createNewConnection()
          partitionOfRecords.foreach(record => connection.send(record))
          connection.close()
      })
  })

这就将连接对象的创建开销分摊到了partition的所有记录上了。

  • 最后,可以通过在多个RDD或者批数据间重用连接对象做更进一步的优化。开发者可以保有一个静态的连接对象池,重复使用池中的对象将多批次的RDD推送到外部系统,以进一步节省开支。
  dstream.foreachRDD(rdd => {
      rdd.foreachPartition(partitionOfRecords => {
          // ConnectionPool is a static, lazily initialized pool of connections
          val connection = ConnectionPool.getConnection()
          partitionOfRecords.foreach(record => connection.send(record))
          ConnectionPool.returnConnection(connection)  // return to the pool for future reuse
      })
  })

需要注意的是,池中的连接对象应该根据需要延迟创建,并且在空闲一段时间后自动超时。这样就获取了最有效的方式发生数据到外部系统。

其它需要注意的地方:

  • 输出操作通过懒执行的方式操作DStreams,正如RDD action通过懒执行的方式操作RDD。具体地看,RDD actions和DStreams输出操作接收数据的处理。因此,如果你的应用程序没有任何输出操作或者 用于输出操作dstream.foreachRDD(),但是没有任何RDD action操作在dstream.foreachRDD()里面,那么什么也不会执行。系统仅仅会接收输入,然后丢弃它们。
  • 默认情况下,DStreams输出操作是分时执行的,它们按照应用程序的定义顺序按序执行。

 

 

参考资料

https://www.ibm.com/developerworks/cn/opensource/os-cn-spark-practice2/index.html

https://github.com/apache/spark/blob/master/streaming/src/main/scala/org/apache/spark/streaming/dstream/DStream.scala

你可能感兴趣的:(Spark,Dstream)