Cris 的 Spark Streaming 筆記

Cris就是我發表於2019-01-01

一、Spark Streaming 概述

1.1 Spark Streaming是什麼

Spark Streaming用於流式資料的處理。Spark Streaming支援的資料輸入源很多,例如:KafkaFlumeTwitterZeroMQ和簡單的TCP套接字等等。

資料輸入後可以用Spark的高度抽象原語如:mapreducejoinwindow等進行運算。而結果也能儲存在很多地方,如HDFS,資料庫等。

Cris 的 Spark Streaming 筆記

Spark基於RDD的概念很相似,Spark Streaming使用離散化流(discretized stream)作為抽象表示,叫作DStream

DStream 是隨時間推移而收到的資料的序列。在內部,每個時間區間收到的資料都作為 RDD 存在,而DStream是由這些RDD所組成的序列(因此得名“離散化”),可以理解為 DStream 是對多個 RDD 的再封裝。

1.2 Spark Streaming特點

  1. 易用

Cris 的 Spark Streaming 筆記

  1. 容錯

Cris 的 Spark Streaming 筆記

  1. 易整合到Spark體系

Cris 的 Spark Streaming 筆記

1.3 Spark Streaming架構

1. 架構圖

整體架構圖

Cris 的 Spark Streaming 筆記

架構實現圖

Cris 的 Spark Streaming 筆記

2. 背壓機制

Spark 1.5以前版本,使用者如果要限制Receiver的資料接收速率,可以通過設定靜態配製引數“spark.streaming.receiver.maxRate”的值來實現,此舉雖然可以通過限制接收速率,來適配當前的處理能力,防止記憶體溢位,但也會引入其它問題。比如:producer資料生產高於maxRate,當前叢集處理能力也高於maxRate,這就會造成資源利用率下降等問題。

為了更好的協調資料接收速率與資源處理能力,1.5版本開始Spark Streaming可以動態控制資料接收速率來適配叢集資料處理能力。背壓機制(即Spark Streaming Backpressure): 根據JobScheduler反饋作業的執行資訊來動態調整Receiver資料接收率。

Cris 的 Spark Streaming 筆記

通過屬性“spark.streaming.backpressure.enabled”來控制是否啟用backpressure機制,預設值false,即不啟用。

二、Dstream 入門

2.1 WordCount 案例實操

需求:使用netcat工具向9999埠不斷的傳送資料,通過Spark Streaming讀取埠資料並統計不同單詞出現的次數

1. 安裝 netcat 工具

netcat(nc)是一個簡單而有用的工具,不僅可以通過使用TCPUDP協議的網路連線讀寫資料,同時還是一個功能強大的網路除錯和探測工具,能夠建立你需要的幾乎所有型別的網路連線。

Linux終端視窗可以直接使用yum工具進行安裝:

Cris 的 Spark Streaming 筆記

然後測試,開啟一個埠輸入資料

Cris 的 Spark Streaming 筆記

另起一個 Terminal 接受資料

Cris 的 Spark Streaming 筆記

測試 ok~

2. 編寫 WC 案例

新建一個模組 Spark Streaming,新增依賴

    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
複製程式碼

編寫程式碼如下

/**
  * 一個簡單的使用 Spark Streaming 統計埠傳送資料的 WC 程式
  *
  * @author cris
  * @version 1.0
  **/
object Main {
  def main(args: Array[String]): Unit = {
    // 1. 初始化 SparkConf 物件
    val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")

    // 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
    val context = new StreamingContext(conf, Seconds(3))

    // 3. 通過監控埠建立DStream,讀進來的資料為一行行
    val receiver: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
    // 將單詞分割,並統計結果
    val dStream: DStream[String] = receiver.flatMap(_.split(" "))
    val dStream2: DStream[(String, Int)] = dStream.map((_, 1))
    val dStream3: DStream[(String, Int)] = dStream2.reduceByKey(_ + _)

    // 將結果列印
    dStream3.print()

    // 4. 啟動 Spark Streaming 程式
    context.start()
    context.awaitTermination()
  }
}
複製程式碼

注意:如果程式執行時,log日誌太多,可以將日誌級別改成 ERROR

Cris 的 Spark Streaming 筆記

log4j.rootLogger=ERROR, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
複製程式碼

然後啟動 9999

[cris@hadoop101 ~]$ nc -lk 9999 
複製程式碼

在啟動 IDEA 中的 Main 程式,此時控制檯如下

Cris 的 Spark Streaming 筆記

如果往 9999 埠輸入資料

Cris 的 Spark Streaming 筆記

Cris 的 Spark Streaming 筆記

