文章目录

  • 依赖
  • 初始化StreamingContext
  • Discretized Streams (DStreams)
  • Input DStreams and Receivers
  • Transformations on DStreams
  • UpdateStateByKey Operation
  • mapWithState算子
  • 以socket模式举例Streaming底层执行逻辑
  • Transform Operation(重点)
  • Window Operations(了解)
  • Join Operations
  • Output Operations on DStreams
  • Design Patterns for using foreachRDD(重点)
  • 案例分析
  • DataFrame and SQL Operations
  • MLlib Operations


参考官网:http://spark.apache.org/docs/latest/streaming-programming-guide.html#basic-concepts

上一节,初识了Spark Streaming,并做了一个示例。这一节来学一下基本概念。
小知识点:
Spark Streaming并不是真正的实时处理,Storm、Flink是真正的实时处理。
Spark:以批处理为主,用微批处理来处理流数据
Flink:以流处理为主,用流处理来处理批数据。
Spark Structured Streaming结构化流可能是Spark未来的发展方向,只是现在刚出来,上生产需要一点时间来适应。结构化流的编程方式和DF DS完全一样,可以使用类似SQL的方式去处理,它的底层做过优化,所以性能更好。现在生产上还是Spark Streaming。

依赖

需要添加Maven依赖,这样能连接到Maven中心仓库,添加后才能去写Spark Streaming程序。

//版本什么的可以看自己情况
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.12</artifactId>
    <version>2.4.3</version>
    <scope>provided</scope>
</dependency>

如果用的数据源不在Spark Streaming core API里面,比如来自于Kafka, Flume, and Kinesis,那么需要另外添加spark-streaming-xyz_2.12这样的依赖,比如:

be sparkling什么意思 sparking是什么意思啊_Spark

初始化StreamingContext

StreamingContext 是主的入口点,要初始化一个 Spark Streaming应用程序,StreamingContext 需要被创建起来。一个 StreamingContext 对象可以通过一个SparkConf 对象创建。

import org.apache.spark._
import org.apache.spark.streaming._

val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))

其中appName 这个参数,是应用程序的名字,用来展示在WebUI上面的。master 是 Spark, Mesos 、 YARN cluster URL,或者local[*]本地模式。实践证明,不要在程序代码里去硬编码master 、appName ,而是要在spark-submit 提交应用程序的时候去设置。对于本地开发测试,可以使用local模式。
批处理间隔必须根据应用程序的延迟要求和可用的集群资源来设置。

还可以从现有的SparkContext对象创建StreamingContext对象。

import org.apache.spark.streaming._

val sc = ...                // existing SparkContext
val ssc = new StreamingContext(sc, Seconds(1))

在创建完context之后,你必须做一下这些:

  • 1.通过创建 input DStreams来定义输入的数据源;
  • 2.对DStreams做transformation和output(action)操作,来定义streaming流的计算;
  • 3.使用streamingContext.start() 来开始接收数据并处理它;
  • 4.使用streamingContext.awaitTermination()来等待处理结束(手动停止或者因报错而停止);
  • 5.可以使用streamingContext.stop()来进行手动停止。(一般不会手工关闭)

总结:input data stream ==> transformation ==> output

不过需要注意一下几点:

  • 一旦context开始之后,新的streaming computations就不能再去设置或者添加进去。比如说ssc.start()后面就不要再添加一些计算的代码了。
  • 一旦context停止之后,context就不能再启动了。
比如
ssc.start()
ssc.stop()
ssc.start()
  • 一个JVM里面同时只能有一个StreamingContext 。不能存在多个StreamingContext 。
    停掉StreamingContext,也会将SparkContext停掉。想要只停止StreamingContext,设置stop()的叫做stopSparkContext的参数为false
  • 为了创建多个StreamingContexts,SparkContext 可以被重新使用,前提是在下一个StreamingContext 创建之前,只要之前的StreamingContext已经停掉(不需要停掉SparkContext)。
Discretized Streams (DStreams)

Discretized Stream或者叫DStream是spark streaming提供的最基本的抽象。它表示一个连续的数据流,要么是从source接收来的输入数据流,要么是通过转换input stream输入流而生成的处理后的数据流。在内部,数据流由一系列连续的RDD表示,这是Spark对不可变的分布式的数据集的抽象。DStream中的每个RDD包含来自指定时间间隔的数据,如下图,每个时间间隔内都是一个RDD:

be sparkling什么意思 sparking是什么意思啊_Spark_02


对DStream应用的任何操作都将 转换为 对底层的RDD的操作。比如,在之前的示例中,把数据流中的每一行去转换为单词,flatMap操作符被应用在lines DStream中的每个RDD上去生成words DStream的RDDs。如下图所示:

对DStream做了某个操作,其实就是对底层的RDD都做了某个操作。

一个DStream = 一系列的 RDD

be sparkling什么意思 sparking是什么意思啊_be sparkling什么意思_03


这些潜在的RDD转换是用Spark的引擎来计算完成的。为了方便,DStream操作隐藏大多数的细节并且向开发者提供更高层次的API。这些操作将在之后的章节中进行讨论。

Input DStreams and Receivers

Input DStreams是DStreams ,它代表了从streaming sources端接收而来的输入流数据。在前面的wordcount统计案例中,lines就是一个 input DStream,它代表了从netcat server.端接收的数据流。每个input DStream(除了文件流,在后面讨论)都会与一个接收器(Scala doc, Java doc)对象相关联,该对象从一个源(streaming source)接收数据并将其存储在Spark的内存中进行处理。

ReceiverAbstract class of a receiver that can be run on worker nodes to receive external data. A custom receiver can be defined by defining the functions onStart() and onStop(). onStart() should define the setup steps necessary to start receiving data, and onStop() should define the cleanup steps necessary to stop receiving data.

