Spark内容分享(六):Spark Streaming 详解

目录

1. 概述

2. WordCount 示例

3. 基本概念

4. 性能调优

5. 容错语义

6. 写在最后


1. 概述

Spark Streaming 是 Spark core API 的扩展,支持实时数据流的可扩展、高吞吐量、容错流处理。可以从 Kafka、Kinesis 或 TCP socket 等多种数据源获取,并使用高级函数(如 map、reduce、join 和 window)表达复杂算法进行处理。最后,处理后的数据推送到文件系统、数据库和实时 Dashboard。同时,可以在数据流上应用 Spark 的机器学习和图形处理算法。

 在内部,它的工作原理如下。Spark 流式处理接收实时输入数据流并将数据分成批处理,然后由 Spark 引擎进行处理,批量生成最终的结果流。

Spark Streaming 提供了一种称为 discretized stream(离散化流)或 DStream 的高级抽象,它表示连续的数据流。DStreams 可以从来自 Kafka 和 Kinesis 等源的输入数据流创建,也可以通过对其他 DStreams 应用高级操作来创建。在内部,DStream 表示为 RDD 序列。 

2. WordCount 示例

① 模拟 TCP socket 数据服务,模拟文本数据数据流。

注意:windows 可以使用 netcat,下载地址:

https://eternallybored.org/misc/netcat/

② 编写程序

object WordCount {

  def main(args: Array[String]): Unit = {

    // 初始化 SparkContext
    val conf: SparkConf = new SparkConf()
      .setAppName(s"${WordCount.getClass.getSimpleName}")
      .setMaster("local[*]")
    val sc:SparkContext = new SparkContext(conf)

    // 每隔5秒分一个批次
    val ssc = new StreamingContext(sc, Seconds(5))

    //1.加载数据
    val lines: ReceiverInputDStream[String] =
      ssc.socketTextStream("10.115.88.47",8888)

    //2.处理数据
    val resultDS: DStream[(String, Int)] =
      lines.flatMap(_.split(" "))
        .map((_, 1))
        .reduceByKey(_ + _)

    //3.输出结果
    resultDS.print()

    //4.启动并等待结果
    ssc.start()
    ssc.awaitTermination()

    //5.关闭资源
    ssc.stop(stopSparkContext = true,stopGracefully = true)

  }
}

③ 模拟输入 word

④ 测试结果。

 

3. 基本概念

3.1 StreamingContext

Spark Streaming 程序,首先要创建一个 StreamingContext 对象,该对象是所有 Spark Streaming 所有流操作的主要入口点。可以通过 SparkConf 创建 StreamingContext 对象。

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

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

根据应用程序和可用群集资源的延迟要求设置批处理间隔:

import org.apache.spark.streaming._

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

定义上下文后,执行以下操作:

① 通过创建 DStreams 来定义输入输入源。

② 通过对 DStream 应用转换(transformation)和输出(output)操作来定义流计算。

③ 开始使用 streamingContext.start() 接收数据并进行处理。

④ 使用 streamingContext.awaitTermination() 等待处理停止(手动或出现异常)。

⑤ 可以使用 streamingContext.stop() 手动停止处理。

注意:

① 一旦启动上下文(context),就不能设置或添加新的流计算操作。

②  一旦停止上下文(context),就无法重新启动

③ JVM 中同一时间只能存在一个 StreamingContext 处于活动状态。

④ StreamingContext 上的 stop() 也会停止 SparkContext。若要仅停止 StreamingContext,将 stop() 的可选参数 stopSparkContext设置为 false。

⑤ 只要在创建下一个 StreamingContext 之前停止(不停止 SparkContext)上一个 StreamengContext,就可以重新使用 SparkContext 来创建多个 StreamngContext。

3.2 离散化流(DStreams)

DStream 是 Spark Streaming 的基本抽象。它表示连续的数据流,既可以是从源接收的输入数据流,也可以是通过转换操作输入流生成的处理后的数据流。在内部,DStream 由一系列连续的 RDD 组成,是 Spark 对不可变的分布式数据集的抽象。DStream 中的每个 RDD 都包含来自特定时间间隔的数据,如下图所示:

DStream 的任何操作都会转换为底层 RDD 上的操作。例如,在 WordCount 示例中,对 lines DStream 中的每个 RDD 应用 flatMap 操作,生成 word DStream 的 RDD。如下图所示:

// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

// Split each line into words
val words = lines.flatMap(_.split(" "))

 

这些底层 RDD 转换为 Spark 引擎计算。DStream 操作隐藏了大部分细节,并为开发人员提供了更高级别的 API,这些 API 后面具体介绍。

3.3 Input DStreams and Receivers(接收器)

