Spark Streaming之基本概念

參考官網: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這樣的依賴,比如:
在這裏插入圖片描述

初始化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:
在這裏插入圖片描述
對DStream應用的任何操作都將 轉換爲 對底層的RDD的操作。比如,在之前的示例中,把數據流中的每一行去轉換爲單詞,flatMap操作符被應用在lines DStream中的每個RDD上去生成words DStream的RDDs。如下圖所示:
對DStream做了某個操作,其實就是對底層的RDD都做了某個操作。
一個DStream = 一系列的 RDD
在這裏插入圖片描述
這些潛在的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一直在運行着。
在這裏插入圖片描述

上面是有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小文件:
在這裏插入圖片描述
那麼問題來了,如果批次有很多,那麼會出現很多很多的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)
  }

在這裏插入圖片描述
①你開發一個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圖:
在這裏插入圖片描述
前面兩個是skipped是因爲在流處理之前,兩個已經準備好了,跟流處理沒關係。後面可以看出,先做join,再做filter,再做map。
但這個性能並不好。可以使用更簡單的方式來處理。

Window Operations(瞭解)

這個瞭解下。
Spark Streaming也提供窗口化計算,這允許你在滑動的數據窗口上 使用transformations算子。下圖說明該滑動窗口。
在這裏插入圖片描述
上圖,假定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指南。

參考:https://www.cnblogs.com/swordfall/p/8378000.html
參考官方案例(代碼寫的很好):https://github.com/apache/spark/tree/master/examples/src/main/scala/org/apache/spark/examples/streaming
或者這個目錄下:$SPARK_HOME/examples/src/main/scala/org/apache/spark/examples/streaming
本篇一些代碼都是參考這裏的寫的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章