Spark Streaming里面有两种可能:有Receiver和没有Receiver。如果看到某个方法返回值带有Receiver,那么就有Receiver。
比如:

//返回值是ReceiverInputDStream[String],带有Receiver,那么这个Spark Streaming就有Receiver去接收数据
  def socketTextStream(
      hostname: String,
      port: Int,
      storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
    ): ReceiverInputDStream[String] = withNamedScope("socket text stream") {
    socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
  }

Spark Streaming提供两类内置streaming sources。

  • Basic sources:这个Sources在StreamingContext API直接可以使用,举例:file - systems, and socket connections
  • Advanced sources:像 Kafka、 Flume、 Kinesis、twitter等这样的Sources,要通过额外的工具类才能使用。这需要添加额外的依赖,可以查看上面的依赖一节。

注意,如果你想要在你的streaming应用程序里以并行的方式去接收多种数据源,你可以创建多个input DStreams(这个会在性能优化这一节进一步讨论)。这将会创建多个receivers ,这些receivers将同时接收多个数据流。但是请注意,一个Spark worker/executor是一项长期运行的任务,因此它将会占用分配给Spark Streaming应用程序的cores的一部分,就是说你给Spark Streaming程序分配了多少个core,worker/executor因为它会一直运行,所以它会占用一部分core。 因此为了处理接收过来的数据和运行receiver(s),就需要分配足够的core(如果是local本地,要有足够的线程)给Spark Streaming应用程序。

需要注意谨记的点:

  • 当在本地local运行Spark Streaming程序的时候,不要使用 “local” 或者 “local[1]” 作为master URL。如果用这两个意味着 本地local运行任务的时候只会有一个线程可以使用。如果以 sockets、 Kafka、 Flume等作为receiver,你去使用一个input DStream,那么这个单个的线程将会被用于运行receiver,就没有线程去处理接收的数据了。因此,当在本地local运行程序的时候,请记得要使用 “local[n]” 作为 master URL,n大于receivers的数量。
  • 把这个逻辑扩展到一个cluster集群,分配给Spark Streaming应用程序core的数量必须多于receivers的数量。要不然系统将会只接收数据,而不去处理它。

可以用前面的wordcount案例测试一下,设置成local[1],netcat server端输入数据,然后去UI看一下,数据并未处理,receiver一直在运行着。

be sparkling什么意思 sparking是什么意思啊_foreachRDD_04

上面是有receiver的情况,如果没有receiver,比如数据源为File Streams,比如本地文件系统或者HDFS文件系统等,它就没有receiver。File streams不需要运行receiver,所以并不需要分配任何的core去接收文件数据。
为什么HDFS上的数据就不需要receiver?因为数据在HDFS 上,直接用Hadoop的API读取数据即可,而且不需要持久运行,如果作业挂了,直接重新读取一下即可。

举例:数据源为hdfs文件系统:

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
//读取hdfs系统
object HDFSWCApp {
  def main(args: Array[String]): Unit = {
  //这里可以设置成local[1]
    val sparkConf = new SparkConf().setMaster("local[1]").setAppName("HDFSWCApp")
    val ssc = new StreamingContext(sparkConf,Seconds(20))

    //val lines = ssc.textFileStream("hdfs://hadoop001:9000/data/streaming")
    val lines = ssc.textFileStream("e:/streaming")
    val result = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
}

然后去hdfs上,把wordcount.txt move到hdfs的/data/streaming目录下即可。(这里注意是move,不是put,put可能会出问题,在put的过程中,可能已经读取了)
如何监控目录等详情,请参考官网。
源码:

/**
   * Create an input stream that monitors a Hadoop-compatible filesystem
   * for new files and reads them as text files (using key as LongWritable, value
   * as Text and input format as TextInputFormat). Files must be written to the
   * monitored directory by "moving" them from another location within the same
   * file system. File names starting with . are ignored.
   * @param directory HDFS directory to monitor for new file
   */
   //返回值为DStream[String],不带receiver,所以不用receiver
  def textFileStream(directory: String): DStream[String] = withNamedScope("text file stream") {
    fileStream[LongWritable, Text, TextInputFormat](directory).map(_._2.toString)
  }
Transformations on DStreams

和RDD很类似,transformations可以让来自input DStream的数据被修改。DStreams 支持很多的RDD中存在的transformations ,比如:
map、flatMap、filter、repartition、union、count、reduce、countByValue、reduceByKey、join、cogroup、transform、updateStateByKey
其中transform、updateStateByKey这两个需要重点讲一下,其它的都是和RDD中一样,具体可参考官网。

UpdateStateByKey Operation

上面的wordcount案例中,统计的都是本批次的结果,这是无状态的。那么如果现在需求改一下,要统计今天到现在的结果呢?这个是有状态的。

updateStateByKey(func):Return a new “state” DStream where the state for each key is updated by applying the given function on the previous state of the key and the new values for the key. This can be used to maintain arbitrary state data for each key.
它返回的是带有状态的DStream,在这个DStream里面,每个key的状态是可以被更新的,通过一个给定的函数,把之前的key的状态和当前key新的状态联合起来操作一波。这可以被用于维持每个key的任意状态。

当有新信息时持续的更新状态,updateStateByKey操作允许你维护任意的状态。就是说用新的状态把老的状态给更新掉。做这些,你需要做以下两步:

  • 1.Define the state - The state can be an arbitrary data type.
  • 2.Define the state update function - Specify with a function how to update the state using the previous state and the new values from an input stream.

在每个批次中,Spark将会把state update function应用于所有存在的key,不管他们在一个批次中是否有新的数据。如果update函数返回none,那么key-value键值对将被消除。

让我们用例子说明。假设你想在文件数据流中保持看到的每个单词的运行数量。这里,运行的数量是状态并且它是一个整型。我们定义更新函数如:

def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
    val newCount = ...  // add the new values with the previous running count to get the new count
    Some(newCount)
}