2.2 WordCount 案例解析

Discretized StreamSpark Streaming的基礎抽象,代表持續性的資料流和經過各種Spark 運算元操作後的結果資料流。在內部實現上,DStream是一系列連續的RDD來表示。每個RDD含有一段時間間隔內的資料,如下圖

Cris 的 Spark Streaming 筆記

三、Dstream 建立

3.1 RDD 佇列

1. 用法及說明

測試過程中,可以通過使用 ssc.queueStream(queueOfRDDs) 來建立DStream,每一個推送到這個佇列中的RDD,都會作為一個DStream處理。

2. 案例實操

需求:迴圈建立幾個RDD,將RDD放入佇列。通過SparkStream建立Dstream,計算WordCount

程式碼如下:

object Main2 {
  def main(args: Array[String]): Unit = {
    //1.初始化Spark配置資訊
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")

    //2.初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(4))

    //3.建立RDD佇列
    val rddQueue = new mutable.Queue[RDD[Int]]()

    //4.建立QueueInputDStream
    val inputStream: InputDStream[Int] = ssc.queueStream(rddQueue, oneAtATime = false)

    //5.處理佇列中的RDD資料
    val mappedStream: DStream[(Int, Int)] = inputStream.map((_, 1))
    val reducedStream: DStream[(Int, Int)] = mappedStream.reduceByKey(_ + _)

    //6.列印結果
    reducedStream.print()

    ssc.start()

    //7.迴圈建立並向RDD佇列中放入RDD
    for (i <- 1 to 5) {
      rddQueue += ssc.sparkContext.makeRDD(1 to 5, 10)
      Thread.sleep(2000)
    }

    ssc.awaitTermination()
  }
}
複製程式碼

結果展示

Cris 的 Spark Streaming 筆記

3.2 自定義資料來源

需要繼承Receiver,並實現onStartonStop方法來自定義資料來源採集。

實質上就是自定義 Spark Streaming 的資料接受器

程式碼如下:

class CustomerReceiver(hostName: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {

  // 開啟資料接收器
  override def onStart(): Unit = {
    new Thread(new Runnable {
      // 開啟一個執行緒執行資料接受的方法
      override def run(): Unit = receive()
    }).start()
  }

  def receive(): Unit = {

    var socket: Socket = null
    var reader: BufferedReader = null

    try {
      socket = new Socket(hostName, port)
      reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
      var str: String = reader.readLine()
      while (str != null) {
        // 儲存資料到 Spark Streaming
        store(str)
        str = reader.readLine()
      }
    } catch {
      case e: Exception => {
        reader.close()
        socket.close()
        println("獲取資料失敗,請除錯!")
      }
    }
  }
  override def onStop(): Unit = {}
}

object Main3 {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("customer receiver").setMaster("local[*]")
    val streamingContext = new StreamingContext(conf, Seconds(2))

    // 從自定義的接收器去接收資料
    val dStream: ReceiverInputDStream[String] = streamingContext.receiverStream(new CustomerReceiver("hadoop101", 9999))
    dStream.print()

    streamingContext.start()
    streamingContext.awaitTermination()
  }
}
複製程式碼

核心就是 Receiver 這個類,以及 onStartstore 核心方法

3.3 Kafka資料來源(開發重點)

1. 用法及說明

在工程中需要引入Maven工件spark-streaming-kafka-0-8_2.11來使用它。包內提供的 KafkaUtils物件可以在 StreamingContextJavaStreamingContext中以你的Kafka訊息建立出 DStream

兩個核心類:KafkaUtilsKafkaCluster

需求:通過SparkStreamingKafka讀取資料,並將讀取過來的資料做簡單計算(WordCount),最終列印到控制檯。

2. 程式碼完成如下

首先匯入依賴

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
            <version>2.1.1</version>
        </dependency>
複製程式碼

然後使用 Kafka 的低階 API 手動完成offset 的獲取和儲存

  • 首先完成 Spark StreamingKafka 的對接程式
  def main(args: Array[String]): Unit = {
    //1.初始化Spark配置資訊
    val conf = new SparkConf().setMaster("local[*]").setAppName("KafkaStream")

    //2.初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(4))

    //3. kafka引數宣告
    val brokers = "hadoop101:9092,hadoop102:9092,hadoop103:9092"
    val topic = "first"
    val group = "cris"
    val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
    val kafkaPropsMap: Map[String, String] = Map(
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
      ConsumerConfig.GROUP_ID_CONFIG -> group,
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization)

    //4. 獲取 KafkaCluster 物件
    val kafkaCluster = new KafkaCluster(kafkaPropsMap)

    //5. 獲取上一次讀取結束後的 offset
    val fromOffset: Map[TopicAndPartition, Long] = getOffset(topic, group, kafkaCluster).toMap

    //6. 讀取 Kafka 資料為 DStream 物件
    val kafkaStream: InputDStream[String] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder,
      String](ssc,
      kafkaPropsMap,
      fromOffset,
      (message: MessageAndMetadata[String, String]) => message.message())

    //7. DStream 資料處理
    kafkaStream.map((_, 1)).reduceByKey(_ + _).print()

    //8. 儲存 offset
    saveOffsets(kafkaCluster, kafkaStream, group)

    //9. 開啟 Spark Streaming 程式
    ssc.start()
    ssc.awaitTermination()
  }
