spark(三)--spark-streaming---(Dstream圖文詳解,基於IDEA開發)

前言

目錄

Spark Streaming概述

Dstream入門案例及解析

Dstream的創建–各個數據源

Dstream轉換–無狀態和有狀態

無狀態轉化

window版本有狀態轉化

UpdateStateByKey版本

Spark Streaming概述

Spark Streaming是什麼

  • Spark Streaming用於流式數據的處理。Spark Streaming支持的數據輸入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和簡單的TCP套接字等等。數據輸入後可以用Spark的高度抽象原語如:map、reduce、join、window等進行運算。而結果也能保存在很多地方,如HDFS,數據庫等。
    在這裏插入圖片描述
  • 和Spark基於RDD的概念很相似,Spark Streaming使用離散化流(discretized stream)作爲抽象表示,叫作DStream。DStream 是隨時間推移而收到的數據的序列。在內部,每個時間區間收到的數據都作爲 RDD 存在,而DStream是由這些RDD所組成的序列(因此得名“離散化”)。

SparkStreaming架構
在這裏插入圖片描述

Dstream入門案例

1 WordCount案例實操
1.需求:使用netcat工具向9999端口不斷的發送數據,通過SparkStreaming讀取端口數據並統計不同單詞出現的次數
window netcat下載教程
2.添加依賴

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.12</artifactId>
            <version>2.4.5</version>

        </dependency>

3代碼

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

    1.初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")

    2.初始化SparkStreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(2))

    3.通過監控端口創建DStream,讀進來的數據爲一行行
    val lineStreams = ssc.socketTextStream("localhost", 9999)

    將每一行數據做切分,形成一個個單詞
    val wordStreams = lineStreams.flatMap(_.split(" "))

    將單詞映射成元組(word,1)
    val wordAndOneStreams = wordStreams.map((_, 1))

    將相同的單詞次數做統計
    val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)

    println("hello")
    打印
    wordAndCountStreams.print()

    繼續等待,不能讓其停止
    ssc.start()
    ssc.awaitTermination()
  }

4先打開window命令行,再在idea中開啓應用程序,讓二者建立連接
5在命令行中隨意輸入

sasdf aaa aaa
--------------

Time: 1589959686000 ms
-------------------------------------------
(aaa,2)
(sasdf,1)

2 WordCount解析
Discretized Stream是Spark Streaming的基礎抽象,代表持續性的數據流和經過各種Spark原語操作後的結果數據流。在內部實現上,DStream是一系列連續的RDD來表示。每個RDD含有一段時間間隔內的數據,如下圖:
在這裏插入圖片描述

對數據的操作也是按照RDD爲單位來進行的
在這裏插入圖片描述
計算過程由Spark engine來完成在這裏插入圖片描述

Dstream創建

Spark Streaming原生支持一些不同的數據源。一些“核心”數據源已經被打包到Spark Streaming 的 Maven 工件中,而其他的一些則可以通過 spark-streaming-kafka 等附加工件獲取。每個接收器都以 Spark 執行器程序中一個長期運行的任務的形式運行,因此會佔據分配給應用的 CPU 核心。此外,我們還需要有可用的 CPU 核心來處理數據。這意味着如果要運行多個接收器,就必須至少有和接收器數目相同的核心數,還要加上用來完成計算所需要的核心數。例如,如果我們想要在流計算應用中運行 10 個接收器,那麼至少需要爲應用分配 11 個 CPU 核心。所以如果在本地模式運行,不要使用local[1]。

1. hdfs文件系統讀取流式文件

文件數據流:能夠讀取所有HDFS API兼容的文件系統文件,通過fileStream方法進行讀取,Spark Streaming 將會監控 dataDirectory 目錄並不斷處理移動進來的文件,記住目前不支持嵌套目錄。
streamingContext.textFileStream(dataDirectory)

注意事項:
1)文件需要有相同的數據格式;
2)文件進入 dataDirectory的方式需要通過移動或者重命名來實現;
3)一旦文件移動進目錄,則不能再修改,即便修改了也不會讀取新數據;
4) 一般不會這麼做,而是用flume收集數據,瞭解一下就好

  • 在HDFS上建好目錄