这是在包含单词的DStream中应用(假设,在早期示例中对DStream包含(word,1)对)。

val runningCounts = pairs.updateStateByKey[Int](updateFunction _)

更新函数将会被每个单词调用,newValues具有1的序列(来自(word,1)对),并且runningCount具有前一个计数。

注意使用updateStateByKey要求配置好checkpoint目录,详情参阅checkpointing节点。

案例
注意两点:

  • ①定义了一个函数updateFunction,这个函数会根据新的值和老的值,去返回两个值之和作为新的值。然后调用updateStateByKey,并把函数updateFunction传进去,作用于每个key。
  • ②另外还需要设置一下checkpoint,为什么要用到checkpoint?因为之前的案例是没有状态的,用完之后就丢掉,不需要了,但是现在要用到之前的那些数据,要把之前的状态保留下来,把以前的处理结果要写的一个地方上去,检查点数据先放到某个地方存起来。
package com.ruozedata.spark.com.ruozedata.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SocketWCStateApp {
  def main(args: Array[String]): Unit = {
    //这个master要是2个core
    val conf = new SparkConf().setMaster("local[2]").setAppName("SocketWCStateApp")
    val ssc = new StreamingContext(conf, Seconds(20))
    //批次的时间是10秒,10秒处理一次

    //如果用到updateStateByKey,此处要加上ssc.checkpoint("目录")这一句,否则会报错:
    // The checkpoint directory has not been set. Please set it by StreamingContext.checkpoint().
    //为什么要用到checkpoint?因为之前的案例是没有状态的,用完之后就丢掉,不需要了,
    // 但是现在要用到之前的那些数据,要把之前的状态保留下来
    //“.”的意思是当前目录
    ssc.checkpoint(".")

    //这个是把server数据源转换成了DStream
    val lines = ssc.socketTextStream("hadoop001",9999)

    //下面就是之前的wordcount代码
    val words = lines.flatMap(_.split(" "))
    val pairs = words.map(word => (word, 1))
    val wordCounts = pairs.reduceByKey(_ + _)

    //调用updateStateByKey,并把updateFunction这个函数传进去
    val state = wordCounts.updateStateByKey(updateFunction)

    //把这个DStream产生的每个RDD的前10个元素打印到控制台上
    state.print()


    //开始计算
    ssc.start()
    //等待计算结束
    ssc.awaitTermination()
  }

  //定义一个函数updateFunction
  //(hello,1)  (hello,1) ==>(1,1) 这个是Seq[Int]
  //为什么要用Option?因为有可能某个单词是第一次出现,以前没有出现过
  def updateFunction(newValues: Seq[Int], preValues: Option[Int]): Option[Int] = {
    // add the new values with the previous running count to get the new count
    val newCount = newValues.sum
    val oldCount = preValues.getOrElse(0)
    //返回新的和老的相加
    Some(newCount + oldCount)
  }
}
//netcat端输入:
[hadoop@hadoop001 sbin]$ nc -lk 9999
word words china china word  love
word words china china word  love
word words china china word  love
word words china china word  love
word words china china word  love
jerry
jerry

输出结果:

//可以看到这些都是累加的,所有批次都在一块
-------------------------------------------
Time: 1564380380000 ms
-------------------------------------------
(love,5)
(,5)
(word,10)
(china,10)
(words,5)
Time: 1564380420000 ms
-------------------------------------------
(love,5)
(,5)
(word,10)
(jerry,1)
(china,10)
(words,5)
Time: 1564380440000 ms
-------------------------------------------
(love,5)
(,5)
(word,10)
(jerry,2)
(china,10)
(words,5)

因为有了ssc.checkpoint("."),会把之前的结果写到当前目录,所以可以到当前目录看一下,有很多的checkpoint小文件:

be sparkling什么意思 sparking是什么意思啊_Spark_05


那么问题来了,如果批次有很多,那么会出现很多很多的checkpoint小文件,该怎么办?

删掉?删掉的话,之前的数据会丢失,后面累加起来的值就不对了。合并的话也不好合并。

上面的代码能满足的需求是:统计这个启动之后到现在的wordcount,那么昨天的呢?今天的呢?明天的呢?

此时可以考虑一下,用upsert这种方式。意思就是说,还是用Spark Streaming,但是不用上面的updateStateByKey更新状态state的方式了。而是按照一个批次一个批次的处理,处理的结果用upsert方式,如果某个表里原来是(hello,3),现在又来了一个(hello,1),那么就更新这个记录,变成了(hello,4)。如果是新来的(world,2),就insert一条新的记录。

或者说这样:后面加个时间戳ts:

hello 1 ts1

hello 3 ts2

world 2 ts2

world 5 ts3

hello 1 ts3

这样的话,如果你想统计某个时间范围内的数据,直接用一条SQL语句就查出来了。

mapWithState算子

上面的wordcount案例中,当<key,value>有state状态更新时,使用的是updateStateByKey,这个是Spark老版本的使用。在Spark新版本中,推荐不要使用updateStateByKey,而是推荐使用mapWithState。
但是这个算子还是Experimental实验性的,是最新出来的算子,暂且不建议投入生产。具体案例用法见下面源码,以及后面的案例。

看mapWithState源码:

/**
   * :: Experimental ::
   * Return a [[MapWithStateDStream]] by applying a function to every key-value element of
   * `this` stream, while maintaining some state data for each unique key. The mapping function
   * and other specification (e.g. partitioners, timeouts, initial state data, etc.) of this
   * transformation can be specified using `StateSpec` class. The state data is accessible in
   * as a parameter of type `State` in the mapping function.
   *
   * Example of using `mapWithState`:
   * {{{
   *    // A mapping function that maintains an integer state and return a String
   *    def mappingFunction(key: String, value: Option[Int], state: State[Int]): Option[String] = {
   *      // Use state.exists(), state.get(), state.update() and state.remove()
   *      // to manage state, and return the necessary string
   *    }
   *
   *    val spec = StateSpec.function(mappingFunction).numPartitions(10)
   *
   *    val mapWithStateDStream = keyValueDStream.mapWithState[StateType, MappedType](spec)
   * }}}
   *
   * @param spec          Specification of this transformation
   * @tparam StateType    Class type of the state data
   * @tparam MappedType   Class type of the mapped data
   */
  @Experimental
  def mapWithState[StateType: ClassTag, MappedType: ClassTag](
      spec: StateSpec[K, V, StateType, MappedType]
    ): MapWithStateDStream[K, V, StateType, MappedType] = {
    new MapWithStateDStreamImpl[K, V, StateType, MappedType](
      self,
      spec.asInstanceOf[StateSpecImpl[K, V, StateType, MappedType]]
    )
  }

这个函数它返回的是一个[MapWithStateDStream],也是一个DStream,它把一个函数作用于这个stream中的每个<key,value>的元素,去维护每个唯一的key的状态。

如何使用?
参考上面源码里的example,稍微变通一下,就变成了下面代码。

package com.ruozedata.spark.com.ruozedata.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}

object SocketWCStateApp2 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SocketWCStateApp2")
    val ssc = new StreamingContext(conf, Seconds(10))
    //就算用mapWithState也是要用checkpoint的,因为还是要保留之前的状态到一个地方去
    ssc.checkpoint(".")

    //这个是把server数据源转换成了DStream
    val lines = ssc.socketTextStream("hadoop001",9999)

    val wordCounts = lines.flatMap(_.split(" ")).map(word => (word, 1)).reduceByKey(_ + _)

    //参考源码,这个是把一个匿名函数赋值给一个变量
    //传进来word单词和value值,比如(hello,3),还有一个state状态
    //Option[Int]表示有可能是某个单词是第一次进来,值为0
    val mappingFunc = (word: String, value: Option[Int], state: State[Int]) =>{
      //获取value的值,如果没有就取0;获取之前的状态的值,如果没有就取0;
      // 然后新的值和老的值相加;相加后赋值给变量sum
      val sum = value.getOrElse(0) + state.getOption().getOrElse(0)

      //用sum更新state的值,把state的值更新到最新
      state.update(sum)

      //返回一个(key,value),就是最新的
      (word,sum)
    }
    val state = wordCounts.mapWithState(StateSpec.function(mappingFunc))

    state.print()


    ssc.start()
    ssc.awaitTermination()
  }
}

上面就是 mapWithState 大致的用法。
mapWithState 大致可以实现和updateStateByKey一样的功能,输出的时候不太一样,当没有数据流传进来时,mapWithState 之前的结果不会打印出来,updateStateByKey会把之前的结果打印出来。

考虑一个问题,上面的输出只是输出到控制台上面了,但是生产上肯定不是这样的,生产上是要输出到RDBMS/NoSQL里面去的。
如何输出?请接着看下面的DStream的输出操作的案例分析。

以socket模式举例Streaming底层执行逻辑

当创建一个StreamingContext (ssc ),同时底层会创建一个SparkContext (SC)

//这个是driver端,程序的入口
val conf = new SparkConf().setMaster("local[2]").setAppName("SocketWCStateApp")
val ssc = new StreamingContext(conf, Seconds(20))
//StreamingContext底层:会创建一个SparkContext   (SC)
  def this(conf: SparkConf, batchDuration: Duration) = {
    this(StreamingContext.createNewSparkContext(conf), null, batchDuration)
  }

  private[streaming] def createNewSparkContext(conf: SparkConf): SparkContext = {
    new SparkContext(conf)
  }

如果是socket/Kafka这种数据源模式,那么会有receiver,receiver的话运行在executor里面的,receiver是用来接收数据的,上面代码是driver端。
来看一下socketTextStream源码,存储级别为MEMORY_AND_DISK_SER_2,内存磁盘序列化 副本数为2。

def socketTextStream(
      hostname: String,
      port: Int,
      storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
    ): ReceiverInputDStream[String] = withNamedScope("socket text stream") {
    socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
  }

be sparkling什么意思 sparking是什么意思啊_foreachRDD_06


①你开发一个Spark Streaming 应用程序代码,它属于driver端,在一开始会创建StreamingContext(给它名字ssc),StreamingContext的底层会创建SparkContext(给它名字sc)。

②因为是socket/Kafka这种数据源模式,那么会有receiver,receiver的话运行在executor里面的,receiver是用来接收数据的。而且存储级别为MEMORY_AND_DISK_SER_2,内存磁盘序列化 副本数为2。

③receiver从外面socket接收数据过来,根据时间间隔,把数据流拆分成多个批次,先放在内存中,内存不够再放到磁盘上,因为是2副本,所以要复制一份到其它节点上。

④ssc.start()开始的时候,就开始走业务逻辑代码了。

⑤当一个批次的时间到了,receiver会给ssc,告诉ssc,要开始处理数据了,receiver接收的数据会放在block块上,所以需要告诉ssc这些block块的信息。

⑥ssc底层会用sc SparkContext去提交,这个时候会生成job任务,然后会把各个job分配给各个executor去执行。

Transform Operation(重点)

前面的都是基于DataStreaming编程的。那现在有个需求,现在有一个DStream,又有一个textFile,这个textFile转成RDD之后,这个DStream和RDD之间如何进行关联?它们之间怎么进行相互的操作?
这就需要借助于transform这个算子了。

transform(func):Return a new DStream by applying a RDD-to-RDD function to every RDD of the source DStream. This can be used to do arbitrary RDD operations on the DStream.