複製程式碼
  • 然後是手動維護 offset 的兩個方法(讀取和更新)
  /**
    * 獲取消費者組上一次資料消費的 offset 位置
    *
    * @param topic        主題
    * @param group        消費者組
    * @param kafkaCluster Kafka 叢集抽象
    */
  def getOffset(topic: String, group: String, kafkaCluster: KafkaCluster): mutable.HashMap[TopicAndPartition, Long] = {

    // 定義一個存放主題分割槽 offset 資訊的 map
    val topicAndPartitionToLong = new mutable.HashMap[TopicAndPartition, Long]()

    // 獲取主題的分割槽資訊
    val partionsInfo: Either[Err, Set[TopicAndPartition]] = kafkaCluster.getPartitions(Set(topic))

    // 如果主題分割槽資訊有資料
    if (partionsInfo.isRight) {
      // 取出主題分割槽資訊物件
      val infos: Set[TopicAndPartition] = partionsInfo.right.get

      // 獲取該消費者組消費 topic 分割槽資料的 offset 資訊
      val offsetInfo: Either[Err, Map[TopicAndPartition, Long]] = kafkaCluster.getConsumerOffsets(group, infos)

      // 如果 offset 資訊有該消費者組消費 topic 分割槽資料的 offset 資料
      if (offsetInfo.isRight) {
        val offsets: Map[TopicAndPartition, Long] = offsetInfo.right.get
        for (offset <- offsets) {
          topicAndPartitionToLong += offset
        }
      } else {
        // 手動初始化該消費者組消費該主題分割槽資料 offset 資訊
        for (topicAndPartition <- infos) {
          topicAndPartitionToLong += (topicAndPartition -> 0)
        }
      }
    }
    topicAndPartitionToLong
  }

  /**
    * 每批次 Spark Streaming 消費資訊完畢都要進行 offset 的更新
    *
    * @param kafkaCluster Kafka 叢集抽象
    * @param kafkaStream  Spark Streaming 消費 Kafka 資料的抽象
    * @param group
    */
  def saveOffsets(kafkaCluster: KafkaCluster, kafkaStream: InputDStream[String], group: String): Unit = {

    // 將 KafkaStream 物件中的每個 rdd 物件中的 offset 取出來
    kafkaStream.foreachRDD(rdd => {
      // 從 rdd 中取出 offsets
      val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges

      // 遍歷每個 rdd 得到的所有分割槽資料的 offsets
      for (offset <- ranges) {
        val untilOffset: Long = offset.untilOffset
        /// 儲存 offset
        val result: Either[Err, Map[TopicAndPartition, Short]] = kafkaCluster.setConsumerOffsets(group, Map(offset.topicAndPartition() -> untilOffset))
        if (result.isLeft) {
          println(s"${result.left.get}")
        } else {
          println(s"${result.right.get} + $untilOffset")
        }
      }
    })
      
  }
複製程式碼

最後測試如下

啟動 Kafka 的生產者

kafka_producer_topic first
# 實質上是 kafka-console-producer.sh --broker-list hadoop101:9092 --topic first
# Cris 這裡使用了別名代替繁瑣的編寫
複製程式碼

然後啟動 IDEA 程式

Cris 的 Spark Streaming 筆記

生產資料

Cris 的 Spark Streaming 筆記

IDEA 控制檯列印資訊

Cris 的 Spark Streaming 筆記

證明 Spark StreamingKafka 對接成功~

四、DStream 轉換(重點)

DStream上的操作與RDD的類似,分為Transformations(轉換)和Output Operations(輸出)兩種,此外轉換操作中還有一些比較特殊的原語,如:updateStateByKeytransform以及各種Window相關的原語。

4.1 無狀態轉化操作