Input DStream 表示来自某个数据源的输入数据流。在 wordcount 例子中,lines 就是一个 Input DStream,代表了从 netcat(nc)服务接收到的数据流。除了文件数据流之外,所有的输入 DStream 都会绑定一个 Receiver 对象,用来从数据源接收数据并将其存储在 Spark 的内存中,以供后续处理。

Spark Streaming 提供了两种内置的数据源支持:

基本数据源:这些源在 StreamingContext API 中可以直接使用。示例:文件系统和 Socket 连接

高级数据:像 KafkaKinesis 等源可以通过额外依赖包获得。

注意,如果要在流应用程序中并行接收多个数据流,可以创建多个 Input DStream。这样就会创建多个 Receiver,并行地接收多个数据流。但是,一个 Spark Streaming 应用的 Executor 是一个长时间运行的任务,它会独占分配给 Spark 程序的 CPU core。所以需要分配足够的内核(如果是本地运行,那么是线程)来处理接收的数据以及运行 Receiver。

3.3.1 基本数据源

前面 wordcount 中已经介绍了,可以通过 ssc.socketTextStream() 从 TCP Socket 接收的文本数据创建 DStream。除了 Socket 之外,StreamingContext API 还提供了 ssc.socketTextStream() 方法实现从文件作为输入源创建 DStreams。

文件流

为了从与 HDFS API 兼容的其它文件系统(即 HDFS、S3、NFS 等)上的文件中读取数据,可以通过,

 .StreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass]

对于简单的文本文件,最简单的方法是:

.StreamingContext.textFileStream(dataDirectory)

如何监听文件目录变化?

① 可以监视一个简单的目录,例如“hdfs://namenode:8040/logs/“,路径下发现新文件时进行处理。

② 可以提供类似 “hdfs://namenode:8040/logs/2017/* 这样的表示,它代表目录模式而不是目录中的文件。

③ 所有文件的数据格式必须相同。

④ 基于文件的修改时间而不是创建时间。

⑤ 当前窗口中的文件更改,不会导致重新读取文件。

⑥ 目录下的文件越多,扫描修改情况所需的时间就越长。

⑦ 如果使用通配符来标识目录,重命名整个目录以匹配路径会将该目录添加到受监视的目录列表中。

⑧ 通过调用 FileSystem.setTimes() 来设置时间戳,重新选取文件(即使内容没有更改)。

3.3.2 基于自定义 Receiver 

Spark Streaming 可以接收任何类型数据源的流数据,而不限于内置支持的数据源(Kafka、Kinesis、文件、套接字等),只需要实现一个 Receiver。

① 实现方法

实现 Receiver 接口,并复写 onStart() 和 onStop() 两个方法:

onstart():Receiver 开始运行时触发方法,在该方法内需要启动一个线程,用来接收数据。

onstop():Receiver 结束运行时触发的方法,在该方法内需要确保停止接收数据。

② 数据存储

一旦接收完数据,就可以通过调用 store(data)将数据存储在 Spark 中,store 是 Receiver 类提供的方法,支持各种类型的数据存储。store 方法是以一次存储一条记录或者一次性收集全部的序列化对象。

③ 异常处理

应捕获并正确处理接收线程中的任何异常,以避免 Receiver 出现故障而不被发现。故障发生后 Receiver 提供了 restart(exception)方法用来重新启动,重启过程为,异步调用 onstop 方法,然后调用 onstart 方法重新接收消息。

④ 具体示例

下面是通过 Socket 接收文本流的自定义 Receiver。如果接收线程在连接或接收时出错,则重新启动接收器以再次尝试连接。

class CustomReceiver(host: String, port: Int)
  extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) with Logging {

  def onStart() {
    // Start the thread that receives data over a connection
    new Thread("Socket Receiver") {
      override def run() { receive() }
    }.start()
  }

  def onStop() {
    // There is nothing much to do as the thread calling receive()
    // is designed to stop by itself if isStopped() returns false
  }

  /** Create a socket connection and receive data until receiver is stopped */
  private def receive() {
    var socket: Socket = null
    var userInput: String = null
    try {
      // Connect to host:port
      socket = new Socket(host, port)

      // Until stopped or connection broken continue reading
      val reader = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
      userInput = reader.readLine()
      while(!isStopped && userInput != null) {
        store(userInput)
        userInput = reader.readLine()
      }
      reader.close()
      socket.close()

      // Restart in an attempt to connect again when server is active again
      restart("Trying to connect again")
    } catch {
      case e: java.net.ConnectException =>
        // restart if could not connect to server
        restart("Error connecting to " + host + ":" + port, e)
      case t: Throwable =>
        // restart if there is any other error
        restart("Error receiving data", t)
    }
  }
}

使用自定义接收器:

// Assuming ssc is the StreamingContext
val customReceiverStream = ssc.receiverStream(new CustomReceiver(host, port))
val words = customReceiverStream.flatMap(_.split(" "))
...