transform(func):通过将一个RDD-to-RDD函数作用于来源source DStream的每个RDD来返回一个新的DStream。这可以用来在DStream上执行任意RDD操作

transform转换操作(连带着它的一些变种,比如TransformWith等)允许任意RDD-to-RDD函数应用于一个DStream。它可以用于 应用任何未在DStream API中公开的RDD操作。例如,在数据流中的每个批次与其他数据集dataset连接join起来的功能不会直接暴露在DStream API中。然而,你可以很容易地使用transform完成此操作。这可能性非常强大。例如,可以通过 将输入数据流与预先计算的垃圾邮件信息(也可以使用Spark生成)进行join关联,这样来进行实时数据清理,然后基于此进行过滤。

val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information

val cleanedDStream = wordCounts.transform { rdd =>
  rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
  ...
}

Note that the supplied function gets called in every batch interval. This allows you to do time-varying RDD operations, that is, RDD operations, number of partitions, broadcast variables, etc. can be changed between batches.
注意,提供的函数在每个批处理间隔中被调用。此函数允许你执行随时间变化的RDD操作,就是说 RDD操作、分区数量、广播变量等等可以在批次之间被更改。

案例分析
生产上,从正常数据里面过滤掉黑名单。
假如现在处理一批日志,这个日志会源源不断的进来。现在刚打算处理,先处理前面20%的,提前评估一下。这一部分处理完成之后,如果没有问题,后续再处理这批日志。那么后面如果再处理,如何把前面这部分处理过得日志给过滤掉。

现在来实现一下,你从netcat上面输入,如果有:telangpu、kelindun、benladeng这三个名字。那么这三个名字会被过滤掉,不会被打印出来。

代码如下:

package com.ruozedata.spark.com.ruozedata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}

object TransformApp {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("TransformApp")
    val ssc = new StreamingContext(conf, Seconds(10))

    //这个是把server数据源转换成了DStream
    val lines = ssc.socketTextStream("hadoop001", 9999)

    //黑名单列表
    val blackList = List("telangpu","kelindun","benladeng")
    //列表转换为RDD,最后变为:(telangpu,true),(kelindun,true),(benladeng,true)
    val blackListRDD = ssc.sparkContext.parallelize(blackList).map(x => (x,true))


    /**
      * 格式:zhangsan,20,0  名字,年龄,性别
      * 变成==> (zhangsan,<zhangsan,20,0>)
      */
      val result = lines.map(x => (x.split(",")(0),x))
      .transform(rdd =>{
        //关联后的格式为:RDD[(K, (V, Option[W]))]
        //没有关联上:(zhangsan,(<zhangsan,20,0>,null))
        //或者关联上:( telangpu, (<telangpu,20,0>,(telangpu,true)) )
        rdd.leftOuterJoin(blackListRDD)
          //取关联后的<k,<k,v>>中的<k,v>中的v,这个v可能是空,拿到就拿,拿不到就为false
          .filter(x => x._2._2.getOrElse(false) !=true)
          //关联上的,取<k,<k,v>>中的<k,v>中的k,就是<zhangsan,20,0>
          //原样进来的原样回去,因为只是过滤掉黑名单而已
          .map(x => x._2._1)
      })

    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
}

输入:

[root@hadoop001 ~]# nc -lk 9999
zhangsan,20,1
telangpu,60,1
zhangsan,20,1
telangpu,60,1

zhangsan,20,1
telangpu,60,1

zhaoliu,30,1
kelindun,70,0

输出:
可以看到带有(“telangpu”,“kelindun”,“benladeng”)都被过滤掉了:

-------------------------------------------
Time: 1564468070000 ms
-------------------------------------------
zhangsan,20,1
zhangsan,20,1

-------------------------------------------
Time: 1564468080000 ms
-------------------------------------------
zhangsan,20,1

-------------------------------------------
Time: 1564468090000 ms
-------------------------------------------
zhaoliu,30,1

然后可以去WebUI上面看一下DGA图:

be sparkling什么意思 sparking是什么意思啊_Transform_07


前面两个是skipped是因为在流处理之前,两个已经准备好了,跟流处理没关系。后面可以看出,先做join,再做filter,再做map。

但这个性能并不好。可以使用更简单的方式来处理。

Window Operations(了解)

这个了解下。

Spark Streaming也提供窗口化计算,这允许你在滑动的数据窗口上 使用transformations算子。下图说明该滑动窗口。

be sparkling什么意思 sparking是什么意思啊_foreachRDD_08


上图,假定1秒一个批次,总共5个批次。

就像上图展示的那样,每次窗口滑过源过一个Source DStream时,落在窗口内的源Source RDDs被组合并操作以产生窗口DStream的RDD。在这个特定情况中,该操作被应用到 在最后3个时间单位(3个批次)的数据上 并滑过2个时间单位。这表明任何窗口操作需要指定两个参数。

  • window length - The duration of the window (3 in the figure).
    窗口长度 —— 窗口的持续时间(图表中是3)
  • sliding interval - The interval at which the window operation is performed (2 in the figure).
    滑动间隔 —— 窗口操作的执行间隔(图表中是2)

这个两个参数必须是源source DStream的批处理间隔的倍数(图表中是1)。

让我们用个例子说明窗口操作。假设你想要通过在最后的30秒数据中每10秒生成一个word counts来扩展前面的示例。为了做到这一点,我们必须在最后的30秒数据内在(word,1)键值对 的 pairs DStream 上使用reduceByKey操作。这是使用reduceByKeyAndWindow操作完成的。

val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))

// Reduce last 30 seconds of data, every 10 seconds
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))

一些普通窗口操作如下。这些操作全部都有上述说的两个参数——windowLength和slideInterval。具体详情参考官网。