hadoop fs -mkdir /fileStream
  • 創建三個文件
 touch a.tsv
 touch b.tsv
 touch c.tsv

添加如下數據:
Hello	
Hello	spark
  • 編寫代碼
  def main(args: Array[String]): Unit = {

    //1.初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")

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

    //3.通過監控端口創建DStream,讀進來的數據爲一行行
    val lineStreams = ssc.textFileStream("hdfs://121.199.16.65:8020/fileSystem")

    //將每一行數據做切分,形成一個個單詞
    val wordStreams = lineStreams.flatMap(_.split("\t"))

    //將單詞映射成元組(word,1)
    val wordAndOneStreams = wordStreams.map((_, 1))

    //將相同的單詞次數做統計
    val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)

    println("hello")
    //打印
    wordAndCountStreams.print()

    //啓動SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
  • 啓動程序並向fileStream目錄上傳文件
hadoop fs -put ./a.tsv /fileStream
 hadoop fs -put ./b.tsv /fileStream
hadoop fs -put ./c.tsv /fileStream
  • 獲取計算結果
-------------------------------------------
Time: 1539073815000 ms
-------------------------------------------
(Hello,4)
(spark,2)


-------------------------------------------
Time: 1539073820000 ms
-------------------------------------------
(Hello,2)
(spark,1)

2.RDD隊列

用法及說明
測試過程中,可以通過使用ssc.queueStream(queueOfRDDs)來創建DStream,每一個推送到這個隊列中的RDD,都會作爲一個DStream處理。

  • 案例實操
    1)需求:循環創建幾個RDD,將RDD放入隊列。通過SparkStream創建Dstream,計算WordCount
  1. 代碼
object RDDStream {

  def main(args: Array[String]) {

    //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 = ssc.queueStream(rddQueue,oneAtATime = false)

    //5.處理隊列中的RDD數據
    val mappedStream = inputStream.map((_,1))
    val reducedStream = mappedStream.reduceByKey(_ + _)

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

    //7.啓動任務
    ssc.start()

//8.循環創建並向RDD隊列中放入RDD
    for (i <- 1 to 5) {
      rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
      Thread.sleep(2000)
    }

    ssc.awaitTermination()
  }
}
-------------------------------------------
Time: 1539075280000 ms
-------------------------------------------
(4,60)
(0,60)
(6,60)
(8,60)
(2,60)

3 Kafka數據源(重點)
1 用法及說明
在工程中需要引入 Maven 工件 spark- streaming-kafka_2.10 來使用它。包內提供的 KafkaUtils 對象可以在 StreamingContext 和 JavaStreamingContext 中以你的 Kafka 消息創建出 DStream。由於 KafkaUtils 可以訂閱多個主題,因此它創建出的 DStream 由成對的主題和消息組成。要創建出一個流數據,需要使用 StreamingContext 實例、一個由逗號隔開的 ZooKeeper 主機列表字符串、消費者組的名字(唯一名字),以及一個從主題到針對這個主題的接收器線程數的映射表來調用 createStream() 方法。
2 案例實操
需求:通過SparkStreaming從Kafka讀取數據,並將讀取過來的數據做簡單計算(WordCount),最終打印到控制檯。
(1)導入依賴

 <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
        <version>2.4.3</version>
    </dependency>
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.11.0.2</version>
</dependency>

(2)
在kafka中創建topic

kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic kafka_spark

生產消息
kafka-console-producer.sh --broker-list localhost:9092 --topic kafka_spark

(3)編寫代碼

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

    val conf = new SparkConf()
      .setAppName("DirectKafka")
      .setMaster("local[2]")

    val ssc = new StreamingContext(conf, Seconds(5))
    val topicsSet = Array("kafka_spark")
    val kafkaParams = mutable.HashMap[String, String]()
    //必須添加以下參數,否則會報錯
    kafkaParams.put("bootstrap.servers", "121.199.16.65:9092")
    kafkaParams.put("group.id", "0")
    kafkaParams.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    kafkaParams.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    val messages = KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topicsSet, kafkaParams
      )
    )

    // Get the lines, split them into words, count the words and print
    val lines = messages.map(_.value)
    val words = lines.flatMap(_.split(" "))
    val wordCounts = words.map(x => (x, 1L)).reduceByKey(_ + _)
    wordCounts.print()

    // Start the computation
    ssc.start()
    ssc.awaitTermination()
  }