3.3.3 RDD 队列作为流

为了使用测试数据测试 Spark Streaming 应用程序,还可以使用:

streamingContext.queueStream(queueOfRDD)

基于 RDD 队列创建 DStream,进入队列的每个 RDD 将被视为 DStream 中的一批数据,并像流一样进行处理。

3.4 DStreams 上的转换

与 RDD 类似,转换操作可以修改 Input DStream(输入流)的数据。一些常见的函数包括:

① map(func)

利用函数 func 处理原 DStream 的每个元素,返回一个新的 DStream。

② flatMap(func)

与 map 相似,但是每个输入项可用被映射为 0 个或者多个输出项。

③ filter(func)

返回一个新的 DStream,它仅仅包含源 DStream 中满足函数 func 的项。

④ repartition(numPartitions)

通过创建更多或者更少的 partition 改变 DStream 的并行级别(level of parallelism)。

⑤ union(otherStream)

返回一个新的 DStream,它包含源 DStream 和 otherStream 的联合元素。

⑥ count()

通过计算源 DStream 中每个RDD的元素数量,返回一个包含单元素(single-element)RDDs的新DStream

⑦ reduce(func)

利用函数 func 聚集源 DStream 中每个 RDD 的元素,返回一个包含单元素(single-element)RDDs 的新 DStream。函数应该是相关联的,使计算可以并行化。

⑧ countByValue()

这个算子应用于元素类型为 K 的 DStream 上,返回一个(K,long)对的新 DStream,每个键的值是在原 DStream 的每个 RDD 中的频率。

⑨ reduceByKey(func, [numTasks])

在一个由(K,V)对组成的 DStream 上调用这个算子,返回一个新的由(K,V)对组成的DStream,每一个 key 的值均由给定的 reduce 函数聚集起来。

⑩ join(otherStream, [numTasks])

应用于两个 DStream(一个包含(K,V)对,一个包含(K,W)对),返回一个包含(K, (V, W))对的新 DStream。

⑪ cogroup(otherStream, [numTasks])

应用于两个 DStream(一个包含(K,V)对,一个包含(K,W)对),返回一个包含(K, Seq[V], Seq[W])的元组。

⑫ transform(func)

通过对源 DStream 的每个 RDD 应用 RDD-to-RDD 函数,创建一个新的 DStream。这个可以在 DStream中 的任何 RDD 操作中使用。

⑬ updateStateByKey(func)

利用给定的函数更新 DStream 的状态,返回一个新"state"的 DStream。

下面对一些转换操作详细讨论。

3.4.1 UpdateStateByKey

updateStateByKey 是有状态的转换操作,它为 Spark Streaming 中每个 Key 维护一个 state(可以是任意类型)状态,并对 state 进行状态更新。

如何使用?

 定义 state:状态可以是任意数据类型。

② 定义 state 更新函数:实现如何使用以前的 state 和输入流中的新值更新 state。

假设想对文本数据流中每个单词的运行次数,运行次数用一个state表示,它的类型是整数:

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)
}

这个函数用到包含 word 的 DStream (包含(word, 1)):

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val runningCounts = pairs.updateStateByKey[Int](updateFunction _)

将为每个 word 调用 update 函数,newValues 的序列为 1(来自(word,1)pairs),runningCount 拥有之前计数。

注意,使用 updateStateByKey 需要配置 checkpoint 目录。

3.4.2 窗口操作

Spark Streaming 还提供了窗口计算,应用程序可以在数据的滑动窗口中进行转换。

 

窗口长度 - 窗口的持续时间。

滑动间隔 - 执行窗口操作的间隔。

每次窗口停放的位置上,都会有一部分 DStream(或者一部分 RDD)被框入窗口内,形成一小段的 Dstream,然后启动对这个小段 DStream 的计算。比如上图,对每 3 秒钟的数据执行一次滑动窗口计算,这 3 秒内的 3 个 RDD 会被聚合起来进行处理,过了 2 秒钟后又会对最近 3 秒内的数据执行滑动窗口计算。所以每个滑动窗口操作,都必须指定两个参数,窗口长度以及滑动间隔,这两个参数必须是源 DStream 的批时间间隔的倍数。

一些常见的窗口操作:

转换操作意义
window(windowLength, slideInterval)基于源 DStream 产生的窗口化的批数据,计算得到一个新的 DStream。
countByWindow(windowLengthslideInterval)返回流中元素的滑动窗口计数。
reduceByWindow(funcwindowLengthslideInterval)返回一个元素流,该流是通过聚合流中的元素而创建的,使用func 的滑动间隔。该函数应该是关联和可交换的,从而支持并行计算。
reduceByKeyAndWindow(funcwindowLengthslideInterval, [numTasks])

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

reduceByKeyAndWindow(funcinvFuncwindowLengthslideInterval, [numTasks])