Transformation

Meaning

window(windowLength, slideInterval)

基于在源source DStream上的窗口化批处理计算来返回一个新的DStream。

countByWindow(windowLength, slideInterval)

返回流中元素的滑动窗口数量。

reduceByWindow(func, windowLength, slideInterval)

返回一个新的单元素流,它是通过使用func经过滑动间隔聚合流中的元素来创建的。

reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])

当在元素类型为(K,V)对的DStream调用时,返回一个新的元素类型为(K,V)对的DStream,其中每个key键的值在滑动窗口中使用给定的reduce函数func来进行批量聚合。

reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])

上述reduceByKeyAndWindow()的一个更高效的版本,其中每个窗口的reduce值是使用前一个窗口的reduce值递增计算的。这是通过减少进入滑动窗口的新数据并“反转减少”离开窗口的旧数据来完成的。示例如当窗口滑动时“增加并减少”key键的数量。然而,它仅适用于“可逆减函数”,即具有相应“反减inverse reduce”函数的函数(作为参数invFunc)。如reduceByKeyAndWindow,reduce任务的数量是通过一个可选参数来设置的。注意使用这个操作checkpointing必须是能启用的。

countByValueAndWindow(windowLength, slideInterval, [numTasks])

当在元素类型为(K,V)对的DStream上调用时,返回一个新的元素类型为(K,Long)对的DStream,其中每个key键的值为它在一个滑动窗口出现的频率。如在reduceByKeyAndWindow中,reduce任务的数量是通过一个可选参数设置的。

案例代码:

package com.ruozedata.spark.com.ruozedata.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds , StreamingContext}

object SocketWCStateApp4 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SocketWCStateApp4")
    val ssc = new StreamingContext(conf, Seconds(10))
    //这个是把server数据源转换成了DStream
    val lines = ssc.socketTextStream("hadoop001", 9999)

    val pairs = lines.flatMap(_.split(",")).map(word => (word, 1))
    //val results = pairs.reduceByKey(_ + _)
    val resultsWindow = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(30),Seconds(10))

    resultsWindow.print()

    ssc.start()
    ssc.awaitTermination()
  }
}
[root@hadoop001 ~]# nc -lk 9999
word,china,love
word,china,love,word

可能的需求,比如说,统计某个范围内的数据等。
不过这个算子也可以用这个算子之外的方法来实现,比如还是每个批次每个批次的处理,然后写到数据库里,然后用SQL来查询。

Join Operations
Output Operations on DStreams

Output Operations on DStreams允许将数据push到外部系统,如数据库或文件系统上。
这个相当于action操作。由于output操作实际上允许被转换的数据外部系统消费,所以这些output操作会触发所有DStream transformations的执行(和RDD里的action操作相似)。
如:print()、saveAsTextFiles(prefix, [suffix])、saveAsObjectFiles(prefix, [suffix])、saveAsHadoopFiles(prefix, [suffix])、foreachRDD(func)等

中间三个算子基本不用,不建议使用,print()可以用于测试。生产上核心的是:foreachRDD(func)

重点: foreachRDD(func):The most generic output operator that applies a function, func, to each RDD generated from the stream. This function should push the data in each RDD to an external system, such as saving the RDD to files, or writing it over the network to a database. Note that the function func is executed in the driver process running the streaming application, and will usually have RDD actions in it that will force the computation of the streaming RDDs.
重点: foreachRDD(func):这是一个最通用的输出的操作,它将一个函数func作用于 由数据流产生的每个RDD 之上。这个函数是将每个RDD里的数据push到一个外部系统上,比如:把RDD保存到文件里,或者通过网络写到数据库里。需要注意的是 这个函数func是在driver进程中被执行的,这个driver进程运行着streaming程序,通常会在这个函数func中有RDD action操作,从而强制执行 streaming RDDs的计算。

Design Patterns for using foreachRDD(重点)

dstream.foreachRDD是一个功能强大的原语primitive,它允许将数据发送到外部系统。然而,了解如何正确并高效地使用原语primitive是很重要的。如下可以避免一些普通错误。

(面试会被问到)
下面是每个版本迭代,每个版本会出现什么问题?连接池pool又为什么会出现?

通常写数据到外部系统要求创建一个连接对象(例如远程服务器的TCP连接)并使用它发送数据到远程系统。为了达到这个目的,开发者可能会不小心尝试在Spark driver驱动程序中创建连接对象,然后尝试在Spark worker节点中使用它来将记录保存在RDD中。如scala中的示例。

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分区使用该连接发送所有的记录。

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

这会缓解许多记录中的连接创建开销。

最终,通过在多个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通过RDD actions延迟执行一样。具体来说,DStream输出操作中的RDD actions会强制处理接收到的数据。因此,如果你的应用程序没有任何输出操作,或者有输出操作但没有任何RDD action在里面,如dstream.foreachRDD(),那么什么都不会执行的。系统将会简单地接收数据并丢弃它。
  • 默认的,输出操作时一次一个执行的。而且它们按照在应用程序中定义的顺序执行。
案例分析

接着上面的mapWithState案例,下面继续案例分析。本次主要针对三个算子:mapWithState、foreachRDD、foreachPartition 。

上面的wordcount案例中,当<key,value>有state状态更新时,使用的是updateStateByKey,这个是Spark老版本的使用。在Spark新版本中,推荐不要使用updateStateByKey,而是推荐使用mapWithState。但是mapWithState目前还是实验性的,暂时还不建议投入生产,以后很有可能会投入使用。

考虑一个问题,上面的输出只是输出到控制台上面了(用的是:state.print()),但是生产上肯定不是这样的,生产上是要输出到RDBMS/NoSQL里面去的。
现在来实现一下,把结果写到MySQL里面去。
先在MySQL数据库test库下面创建一张表:

create table wc(word varchar(20) default null,count int(10));

可以这样:state.foreachRDD(函数)

看一下foreachRDD源码:
foreachRDD(函数)把一个函数作用于DStream中的每个RDD,它是一个输出操作,因此这个DStream将会注册成为一个输出流,然后被物化。

/**
   * Apply a function to each RDD in this DStream. This is an output operator, so
   * 'this' DStream will be registered as an output stream and therefore materialized.
   */
  def foreachRDD(foreachFunc: (RDD[T], Time) => Unit): Unit = ssc.withScope {
    // because the DStream is reachable from the outer object here, and because
    // DStreams can't be serialized with closures, we can't proactively check
    // it for serializability and so we pass the optional false to SparkContext.clean
    foreachRDD(foreachFunc, displayInnerRDDOps = true)
  }

state.foreachRDD(函数)
state是结果,现在考虑如何写这个函数,然后把结果写到MySQL数据库里?
代码如下:

package com.ruozedata.spark.com.ruozedata.spark.streaming
import java.sql.DriverManager
import org.apache.spark.SparkConf
import org.apache.spark.internal.Logging
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}

object SocketWCStateApp3 extends Logging{
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SocketWCStateApp3")
    val ssc = new StreamingContext(conf, Seconds(10))
    //就算用mapWithState也是要用checkpoint的,因为还是要保留之前的状态到一个地方去
    ssc.checkpoint(".")

    //这个是把server数据源转换成了DStream
    val lines = ssc.socketTextStream("hadoop001",9999)

    val wordCounts = lines.flatMap(_.split(",")).map(word => (word, 1)).reduceByKey(_ + _)

    //参考源码,这个是把一个匿名函数赋值给一个变量
    //传进来word单词和value值,比如(hello,3),还有一个state状态
    //Option[Int]表示有可能是某个单词是第一次进来,值为0
    val mappingFunc = (word: String, value: Option[Int], state: State[Int]) =>{

      //获取value的值,如果没有就取0;获取之前的状态的值,如果没有就取0;
      // 然后新的值和老的值相加;相加后赋值给变量sum
      val sum = value.getOrElse(0) + state.getOption().getOrElse(0)

      //用sum更新state的值,把state的值更新到最新
      state.update(sum)

      //返回一个(key,value),就是最新的
      (word,sum)
    }
    val state = wordCounts.mapWithState(StateSpec.function(mappingFunc))

    //state.print()

    //把输出结果写到MySQL数据库里
    state.foreachRDD(rdd => {
      val connection = getConnection()
      rdd.foreach(kv =>{
        val sql =s"insert into wc(word,count) values('${kv._1}','${kv._2}')"
        connection.createStatement().execute(sql)
      })
      connection.close()
    })


    ssc.start()
    ssc.awaitTermination()
  }


  //定义一个获取连接的函数
  def getConnection() = {
    Class.forName("com.mysql.jdbc.Driver")
    DriverManager.getConnection("jdbc:mysql://hadoop001:3306/test","root","123456")
  }
}

nc -lk 9999命令启动起来,运行一下程序

报错:
org.apache.spark.SparkException: Task not serializable
.......省略1000字
Caused by: java.io.NotSerializableException: com.mysql.jdbc.SingleByteCharsetConverter
Serialization stack:
	- object not serializable (class: com.mysql.jdbc.SingleByteCharsetConverter, value: com.mysql.jdbc.SingleByteCharsetConverter@289969df)
	- writeObject data (class: java.util.HashMap)
	- object (class java.util.HashMap, {Cp1252=com.mysql.jdbc.SingleByteCharsetConverter@289969df, UTF-8=java.lang.Object@4a589a56, US-ASCII=com.mysql.jdbc.SingleByteCharsetConverter@3d186411, utf-8=java.lang.Object@4a589a56})
	- field (class: com.mysql.jdbc.ConnectionImpl, name: charsetConverterMap, type: interface java.util.Map)
	- object (class com.mysql.jdbc.JDBC4Connection, com.mysql.jdbc.JDBC4Connection@5a313f93)
	- field (class: com.ruozedata.spark.com.ruozedata.spark.streaming.SocketWCStateApp3$$anonfun$main$1$$anonfun$apply$1, name: connection$1, type: interface java.sql.Connection)
	- object (class com.ruozedata.spark.com.ruozedata.spark.streaming.SocketWCStateApp3$$anonfun$main$1$$anonfun$apply$1, <function1>)
......

从上面报错可以看出,是因为connection不能被serializable序列化。这个在上面foreachRDD已经讲过。connection对象需要被序列化,而且要从driver端发送到worker端。但是connection对象是不能被跨机器传输的。所以上面写的是不对的。解决方法是在worker端去创建connection对象。
假如你在外面定义了某个东西,如某个类,那么如果在foreachRDD(rdd => {…})里面用到了这个东西,那么你必须把它序列化。
可以看到Spark Streaming和数据库交互,很容易出现坑,容易出错。

然后修改一下代码:
把val connection = getConnection()放到foreach里面,就会在worker端进行获取连接

//把输出结果写到MySQL数据库里
    state.foreachRDD(rdd => {
      rdd.foreach(kv =>{
        val connection = getConnection()
        logError("...............")
        val sql =s"insert into wc(word,count) values('${kv._1}','${kv._2}')"
        connection.createStatement().execute(sql)
        connection.close()
      })
    })

这样就ok了。
输入:

[root@hadoop001 ~]# nc -lk 9999
love word  word china
love word  word china

去MySQL看一下:

mysql> select * from wc;
+-------+-------+
| word  | count |
+-------+-------+
| love  |     2 |
| word  |     4 |
| china |     2 |
+-------+-------+
3 rows in set (0.00 sec)

mysql>