(4)
在kafka中輸入消息

aa bb cc aa

Time: 1589984005000 ms
-------------------------------------------
(aa,2)
(bb,1)
(cc,1)

4 自定義數據源

1 用法及說明
需要繼承Receiver,並實現onStart、onStop方法來自定義數據源採集。
3.3.2 案例實操
1)需求:自定義數據源,實現監控某個端口號,獲取該端口號內容。
2)代碼實現

自定義採集器:

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

  最初啓動的時候,調用該方法,作用爲:讀數據並將數據發送給Spark
  override def onStart(): Unit = {
    new Thread("Socket Receiver") {
      override def run() {
        receive()
      }
    }.start()
  }

  讀數據並將數據發送給Spark
  def receive(): Unit = {

    創建一個Socket
    var socket: Socket = new Socket(host, port)

    定義一個變量,用來接收端口傳過來的數據
    var input: String = null

    創建一個BufferedReader用於讀取端口傳來的數據
    val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))

    讀取數據
    input = reader.readLine()

    當receiver沒有關閉並且輸入數據不爲空,則循環發送數據給Spark
    while (!isStopped() && input != null) {
      store(input)
      input = reader.readLine()
    }

    跳出循環則關閉資源
    reader.close()
    socket.close()

    重啓任務
    restart("restart")
  }

  override def onStop(): Unit = {}
}

測試:

def main(args: Array[String]): Unit = {
    //上下文
    val conf=new SparkConf().setAppName("streamWordCount").setMaster("local");
   // var sc=new SparkContext(conf);
    //sc.setLogLevel("ERROR")

    val ssc=new StreamingContext(conf,Seconds(3));

    val lineStream=ssc.receiverStream(new myReceiver("localhost",9999));

    //4.將每一行數據做切分,形成一個個單詞
    val wordStreams = lineStream.flatMap(_.split(" "))

    //5.將單詞映射成元組(word,1)
    val wordAndOneStreams = wordStreams.map((_, 1))

    //6.將相同的單詞次數做統計
    val wordAndCountStreams = wordAndOneStreams.reduceByKey(_ + _)

    //7.打印
    println("hello")
    wordAndCountStreams.print();

    //8.啓動SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()



  }

DStream轉換

1 無狀態轉化操作

無狀態轉化操作就是把簡單的RDD轉化操作應用到每個批次上,也就是轉化DStream中的每一個RDD。
部分無狀態轉化操作列在了下表中。在這裏插入圖片描述
需要記住的是,儘管這些函數看起來像作用在整個流上一樣,但事實上每個DStream在內部是由許多RDD(批次)組成,且無狀態轉化操作是分別應用到每個RDD上的。例如,reduceByKey()會歸約每個時間區間中的數據,但不會歸約不同區間之間的數據。
舉個例子,在之前的wordcount程序中,我們只會統計5秒內接收到的數據的單詞個數,而不會累加。

Window Operations

先來看看什麼叫窗口滑動

 def main(args: Array[String]): Unit = {
    val list=Array(1,2,3,4,5,6);
    val window=list.sliding(2);
    for (r<-window){
      println(r.mkString(";"));
    }
  }

-----------------
1;2
2;3
3;4
4;5
5;6

可以想像,有一個窗口,把兩個數包圍起來,然後一步一步的往後滑動
window Operations