上述reduceByKeyAndWindow()的一个更有效的版本,其中每个窗口的reduce值是使用前一个窗口的reducte值递增计算的。这是通过减少进入滑动窗口的新数据和“反向减少”离开窗口的旧数据来实现的。一个例子是在窗口滑动时“加”和“减”键的计数。然而,它仅适用于“可逆归约函数”,即那些具有相应“逆归约”函数(作为参数invFunc)的归约函数。与reduceByKeyAndWindow类似,reduce任务的数量可以通过可选参数进行配置。请注意,必须启用检查点才能使用此操作。

countByValueAndWindow(windowLengthslideInterval, [numTasks])当调用(K,V)对的DStream时,返回(K,Long)对的新DStream,其中每个键的值是其在滑动窗口中的频率。与reduceByKeyAndWindow类似,reduce任务的数量可以通过可选参数进行配置。

3.4.3 连接操作

① Stream-Stream 连接

val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)

每个批处理间隔中,stream1 生成的 RDD 将与 stream2 生成的 RDD 连接。还可以执行 leftOuterJoin、rightOuterJon 和 fullOuterJoin。此外,在 Stream 窗口进行连接也非常有用,也很容易。

② Stream-Dataset 连接

val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)

还可以动态更改要加入的 Dataset  数据集。

3.5 DStreams 上的输出操作

输出操作含义
print()在运行流应用程序的驱动程序节点上打印DStream中每批数据的前十个元素。这对于开发和调试很有用。
saveAsTextFiles(prefix, [suffix])将此DStream的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS[.suffi]”。
saveAsObjectFiles(prefix, [suffix])将此DStream的内容保存为序列化Java对象的SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS[.suffi]”
saveAsHadoopFiles(prefix, [suffix])将此DStream的内容保存为Hadoop文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS[.suffi]”。
foreachRDD(func)最通用的输出运算符,函数 func 应用于每个从流生成的RDD。此函数可以将每个RDD中的数据推送到外部系统,例如将RDD保存到文件,或通过网络将其写入数据库。

3.6 DataFrame 和 SQL 操作

在流数据上进行 DataFrames 和 SQL 操作,必须使用 StreamingContext 关联的 SparkContext 来创建 SparkSession。此外,必须这样做的目的还在于在 driver 出现故障时重新启动。修改前面的 WordCount 示例,使用 DataFrames 和 SQL 生成单词计数。将每个 RDD 都转换为 DataFrame,注册临时表,然后使用 SQL 进行查询。

object SQLWordCount {

  def main(args: Array[String]): Unit = {
    // 初始化 SparkContext
    val conf: SparkConf = new SparkConf()
      .setAppName(s"${SQLWordCount.getClass.getSimpleName}")
      .setMaster("local[*]")
    val sc:SparkContext = new SparkContext(conf)

    // 每隔5秒分一个批次
    val ssc = new StreamingContext(sc, Seconds(5))

    // 加载数据
    val lines: ReceiverInputDStream[String] =
      ssc.socketTextStream("10.115.88.47",8888)

    // 按空格分割,转换成 word
    val words = lines.flatMap(_.split(" "))

    words.foreachRDD { rdd =>

      // 获取 SparkSession 单例
      val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
      import spark.implicits._

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

      // 创建临时视图 temporary view
      wordsDataFrame.createOrReplaceTempView("words")

      // 使用 SQL 查询 temporary view 并打印
      val wordCountsDataFrame =
        spark.sql("select word, count(*) as total from words group by word")
      wordCountsDataFrame.show()
    }

    ssc.start()
    ssc.awaitTermination()

    ssc.stop(stopSparkContext = true,stopGracefully = true)

  }
}

输入数据:

查询结果:

 

3.7 缓存/持久化

与 RDD 类似,DStream 也可以在内存中持久化流数据,对 DStream 使用 persist() 方法自动每个 RDD 保存在内存中。如果 DStream 中的数据会被多次计算(例如,对同一数据进行多次操作),缓存会非常有用。对于 reduceByWindow 和 reduceByKeyAndWindow 等基于窗口的操作以及 updateStateByKey 等基于状态的操作都是隐式执行的,所以 DStream 会自动保存在内存中而无需调用 persist() 方法。

对于通过网络接收数据的输入流(如 Kafka、Socket 等),默认的持久化级别会将数据复制到两个节点来实现容错。

3.8 Checkpointing(检查点)

流式应用程序必须全天候 7*24 小时运行,因此必须能够应对与应用程序逻辑无关的故障(例如,系统故障、JVM崩溃等)。为了实现这一点,Spark Streaming 需要足够的 Checkpointing 信息保存在存储系统中,来实现故障恢复。有两种类型的 Checkpointing:

① 元数据检查点(Metadata checkpointing)