無狀態轉化操作就是把簡單的RDD轉化操作應用到每個批次上,也就是轉化DStream中的每一個RDD。部分無狀態轉化操作列在了下表中。注意,針對鍵值對的DStream轉化操作(比如 reduceByKey要新增import StreamingContext._才能在Scala中使用。

Cris 的 Spark Streaming 筆記

需要記住的是,儘管這些函式看起來像作用在整個流上一樣,但事實上每個DStream在內部是由許多RDD(批次)組成,且無狀態轉化操作是分別應用到每個RDD上的。例如,reduceByKey會歸約每個時間區間中的資料,但不會歸約不同時間區間之間的資料。

Transform

Transform允許DStream上執行任意的RDD-to-RDD函式。即使這些函式並沒有在DStreamAPI中暴露出來,通過該函式可以方便的擴充套件Spark API。該函式每一批次排程一次。其實也就是對DStream中的RDD應用轉換。

比如之前使用 DStream 完成 WC 案例,我們可以對 DStream 中的每個 RDD 執行 WC 操作,通過 transform 運算元

/**
  * 將 DStream 通過 transform 運算元轉換為一系列的 RDD 進行操作
  *
  * @author cris
  * @version 1.0
  **/
object Main4 {
  def main(args: Array[String]): Unit = {
    // 1. 初始化 SparkConf 物件
    val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")

    // 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
    val context = new StreamingContext(conf, Seconds(3))

    // 3. 通過監控埠建立DStream,讀進來的資料為一行行
    val Dstream: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
    val result: DStream[(String, Int)] = Dstream.transform(rdd => {
      val words: RDD[String] = rdd.flatMap(_.split(" "))
      val wordsToTuple: RDD[(String, Int)] = words.map((_, 1))
      val wordsCount: RDD[(String, Int)] = wordsToTuple.reduceByKey(_ + _)
      wordsCount
    })

    result.print()

    context.start()
    context.awaitTermination()
  }
}
複製程式碼

4.2 有狀態轉換操作

UpdateStateByKey

UpdateStateByKey運算元用於記錄歷史記錄,有時,我們需要在DStream中跨批次維護狀態(例如流計算中累加wordcount)。針對這種情況,updateStateByKey為我們提供了對一個狀態變數的訪問,用於鍵值對形式的DStream。給定一個由(鍵,事件)對構成的 DStream,並傳遞一個指定如何根據新的事件更新每個鍵對應狀態的函式,它可以構建出一個新的 DStream,其內部資料為(鍵,狀態) 對。

updateStateByKey的結果會是一個新的DStream,其內部的RDD 序列是由每個時間區間對應的(鍵,狀態)對組成的。

updateStateByKey操作使得我們可以在用新資訊進行更新時保持任意的狀態。為使用這個功能,需要做下面兩步:

  1. 定義狀態,狀態可以是一個任意的資料型別。

  2. 定義狀態更新函式,用此函式闡明如何使用之前的狀態和來自輸入流的新值對狀態進行更新。

使用updateStateByKey需要對檢查點目錄進行配置,會使用檢查點來儲存狀態。

更新版的wordcount

/**
  * 通過儲存上一批次的計算結果和當前批次計算結果整合完成資料狀態的更新
  *
  * @author cris
  * @version 1.0
  **/
object StatusWC {
  def main(args: Array[String]): Unit = {
    // 1. 初始化 SparkConf 物件
    val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")

    // 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
    val context = new StreamingContext(conf, Seconds(3))
    // 2.1 需要設定 CheckPoint 來儲存每批次計算的狀態,以便於和下一批次計算的結果做整合
    context.sparkContext.setCheckpointDir("./checkpoint")

    // 3. 使用 DStream 完成 WC
    val words: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
    val wordsSeparated: DStream[String] = words.flatMap(_.split(" "))
    val wordsTuple: DStream[(String, Int)] = wordsSeparated.map((_, 1))

    // 3.1 定義每批次計算結果和上批次計算結果的整合函式
    val updateStateFunc: (Seq[Int], Option[Int]) => Option[Int] = (values: Seq[Int], state: Option[Int]) => {
      val sum: Int = values.sum
      val result: Int = state.getOrElse(0) + sum
      Some(result)
    }

    // 4. 批次計算並列印
    val result: DStream[(String, Int)] = wordsTuple.updateStateByKey(updateStateFunc)
    result.print()

    context.start()
    context.awaitTermination()
  }
}
複製程式碼

測試結果

Cris 的 Spark Streaming 筆記

Cris 的 Spark Streaming 筆記

總結:所謂的有狀態轉換就是通過儲存上一批次計算結果,然後和下一批次計算結果整合得到新的計算結果,依次類推~

Window Operations

Window Operations可以設定視窗的大小和滑動視窗的間隔來動態的獲取當前Steaming的允許狀態。所有基於視窗的操作都需要兩個引數,分別為視窗時長以及滑動步長。

(1)視窗時長:計算內容的時間範圍;

(2)隔多久觸發一次計算。

注意:這兩者都必須為批次大小的整數倍。

如下圖所示WordCount案例:視窗大小為計算批次的2倍,滑動步等於批次大小。

Cris 的 Spark Streaming 筆記

程式碼如下:

/**
  * 使用視窗函式,根據步長(計算間隔)來統計視窗長度(計算批次個數)的資料
  *
  * @author cris
  * @version 1.0
  **/
object WindowWC {
  def main(args: Array[String]): Unit = {
    // 1. 初始化 SparkConf 物件
    val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")

    // 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
    val context = new StreamingContext(conf, Seconds(3))

    // 3. 使用 DStream 完成 WC
    val words: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)

    val wordsTuple: DStream[(String, Int)] = words.flatMap(_.split(" ")).map((_, 1))

    // 使用視窗函式,每經過 3 秒就計算當前時刻前 6 秒的所有資料
    val result: DStream[(String, Int)] = wordsTuple.reduceByKeyAndWindow((x: Int, y: Int) => x + y, Seconds(6), Seconds(3))
    result.print()

    context.start()
    context.awaitTermination()
  }
}
複製程式碼