window Operations可以設置窗口的大小和滑動窗口的間隔來動態的獲取當前Steaming的允許狀態。基於窗口的操作會在一個比 StreamingContext 的批次間隔更長的時間範圍內,通過整合多個批次的結果,計算出整個窗口的結果。
在這裏插入圖片描述
上圖可以觀察到,窗口的大小爲三,每次往後滑動兩個步長

  • 注意:所有基於窗口的操作都需要兩個參數,分別爲窗口時長以及滑動步長,兩者都必須是 StreamContext 的批次間隔的整數倍。
  • 窗口時長控制每次計算最近的多少個批次的數據,其實就是最近的 windowDuration/batchInterval 個批次。如果有一個以 10 秒爲批次間隔的源 DStream,要創建一個最近 30 秒的時間窗口(即最近 3 個批次),就應當把 windowDuration 設爲 30 秒。而滑動步長的默認值與批次間隔相等,用來控制對新的 DStream 進行計算的間隔。如果源 DStream 批次間隔爲 10 秒,並且我們只希望每兩個批次計算一次窗口結果, 就應該把滑動步長設置爲 20 秒。
  • 假設,你想拓展前例從而每隔十秒對持續30秒的數據生成word count。爲做到這個,我們需要在持續30秒數據的(word,1)對DStream上應用reduceByKey。使用操作reduceByKeyAndWindow.

WordCount第三版:3秒一個批次,窗口12秒,滑步6秒

  def main(args: Array[String]) {



    val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
    val ssc = new StreamingContext(conf, Seconds(3))
    ssc.checkpoint(".")

    // 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(" "))

    // Count each word in each batch
    val pairs = words.map(word => (word, 1))

    val wordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(12), Seconds(6))

    // Print the first ten elements of each RDD generated in this DStream to the console
    wordCounts.print()

    ssc.start()             // Start the computation
    ssc.awaitTermination()  // Wait for the computation to terminate

  }
-------------------------------------


結果:

-------------------------------------------
Time: 1589966829000 ms
-------------------------------------------
(bbb,1)
(ccc,1)
(aaa,2)

-------------------------------------------
Time: 1589966835000 ms
-------------------------------------------
(ssss,1)
(bbb,2)
(ddd,1)
(lll,1)
(ccc,1)
(aaa,2)

-------------------------------------------
Time: 1589966841000 ms
-------------------------------------------
(ssss,1)
(bbb,2)
(ddd,1)
(lll,1)
(aaa,1)

-------------------------------------------
Time: 1589966847000 ms
-------------------------------------------
(bbb,1)
(aaa,1)

-------------------------------------------
Time: 1589966853000 ms
-------------------------------------------


Process finished with exit code -1

UpdateStateByKey

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

  1. 定義狀態,狀態可以是一個任意的數據類型。
  2. 定義狀態更新函數,用此函數闡明如何使用之前的狀態和來自輸入流的新值對狀態進行更新。
    使用updateStateByKey需要對檢查點目錄進行配置,會使用檢查點來保存狀態。
    更新版的wordcount:
ef main(args: Array[String]) {

     定義更新狀態方法,參數values爲當前批次單詞頻度,state爲以往批次單詞頻度
    val updateFunc = (values: Seq[Int], state: Option[Int]) => {
      val currentCount = values.foldLeft(0)(_ + _)
      val previousCount = state.getOrElse(0)
      Some(currentCount + previousCount)
    }

    val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
    val ssc = new StreamingContext(conf, Seconds(3))
    ssc.checkpoint("hdfs://localhost:9000/streamCheck")

    
    val lines = ssc.socketTextStream("hadoop102", 9999)

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

    //import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
    // Count each word in each batch
    val pairs = words.map(word => (word, 1))


    // 使用updateStateByKey來更新狀態,統計從運行開始以來單詞總的次數
    val stateDstream = pairs.updateStateByKey[Int](updateFunc)
    stateDstream.print()

    ssc.start()             // Start the computation
    ssc.awaitTermination()  // Wait for the computation to terminate

  }

(2)啓動程序並向9999端口發送數據
nc -lk 9999
ni shi shui
ni hao ma

(3)結果展示


-------------------------------------------
Time: 1504685175000 ms
-------------------------------------------
-------------------------------------------
Time: 1504685181000 ms
-------------------------------------------
(shi,1)
(shui,1)
(ni,1)
-------------------------------------------
Time: 1504685187000 ms
-------------------------------------------
(shi,1)
(ma,1)
(hao,1)
(shui,1)
(ni,2)

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