上面虽然看着很OK,但是有一个问题,就是导致另一个普遍的错误 —— 为每条记录都创建一个新的连接。上面也讲过,通常,创建一个连接对象有时间和资源的开销。因此,为每条记录创建和销毁连接对象可能会产生不必要的高开销,并且会显著地降低系统的整体吞吐量。一个更好的解决方案是使用rdd.foreachPartition —— 创建一个单连接对象并在RDD分区使用该连接发送所有的记录。创建一个connection对象,给一个partition使用。

foreachRDD里面用foreachPartition, foreachPartition里面再用foreach。三层结构。
这一条线路,是Spark Streaming写数据库唯一的一条正确的线路。 所以务必掌握。

//把输出结果写到MySQL数据库里
    state.foreachRDD(rdd => {
      rdd.foreachPartition(partitionOfRecords =>{
      	//生产上不能这样转成list,数据量很大会出问题
        val list = partitionOfRecords.toList
        if (list.size>0){
          val connection = getConnection()
          logError("...............")
          list.foreach(kv => {
            val sql =s"insert into wc(word,count) values('${kv._1}','${kv._2}')"
            connection.createStatement().execute(sql)
          })
          connection.close()
        }
      })
    })

进一步的,通过在多个RDD/批次重用连接对象,可以进一步优化这个功能。我们可以维护一个静态的可重用的连接对象池,因为多个批处理的RDD被推送到外部系统,从而进一步降低了开销。

那么如何去开发一个连接对象池?
添加依赖:
(需要用到bonecp这个工具:https://mvnrepository.com/artifact/com.jolbox/bonecp/0.8.0.RELEASE

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.28</version>
    </dependency>
    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.8.0.RELEASE</version>
    </dependency>

connection连接池代码:

package com.ruozedata.spark.com.ruozedata.spark.streaming

import java.sql.{Connection, DriverManager}
import com.jolbox.bonecp.{BoneCP, BoneCPConfig}
import org.slf4j.LoggerFactory

object ConnectionPool {
  val logger = LoggerFactory.getLogger(this.getClass)

  val pool = {
    try{
      Class.forName("com.mysql.jdbc.Driver")
      val config = new BoneCPConfig()
      config.setJdbcUrl("jdbc:mysql://hadoop001:3306/test")
      config.setUsername("root")
      config.setPassword("123456")
      config.setMinConnectionsPerPartition(2)
      config.setMaxConnectionsPerPartition(5)
      config.setCloseConnectionWatch(true)

      Some(new BoneCP(config))
    }catch {
      case e:Exception => {
        e.printStackTrace()
        None
      }
    }
  }

  def getConnection():Option[Connection] = {
    pool match {
      case Some(pool) => Some(pool.getConnection)
      case None => None
    }
  }

  def returnConnection(connection:Connection) = {
    if(null != connection){
      connection.close()
    }
  }
}

如何使用上面的连接池?

//把输出结果写到MySQL数据库里
    wordCounts.foreachRDD(rdd => {
      rdd.foreachPartition(partitionOfRecords =>{
        val list = partitionOfRecords.toList
        if (list.size>0){
          val connection = ConnectionPool.getConnection().get
          logError("...............")
          list.foreach(kv => {
            val sql =s"insert into wc(word,count) values('${kv._1}','${kv._2}')"
            connection.createStatement().execute(sql)
          })
          ConnectionPool.returnConnection(connection)
        }
      })
    })

上面的连接池还有很多需要优化的地方,比如pool前面可以加private,return里面最好不要关闭,还有超时就会关闭连接等等。在这里只是大致框架。

DataFrame and SQL Operations

你可以在streaming流数据上很容易地使用DataFrame和SQL操作。你必须通过使用StreamingContext正在使用的SparkContext创建SparkSession。此外,必须这样做才能在driver驱动程序故障时重新启动。这是通过创建SparkSession的一个延迟的实例化单例实例来完成的。这在下面的实例会展示。它通过使用DataFrames和SQL来修改前面的word count例子来产生word counts单词数量。每个RDD都可转换为一个DataFrame,注册为一个临时表,然后使用SQL进行查询。

/** DataFrame operations inside your streaming program */

val words: DStream[String] = ...

words.foreachRDD { rdd =>

  // Get the singleton instance of SparkSession
  val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
  import spark.implicits._

  // Convert RDD[String] to DataFrame
  val wordsDataFrame = rdd.toDF("word")

  // Create a temporary view
  wordsDataFrame.createOrReplaceTempView("words")

  // Do word count on DataFrame using SQL and print it
  val wordCountsDataFrame = 
    spark.sql("select word, count(*) as total from words group by word")
  wordCountsDataFrame.show()
}
完整的源代码请看:(案例写的非常好,需要好好学习)
https://github.com/apache/spark/blob/v2.4.3/examples/src/main/scala/org/apache/spark/examples/streaming/SqlNetworkWordCount.scala

你也可以在来自不同线程(即与正在运行的StreamingContext异步)的streaming流数据定义的table表上运行SQL查询。只要确保你将StreamingContext设置为记住足够多的streaming流数据,以便查询可以运行。否则,不知道任何异步SQL查询的StreamingContext将会在查询完成之前删除旧streaming流数据。例如,如果你想要查询最后一个批次,但是查询可能花费5分钟才能运行,请调用streamingContext.remember(Minutes(5))(以Scala或其他语言的等效方式)。

MLlib Operations

你可以很容易地使用MLlib提供的机器学习算法。首先,streaming流机器学习算法(例如流式线性回归Streaming Linear Regression,Streaming KMeans等等)可以从steaming流数据中学习并在将模型应用在streaming流数据上。除此之外,对于机器学习算法更大的类,你可以离线学习一个学习模型(即使用历史数据),然后将线上模型应用于streaming流数据。详情参阅MLlib指南。