測試結果如下

Cris 的 Spark Streaming 筆記

關於Window的操作還有如下方法:

(1)window(windowLength, slideInterval): 基於對源DStream窗化的批次進行計算返回一個新的Dstream

(2)countByWindow(windowLength, slideInterval): 返回一個滑動視窗計數流中的元素個數;

(3)reduceByWindow(func, windowLength, slideInterval): 通過使用自定義函式整合滑動區間流元素來建立一個新的單元素流;

(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]): 當在一個(K,V)對的DStream上呼叫此函式,會返回一個新(K,V)對的DStream,此處通過對滑動視窗中批次資料使用reduce函式來整合每個keyvalue值。

(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]): 這個函式是上述函式的更高效版本,每個視窗的reduce值都是通過用前一個窗的reduce值來遞增計算。通過reduce進入到滑動視窗資料並”反向reduce”離開視窗的舊資料來實現這個操作。

一個例子是隨著視窗滑動對keys的“加”“減”計數。通過前邊介紹可以想到,這個函式只適用於”可逆的reduce函式”,也就是這些reduce函式有相應的”反reduce”函式(以引數invFunc形式傳入)。如前述函式,reduce任務的數量通過可選引數來配置。(這個方法可以用於視窗函式的優化)

五、DStream 輸出

輸出操作指定了對流資料經轉化操作得到的資料所要執行的操作(例如把結果推入外部資料庫或輸出到螢幕上)

RDD中的惰性求值類似,如果一個DStream及其派生出的DStream都沒有被執行輸出操作,那麼這些DStream就都不會被求值。如果StreamingContext中沒有設定輸出操作,整個context就都不會啟動。

輸出操作如下:

(1)print():在執行流程式的驅動結點上列印DStream每一批次資料的最開始 10 個元素。這用於開發和除錯。在Python API 中,同樣的操作叫print

(2)saveAsTextFiles(prefix, [suffix]):以text檔案形式儲存這個DStream的內容。每一批次的儲存檔名基於引數中的prefixsuffix。”prefix-Time_IN_MS[.suffix]”。

(3)saveAsObjectFiles(prefix, [suffix]):以Java物件序列化的方式將Stream中的資料儲存為 SequenceFiles . 每一批次的儲存檔名基於引數中的為"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。

(4)saveAsHadoopFiles(prefix, [suffix]):將Stream中的資料儲存為 Hadoop files. 每一批次的儲存檔名基於引數中的為"prefix-TIME_IN_MS[.suffix]"。Python API 中目前不可用。

(5)foreachRDD(func):這是最通用的輸出操作,即將函式 func 用於產生於 stream的每一個RDD。其中引數傳入的函式func應該實現將每一個RDD中資料推送到外部系統,如將RDD存入檔案或者通過網路將其寫入資料庫。

通用的輸出操作foreachRDD,它用來對DStream中的RDD執行任意計算。這和transform 有些類似,都可以讓我們訪問任意RDD。在foreachRDD()中,可以重用我們在Spark中實現的所有行動操作。比如,常見的用例之一是把資料寫到諸如MySQL的外部資料庫中。

注意:

(1)連線不能寫在driver層面(序列化);

(2)如果寫在 RDDforeach方法則每個RDD都建立,得不償失;

(3)增加foreachPartition,在分割槽建立。

相關文章