保存流计算的定义信息到容错存储系统(如 HDFS )中,用来恢复 drvier 节点的故障。元数据包括:

    • Configuration :创建 Spark Streaming 应用程序的配置信息。

    • DStream 操作:定义流应用程序的一组 DStream 操作集合。

    • 未完成的批处理:操作存在队列中的未完成的批操作。

② 数据检查点(Data checkpointing)

当有状态的 transformation(如结合跨多个批次的数据)操作中,生成的 RDD 依赖于之前批的 RDD,随着时间的推移,这个“依赖链”的长度会持续增长。为了实现快速故障恢复,需要将这个有状态转换操作的中间 RDD 定时存储到可靠存储系统中,截断这个“依赖链”。

何时启用 checkpointing?

必须为具有以下要求的应用程序启用 checkpoint:

    • 使用有状态的转换算子:如 updateStateByKey or reduceByKeyAndWindow ,必须提供 checkpoint 目录,并设置检查点。

    • 从 driver 的故障中恢复:元数据检查点用于使用进度信息进行恢复。

如何配置 checkpointing?

通过容错、可靠的文件系统(例如 HDFS、S3 等)中设置一个目录来启用 checkpoint,可以使用 streamingContext.checkpoint(checkpointDirectory),将 checkpoint 信息保存到 checkpointDirectory 目录中。此外,如果想让应用程序从 drvier 故障中恢复,需要重新考虑应用程序的实现:

    • 当应用程序首次启动时,新建一个 StreamingContext,启动所有 Stream,然后调用 start() 方法。

    • 当应用程序因为故障重新启动时,它将会从 checkpoint 目录的 checkpoint 数据重新创建 StreamingContext。

// Function to create and setup a new StreamingContext
def functionToCreateContext(): StreamingContext = {
  val ssc = new StreamingContext(...)   // new context
  val lines = ssc.socketTextStream(...) // create DStreams
  ...
  ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
  ssc
}

// Get StreamingContext from checkpoint data or create a new one
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)

// Do additional setup on context that needs to be done,
// irrespective of whether it is being started or restarted
context. ...

// Start the context
context.start()
context.awaitTermination()

如果 checkpointDirectory 存在,则将根据 checkpoint 数据重新创建 context 。如果目录不存在(即第一次运行),则会调用函数 functionToCreateContext 来创建新的 checkpoint 并设置 DStream。

累加器和广播变量的 checkpoint

累加器(Accumulators)和广播变量(Broadcast variables)是无法从 Spark Streaming 的检查点中恢复回来的。所以如果开启了 checkpoint,并同时使用累加器和广播变量,最好使用延迟实例化的单例模式,这样累加器和广播变量才能在驱动器(driver)故障恢复后重新实例化。

object WordExcludeList {

  @volatile private var instance: Broadcast[Seq[String]] = null

  def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          val wordExcludeList = Seq("a", "b", "c")
          instance = sc.broadcast(wordExcludeList)
        }
      }
    }
    instance
  }
}

object DroppedWordsCounter {

  @volatile private var instance: LongAccumulator = null

  def getInstance(sc: SparkContext): LongAccumulator = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = sc.longAccumulator("DroppedWordsCounter")
        }
      }
    }
    instance
  }
}

wordCounts.foreachRDD { (rdd: RDD[(String, Int)], time: Time) =>
  // Get or register the excludeList Broadcast
  val excludeList = WordExcludeList.getInstance(rdd.sparkContext)
  // Get or register the droppedWordsCounter Accumulator
  val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
  // Use excludeList to drop words and use droppedWordsCounter to count them
  val counts = rdd.filter { case (word, count) =>
    if (excludeList.value.contains(word)) {
      droppedWordsCounter.add(count)
      false
    } else {
      true
    }
  }.collect().mkString("[", ", ", "]")
  val output = "Counts at time " + time + " " + counts
})

3.9 应用程序的部署、升级和监控

3.9.1 部署要求

 应用程序打 JAR 包,如果使用 spark-submit 来启动应用程序,则不需要 JAR 包中提供 spark 和 spark Streaming 相关的依赖包。

② 为 Executor 配置足够的内存,由于接收到的数据存储在内存中,所以 Executor 必须配置足够的内存来保存接收到的信息。

③ 配置 checkpoint,配置一个 Hadoop API 兼容的文件系统(比如 HDFS,S3等)的目录作为 checkpoint 目录。

④ 配置应用程序 driver 自动恢复,从 driver 故障中自动恢复,不同的集群管理器有不同的实现。

⑤ 配置预写日志(write-ahead),Spark 1.2 以后,引入了预写日志,目的时提供强大的容错保证。通过参数 spark.streaming.receiver.writeHeadLog 启用,Receiver 接收到的所有数据都会被写入配置的 checkpoint 目录中的预写日志。这种机制可以让 driver 在恢复的时候,避免数据丢失,并且可以确保整个实时计算过程中,零数据丢失。但同时会导致 Receiver 的吞吐量大幅度下降。

⑥ 设置 Receiver 接收速度,如果集群资源有限,流应用程序无法以接收数据的速度处理数据,Receiver 则可以通过设置记录/秒的最大速率进行限制。请参考配置参数 spark.streaming.receiver.maxRate 和 spark.streaming.kafka.maxRatePerPartition 参数可以用来设置,前者设置普通 Receiver,后者是 Kafka Direct 方式。Spark 1.5中,对于Kafka Direct方式,引入了 backpressure(背压) 机制,从而不需要设置Receiver 的限速,Spark 可以自动估算 Receiver 最合理的接收速度,并根据情况动态调整。只要将 spark.streaming.backpressure.enabled 设置为 true 即可。

3.9.2 升级代码

① 升级后的 Spark Streaming 应用程序直接启动并与现有应用程序并行运行。当确保新的应用程序启动没问题之后,就可以将旧的应用程序给停掉。但是要注意的是,这种方式适用于,能够将数据发送到两个目的地的数据源(即升级前和升级后的应用程序)。

② 首先现有应用程序优雅得关闭(StreamingContext.stop() 或  JavaStreamingContext.stopp()),确保在关闭之前完全处理已接收的数据。然后启动升级的应用程序,它将从上一版本的应用程序退出的同一点开始处理。注意,只能使用支持缓冲的输入源(如 Kafka),因为在升级前的应用程序关闭和升级后的应用程序启动前,需要缓冲数据。如果从先前的 checkpoint 信息点重新开始,是无法完成预先升级的程序代码的,checkpoint 本质上包含序列化后的 Scala/Java/Python 对象,将对象进行反序列化为新的对象,修改的类可能会导致错误,在这种情况下,可以让升级的应用程序使用不同的 checkpoint 目录或者删除以前的检查点目录。

简单来说:

线上的先不要停,将新代码部署之后在停掉旧的。

优雅关闭旧的,确保数据处理正常,然后启动新的。

3.9.3 监控

当 Spark Streaming 应用启动时,Spark Web UI 会显示一个独立的 Streaming tab 页,会显示 Receiver 的信息,比如是否活跃、接收到了多少数据、是否有异常等。还会显示完成的 batch 的信息,batch 的处理时间、队列延迟等。这些信息可以用于监控 Spark Streaming 应用的进度。

Web UI中的以下两个指标特别重要:

    • 处理时间,每个 batch 的数据的处理耗时。

    • 调度延迟,一个 batch 在队列中阻塞,等待上一个 batch 完成处理的时间。

如果 batch 的处理时间比 batch 的间隔要长的话,而且调度延迟时间持续增长,应用程序不足以使用当前设定的速率来处理接收到的数据,此时,可以考虑增加 batch 的间隔时间。

4. 性能调优

要想在集群上获得 Spark Streaming 应用程序的最佳性能,需要考虑两件事:

    • 如何有效地使用集群资源,减少 batch 数据的处理时间。

    • 如何设置的 batch size,以便在接收到 batch 数据时尽快处理(即,数据处理与数据摄取保持同步)。

 

4.1 设置正确的批处理间隔

为了让集群上运行的 Spark Streaming 应用程序保持稳定,系统应该能够像接收数据一样快速地处理数据。换句话说,成批数据的处理速度应与生成数据的速度一样快。通过 Streaming web UI中的处理时间,可以比较下 batch 数据的处理时间和间隔时间。

根据流计算的性质,batch 间隔时间可能会对应用程序数据速率产生重大影响。确定应用程序 batch 间隔时间一个好方式是使用保守的批处理间隔(例如 5-10 秒)和低数据速率对其进行测试。为了验证系统是否能够跟上数据速率,可以检查每个处理批次所经历的端到端延迟值(在Spark驱动程序log4j日志中查找“Total delay”,或使用 StreamingListener 界面)。

4.2 内存优化

下面主要针对 Spark Streaming 应用程序上下文中的几个调优参数进行介绍。

Spark Streaming 应用程序所需群集内存大小,很大程度上取决于所使用的转换操作类型。例如,如果想对最后 10 分钟的数据使用窗口操作,那么集群应该有足够的内存保存 10 分钟的时间。或者,如果想将 updateStateByKey 与大量键一起使用,则所需的内存将很高。相反,如果执行简单的 map-filter-store 等操作,则所需的内存会很低。

通常,由于 Receiver 会使用 StorageLevel.MEMORY_AND_DISK_SER_2 持久化级别来进行存储,无法保存的内存数据将溢出到磁盘,可能会影响性能,因此建议根据流应用程序需要提供足够的内存。最好尝试在小范围内查看内存使用情况,并进行相应的估算。

内存调整的另一个方面是垃圾回收。对于需要低延迟的流应用程序,应用程序不希望 JVM 垃圾回收导致大量的暂停。

下面几个参数可以调整内存使用率和 GC 开销:

① DStreams 的持久化级别

默认情况下,输入数据和 RDD 被持久化为序列化字节。与反序列化持久性相比,这减少了内存使用和 GC 开销。启用 Kryo 序列化进一步减少了序列化大小和内存使用。通过压缩(Spark.rdd.compress)可以进一步减少内存使用量,单这是以 CPU 时间为代价的。

② 清除旧数据

默认情况下,DStream 转换生成的所有输入数据和持久化 RDD 都会自动清除。Spark Streaming 根据使用的转换决定何时清除数据。例如,如果您使用的是 10 分钟的窗口操作,那么 Spark Streaming 将保留最后 10 分钟的数据,并主动丢弃旧数据。通过设置 streamingContext.rember,数据可以保留更长的时间(例如,交互查询旧数据)。

③ CMS 垃圾回收器

强烈建议使用  CMS(concurrent mark-and-sweep),减少 GC 时的暂停时间。尽管已知并发 GC 会降低系统的总体处理吞吐量,但仍建议使用它来实现更一致的批处理时间。确保在 driver (使用 spark-submit 时的 --driver-java-options 参数)和 Executor (使用 spark 配置的 spark.executor.extraJavaOptions 参数)上都设置了 CMS 垃圾回收期。

④ 其他优化

如果还想进一步减少 GC 开销,以下是更进一步的可以尝试的手段:

使用堆外内存(OFF_HEAP)来持久化RDD。

使用更多但是更小的执行器进程。这样 GC 压力就会分散到更多的 JVM 堆中。

5. 容错语义

5.1 容错机制的背景

为了理解 Spark Streaming 提供的语义,回顾下 RDD 的基本容错语义。

① RDD 是不可变的、确定的、可重新计算的分布式的数据集。每个 RDD 都会记住确定好的计算操作的血缘关系(lineage),这些操作在一个容错的数据集上来创建 RDD。

② 如果 RDD 的一个分区因 worker 节点故障而丢失,那么这个分区可以通过操作血缘从源容错的数据集中重新计算得到。

③ 假设所有 RDD 转换操作都是确定的,那么最终转换的 RDD 数据将始终相同,不论 Spark 集群中发生何种错误。

Spark 运行在像 HDFS 或 S3 等容错系统的数据上。然而,Spark Streaming 并非如此,因为 Spark Streaming 的数据大部分情况下是通过网络接收的(使用 fileStream 时除外)。为了所有生成的 RDD 实现相同的容错属性,接收的数据需要重复保存在 worker node 的多个 Executor 上(默认的副本数是 2),这会导致系统中的两种数据在发生故障时需要恢复:

    • 数据接收,并且已经复制,当单个 worker 节点出现故障时,这种情况的数据副本依然存在于其它 worker 节点上,因此数据不会丢失。

    • 数据接已接收,但正在缓存中,由于数据还没有被复制,因此恢复数据的唯一方法是从数据源重新获取。

此外,还有两种故障需要考虑:

    • worker 节点的故障:任何一个运行了 Executor 的 Worker 节点发生故障时,都会导致该节点上所有内存中的数据丢失。如果 Receiver 运行在此故障节点上时,则缓冲数据将会丢失。

    • driver 节点的故障:如果运行 Spark Streaming 应用程序的 drvier 节点发生故障,那么 SparkContext 会丢失,该应用程序的所有 Executor 相关的数据都会丢失。

有了这些基本知识,下面开始了解 Spark Streaming 的容错语义。

5.2 Spark Streaming 容错语义的定义

流式计算系统的容错语义,通常是根据系统每条记录能够被处理多少次来衡量的。有三种类型的语义可以提供:

① 最多一次:每条记录可能会被处理一次,或者根本就不会被处理。可能有数据丢失

② 至少一次:每条记录会被处理一次或多次,这种语义比最多一次要更强,因为它确保零数据丢失。但是可能会导致记录被重复处理几次。

③ 精准一次:每条记录只会被处理一次,没有数据会丢失,并且没有数据会处理多次。这是最强的一种容错语义

5.3 基本语义

在 Spark Streaming 中,处理数据都有三个步骤:

① 接收数据:使用 Receiver 或其它方式接收数据。

② 转换(计算)数据:使用 DStream 和 RDD 转换转换接收到的数据。

③ 推送数据:最终转换后的数据被推送到文件系统、数据库、Dashboard 等外部系统。

如果流式处理应用程序必须实现端到端精准一次保证,则每个步骤都必须提供精准一次的予以。也就是说,每条记录必须只接收一次,只转换一次,并精确推送到下游系统一次。下面在 Spark Streaming 的上下文中理解这些步骤的语义:

① 接收数据:不同的数据源提供不同的语义保障。

② 转换数据:所有已接收的数据一定只会被计算一次,这是基于 RDD 的基础语义所保障的。即使出现故障,只 要接收到的数据是可访问的,最终转换后的 RDD 将始终具有相同的内容。

 推送数据:默认输出操作能确保至少一次的语义,因为它取决于输出操作的类型(幂等或非幂等)和下游系统的语义(是否支持事务)。但用户可以自己实现事务机制来保证精准一次的语义。

5.3.1 接收数据的容错语义

不同的输入源提从至少一次精确一次供了不同的保证。

① 基于文件的数据源

如果所有的输入数据都已存在于 HDFS 等容错文件系统,Spark Streaming 一定可以从任何故障中恢复并处理所有数据。这提供了精准一次语义,意味着所有的数据只会处理一次。

② 基于 Receiver 的数据源

对于基于 Receiver 的输入源,容错语义取决于故障场景和 Receiver 类型。

可靠的 Receiver:这种 Receiver 会在接收到了数据,并且将数据复制之后,才对数据源执行确认操作。如果 Receiver 发生故障(数据接收和复制完成之前),那么数据源不会接收到缓冲(未复制)数据的确认。因此,当 Receiver 重新启动,数据源会重新发送数据,并且不会因故障而丢失任何数据。

不可靠的 Receiver:这种 Receiver 不会发送确认操作,因此当 worker 或者 driver 节点失败的时候,可能会导致数据丢失。

根据 Receiver 的类型,提供了不同的语义。如果 work 节点发生故障,那么可靠的 Receiver 不会丢失数据。对于不可靠的 Receiver,接收到但未复制的数据可能会丢失。如果 driver 节点发生故障,所有过去接收到的和复制过缓存在内存中的数据,全部会丢失。这将影响有状态转换的结果。

为了避免这种过去接收的所有数据都丢失的问题,Spark 1.2 引入了预写日志,将 Receiver 接收的数据保存到容错存储中。由于启用了预写日志和可靠的 Receiver,所以不会丢失数据。在语义方面,它提供了至少一次的语义保证。

下表总结了故障下的语义:

部署方案worker 故障driver 故障

Spark 1.1 或更早版本,

Spark 1.2 或更高版本,不带预写日志

使用不可靠的 Receiver 丢失缓冲数据

使用可靠的 Receiver 实现零数据丢失

至少一次语义

不可靠的 Receiver 丢失缓冲数

所有  Receiver 丢失过去的数据

未定义的语义
Spark 1.2 或更高版本,带有预写日志使用可靠的 Receiver

实现零数据丢失

至少一次语义

通过可靠的 Receiver 和文件实现零数据丢失

至少一次语义

③ 使用 Kafka Direct API

从 Spark 1.3 版本开始,引入了新的 Kafka Direct API,可以保证从 Kafka 接收到的数据是精准一次的。如果自己再实现精准一次的输出操作,那么就可以实现整个 park Streaming 应用程序精准一次的语义。

5.3.2 输出操作的容错语义

通常情况下,输出操作(如 foreachRDD )可以提供至少一次的语义。也就是说,在 worker 节点失败的情况下,转换后的数据可能会多次写入外部系统。虽然对于 saveAs***Files() 这样的操作,多次保存到文件系统是可以接受的(文件只会被相同的数据覆盖),但是要真正获得精准一次的语义,有两个方法:

① 幂等更新

多次写操作,都是写相同的数据。例如,saveAs***Files() 总是写入相同的数据。

② 事务性更新

所有的更新都是以事务方式进行的,因此更新只会以原子方式进行一次。

    • 使用批处理时间(在 foreachRDD 中可用)和 RDD 的分区索引来创建唯一标识符,用来标识应用程序中的 blob 数据。

    • 使用此标识符以事务方式(即,精准一次,原子方式)更新外部系统。如果尚未提交标识符,则以原子方式提交分区数据和标识符。否则,如果这已经提交,请跳过更新。

dstream.foreachRDD { (rdd, time) =>
  rdd.foreachPartition { partitionIterator =>
    val partitionId = TaskContext.get.partitionId()
    val uniqueId = generateUniqueId(time.milliseconds, partitionId)
    // use this uniqueId to transactionally commit the data in partitionIterator
  }
}

6. 写在最后

Spark Streaming 从 Spark 1.6 开始引入,使用批处理模拟流式计算,它的数据抽象 DStream,基于 RDD 算子,本质上是时间上连续的 RDD

随着 Spark 版本的不断更新迭代,Spark Streaming 已经成为上一代流引擎。官方宣称 Spark Streaming 不会再有更新,成为一个遗留项目(legacy project)。 Spark 2.0 以后推出了一个更新的、更易于使用的流引擎 — Structured Streaming

 

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值