sparkStreaming

Spark Streaming(流處理)

什麼是流處理?

一般流式計算會與批量計算相比較。在流式計算模型中,輸入是持續的,可以認爲在時間上是無界的,也就意味着,永遠拿不到全量數據去做計算。同時,計算結果是持續輸出的,也即計算結果在時間上也是無界的。流式計算一般對實時性要求較高,同時一般是先定義目標計算,然後數據到來之後將計算邏輯應用於數據。同時爲了提高計算效率,往往儘可能採用增量計算代替全量計算。批量處理模型中,一般先有全量數據集,然後定義計算邏輯,並將計算應用於全量數據。特點是全量計算,並且計算結果一次性全量輸出。

在這裏插入圖片描述

Spark Streaming是核心Spark API的擴展,可實現實時數據流的可擴展,高吞吐量,容錯流處理。數據可以從許多來源(如Kafka,Flume,Kinesis或TCP套接字)中獲取,並且可以使用以高級函數(如map,reduce,join和window)表示的複雜算法進行處理。最後,處理後的數據可以推送到文件系統,數據庫和實時dashboards。
在這裏插入圖片描述
在內部,它的工作原理如下。 Spark Streaming接收實時輸入數據流並將數據分成批處理,然後由Spark引擎處理以批量生成最終結果流。
在這裏插入圖片描述

Spark Streaming提供稱爲離散流或DStream的高級抽象,表示連續的數據流。DStream可以從來自Kafka,Flume和Kinesis等源的輸入數據流創建,也可以通過在其他DStream上應用高級操作來創建。在內部DStream表示爲一系列RDD。

備註:Spark Streaming 因爲底層使用批處理模擬流處理,因此在實時性上大打折扣,這就導致了Spark Streaming在流處理領域有者着先天的劣勢。雖然Spark Streaming 在實時性上不如一些專業的流處理引擎(Storm/Flink)但是Spark Stream在使用吸取RDD設計經驗,提供了比較友好的API算子,使得使用RDD做批處理的程序員可以平滑的過渡到流處理。

針對於Spark Streaming的微觀的批處理問題,目前大數據處理領域又誕生了新秀Flink,該大數據處理引擎,在API易用性上和實時性上都有一定的兼顧,但是與spark最大的差異是Flink底層的處理引擎是流處理引擎,因此Flink天生就是流處理,但是Spark因爲底層是批處理,導致了Spark Streaming在實時性上就沒法和其他的專業流處理框架對比了。

快速入門

  • pom.xml
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-core_2.11</artifactId>
    <version>2.4.3</version>
</dependency>
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.4.3</version>
</dependency>
  • SparkStreamWordCounts
//本地測試
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
    val ssc = new StreamingContext(conf, Seconds(5))//Seconds(5)底層微批的時間間隔5s
    ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印

    ssc.socketTextStream("CentOS",9999)//從外部netCat獲得流數據
      .flatMap(_.split(" "))
      .map((_,1))
      .reduceByKey(_+_)
      .print()

    ssc.start()
    ssc.awaitTermination()//等待系統發送指令關閉流計算

需要注意:[root@CentOS ~]# yum install -y nc啓動nc服務[root@CentOS ~]# nc -lk 9999,注意在調用改程序的時候,需要設置local[n],n>1

概念介紹

通過上述案例的運行,現在我們來一起探討一些流處理的概念。在處理流計算的時候,除去spark-core依賴以外我們還需要引入spark-streaming模塊。要從Spark Streaming核心API中不存在的Kafka,Flume和Kinesis等源中提取數據,您必須將相應的工件spark-streaming-xyz_2.11添加到依賴項中。例如Kafka

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

初始化 StreamingContext

要初始化Spark Streaming程序,必須創建一個StreamingContext對象,它是所有Spark Streaming功能的主要入口點。

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參數是應用程序在集羣UI上顯示的名稱。 master是Spark,YARN羣集URL,或者是在本地模式下運行的特殊"local []"字符串。實際上,在羣集上運行時,您不希望在程序中對master進行硬編碼,而是使用spark-submit啓動指定–master配置。但是,對於本地測試和單元測試,您可以傳遞“local []”以在進程中運行Spark Streaming(系統會自動檢測本地系統的核的數目)。

請注意ssc會在內部創建一個SparkContext(所有Spark功能的起點),如果需要獲取SparkContext對象用戶可以調用ssc.sparkContext訪問。例如用戶使用SparkContext關閉日誌。

val conf = new SparkConf()
	.setMaster("local[5]")
	.setAppName("wordCount")
val ssc = new StreamingContext(conf,Seconds(1))
//關閉其他日誌
ssc.sparkContext.setLogLevel("FATAL")

必須根據應用程序的延遲要求和可用的羣集資源設置批處理間隔。要使羣集上運行的Spark Streaming應用程序保持穩定,系統應該能夠以接收數據的速度處理數據。換句話說,批處理數據應該在生成時儘快處理。通過監視流式Web UI中的處理時間可以找到是否適用於應用程序,其中批處理時間應小於批處理間隔。

val conf = new SparkConf()
.setMaster("local[5]")
.setAppName("wordCount")
val sc = new SparkContext(conf)
val ssc = new StreamingContext(sc,Seconds(1))

當用戶創建完StreamingContext對象之後,用戶需要完成以下步驟

  • 定義數據源,用於創建輸入的 DStreams.
  • 定義流計算算子,通過定義這些算子實現對DStream數據轉換和輸出
  • 調用streamingContext.start()啓動數據.
  • 等待計算結束 (人工結束或者是錯誤) 調用 streamingContext.awaitTermination().
  • 如果是人工結束,程序應當調用 streamingContext.stop()結束流計算.

重要因素需要謹記

  • 一旦流計算啓動,無法再往計算流程中添加計算算子
  • 一旦SparkContext對象被stop後,無法重啓。
  • 一個JVM系統中只能實例化一個StreamingContext對象。
  • SparkContext被stop()後,內部創建的SparkContext也會被stop.如果僅僅是想Stop StreamingContext, 可以設置stop() 中的可選參數 stopSparkContext=false即可.
ssc.stop(stopSparkContext = false)
  • 一個SparkContext 可以重複使用並且創建多個StreamingContexts, 前提是上一個啓動的StreamingContext 被停止了(但是並沒有關閉 SparkContext對象) 。

Discretized Streams (DStreams)

Discretized Stream或DStream是Spark Streaming提供的基本抽象。它表示連續的數據流,可以是從源接收的輸入數據流,也可以是通過轉換輸入流生成的已處理數據流。在內部,DStream由一系列連續的RDD表示,這是Spark對不可變分佈式數據集的抽象。DStream中的每個RDD都包含來自特定時間間隔的數據,如下圖所示。

在這裏插入圖片描述

應用於DStream的任何操作都轉換爲底層RDD上的操作。例如,在先前Quick Start示例中,flatMap操作應用於行DStream中的每個RDD以生成單詞DStream的RDD。如下圖所示。

在這裏插入圖片描述

這些底層RDD轉換由Spark引擎計算。 DStream操作隱藏了大部分細節,併爲開發人員提供了更高級別的API以方便使用。

InputStream & Receivers

Input DStream 表示流計算的輸入,Spark中默認提供了兩類的InputStream:

  • Baisc Source :例如 filesystem、scoket
  • Advance Source:例如:Kafka、Flume等外圍系統的數據。

filesystem以外,其他的Input DStream默認都會佔用一個Core(計算資源),在測試或者生產環境下,分配給計算應用的Core數目必須大於Receivers個數。(本質上除filesystem源以外,其他的輸入都是Receiver抽象類的實現。)了例如socketTextStream底層封裝了SocketReceiver

Basic Sources

因爲在快速入門案例中已經使用了socketTextStream,後續我們只測試一下filesystem對於從與HDFS API兼容的任何文件系統(即HDFS,S3,NFS等)上的文件讀取數據,可以通過StreamingContext.fileStream [KeyClass,ValueClass,InputFormatClass]創建DStream。文件流不需要運行Receiver,因此不需要爲接收文件數據分配任何core。對於簡單的文本文件,最簡單的方法是StreamingContext.textFileStream(dataDirectory)

 val conf = new SparkConf().setMaster("local[2]").setAppName("FileSystemWordCount")
    val ssc = new StreamingContext(conf, Seconds(5))
    ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印

    val lines = ssc.textFileStream("hdfs://CentOS:9000/demo/words")

    lines.flatMap(_.split(" "))
      .map((_,1))
      .reduceByKey(_+_)
      .print()

    ssc.start()
    ssc.awaitTermination()

在HDFS上創建目錄

[root@CentOS ~]# hdfs dfs -mkdir -p /demo/words
[root@CentOS ~]# hdfs dfs -put install.log /demo/words

Queue of RDDs as a Stream(測試)

爲了使用測試數據測試Spark Streaming應用程序,還可以使用streamingContext.queueStream(queueOfRDDs)基於RDD隊列創建DStream。推入隊列的每個RDD將被視爲DStream中的一批數據,並像流一樣處理。

val conf = new SparkConf().setMaster("local[2]").setAppName("FileSystemWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印

val queue=new mutable.Queue[RDD[String]]();
val lines = ssc.queueStream(queue)

lines.flatMap(_.split(" "))
.map((_,1))
.reduceByKey(_+_)
.print()

ssc.start()
for(i <- 1 to 30){
    queue += ssc.sparkContext.makeRDD(List("this is a demo","hello how are you"))
    Thread.sleep(1000)
}
ssc.stop()

Advance Source Kafka

  • pom.xml
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
    <version>2.4.3</version>
</dependency>
  • Kafka對接Spark Streaming
val conf = new SparkConf().setMaster("local[2]").setAppName("FileSystemWordCount")
    val ssc = new StreamingContext(conf, Seconds(1))
    ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "CentOS:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "group1",
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )

    KafkaUtils.createDirectStream(ssc,
      LocationStrategies.PreferConsistent,//設置加載數據位置策略,
      Subscribe[String,String](Array("topic01"),kafkaParams))
        .map(record => record.value())
        .flatMap(_.split(" "))
        .map((_,1))
        .reduceByKey(_+_)
        .print()

    ssc.start()
    ssc.awaitTermination()
    //注:Subscribe導包注意import org.apache.spark.streaming.kafka010.ConsumerStrategies._

Spark Stream 算子

  • transform(func)

改算子可以將DStream的數據轉變成RDD,用戶操作流數據就像操作RDD感覺是一樣的。

object SparkKafkaWordCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SparkKafkaWordCount")
    val ssc = new StreamingContext(conf,Seconds(1))
    ssc.sparkContext.setLogLevel("FATAL")
    //添加Kafka連接信息
    val kafkaParams=Map[String,Object](
      "bootstrap.servers"->"CentOSX:9092",
      "key.deserializer"->classOf[StringDeserializer],
      "value.deserializer"->classOf[StringDeserializer],
      "group.id"->"group1",
      "enable.auto.commit"->(false:java.lang.Boolean)
    )
    //模擬從數據庫中讀取的靜態RDD
    val cacheRDD= ssc.sparkContext.makeRDD(List("001 zhangsan", "002 lisi", "003 wangwu"))
      //使用map將文本數據進行切分,得到RDD[(String,String)]形式
      .map(item => (item.split("\\s+")(0), item.split("\\s+")(1)))
      .distinct()//去除重複
      .cache()//添加緩存,節省重複讀取佔用資源

    KafkaUtils.createDirectStream(
      ssc,
      LocationStrategies.PreferConsistent,
      Subscribe[String,String](Array("topic01"),kafkaParams))
      .map(_.value())//根據topic01中的record,獲得record.value
      //對於來自kafka中的record.value進行切分
      .map(value=>{
          val tokens = value.split("\\s+")
          (tokens(0),tokens(1))})
      //將靜態RDD與動態RDD進行join輸出-右連接
      .transform(rdd=>rdd.rightOuterJoin(cacheRDD))
      //因爲靜態RDD始終存在,因此在使用右連接後程序始終打印輸出,此時需要加上過濾濾掉如(001,(null,zhangsan))的結果
      .filter(_._2._1!=None)
      .print()
    ssc.start()
    ssc.awaitTermination()
  }
}
  • UpdateStateByKey

object SparkKafkaWordCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SparkKafkaWordCount")
    val ssc = new StreamingContext(conf,Seconds(1))
    ssc.sparkContext.setLogLevel("FATAL")
    //將狀態信息存儲在hdfs的/checkpoints目錄下(自動創建)
    ssc.checkpoint("hdfs://CentOSX:9000/checkpoints")
    //定義函數updateFun,作爲updateStateBykey狀態算子的參數
    def updateFun(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
      //newValues.sum獲得增量值的和,runningCount.getOrElse(0)獲得歷史數據,不存在則爲0
      var total= newValues.sum+runningCount.getOrElse(0)
      Some(total)
    }
    ssc.socketTextStream("CentOSX",9999)
        .flatMap(_.split("\\s+"))
        .map((_,1))
        .updateStateByKey(updateFun)
        .print()
    ssc.start()
    ssc.awaitTermination()
  }
}

因爲UpdateStateByKey 算子每一次的輸出都是全量輸出,在做狀態更新的時候代價較高,因此推薦大家使用mapWithState

  • mapWithState
object SparkKafkaWordCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SparkKafkaWordCount")
    val ssc = new StreamingContext(conf,Seconds(5))
    ssc.sparkContext.setLogLevel("FATAL")
    //將狀態信息存儲在hdfs的/checkpoints目錄下(自動創建)
    ssc.checkpoint("hdfs://CentOSX:9000/checkpoints")

    ssc.socketTextStream("CentOSX",9999)
        .flatMap(_.split("\\s+"))
        .map((_,1))
        .mapWithState(StateSpec.function((k:String,v:Option[Int],state:State[Int])=>{
            var total=0
            //判斷存儲狀態的state是否存在
            if(state.exists()){
              //如果存在,獲得state存儲的值賦值給total
              total=state.getOption().getOrElse(0)
            }
              //得到歷史值之後的total加上增量值
              total+=v.getOrElse(1)//v.getOrElse不存在則增量爲1
              //根據total修改state
              state.update(total)
              //輸出(k,出現次數)
              (k,total)
        }))
        //設置狀態持久化的頻率,改頻率不能高於 微批 拆分頻率 ts>=5s
        .checkpoint(Seconds(5))
        .print()
    ssc.start()
    ssc.awaitTermination()
  }
}

思考如何從故障中|重啓中恢復狀態?

object SparkKafkaWordCount {
  def main(args: Array[String]): Unit = {
    //將狀態信息存儲在hdfs的/checkpoints目錄下(自動創建)
    var checkpointPath="hdfs://CentOSX:9000/checkpoint"
    //第一次啓時候初始化,一旦書寫完成後,無法進行修改!
    var sscg=StreamingContext.getOrCreate(checkpointPath,()=>{
      val conf = new SparkConf().setMaster("local[2]").setAppName("SparkKafkaWordCount")
      val ssc=new StreamingContext(conf,Seconds(5))
      ssc.checkpoint(checkpointPath)
      def updateFun(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
        var total= newValues.sum+runningCount.getOrElse(0)
        Some(total)
      }
      ssc.socketTextStream("CentOSX",9999)
        .flatMap(_.split("\\s+"))
        .map((_,1))
        .mapWithState(StateSpec.function((k:String,v:Option[Int],state:State[Int])=>{
          var total=0
          //判斷存儲狀態的state是否存在
          if(state.exists()){
            //如果存在,獲得state存儲的值賦值給total
            total=state.getOption().getOrElse(0)
          }
          //得到歷史值之後的total加上增量值
          total+=v.getOrElse(1)//v.getOrElse不存在則增量爲1
          //根據total修改state
          state.update(total)
          //輸出(k,出現次數)
          (k,total)
        }))
        //設置狀態持久化的頻率,改頻率不能高於 微批 拆分頻率 ts>=5s
        .checkpoint(Seconds(5))
        .print()
      ssc

    })

    sscg.sparkContext.setLogLevel("FATAL")//關閉日誌打印
    sscg.start()
    sscg.awaitTermination()
  }
}

窗口 - window

Spark Streaming還提供窗口計算,允許您在滑動數據窗口上應用轉換。下圖說明了此滑動窗口。
在這裏插入圖片描述

以上描述了窗口長度是3個時間單位的微批,窗口的滑動間隔是2個時間單位的微批,注意:Spark的流處理中要求窗口的長度以及滑動間隔必須是微批的整數倍。

val conf = new SparkConf().setMaster("local[2]").setAppName("KafkaStreamWordCount")
    val ssc = new StreamingContext(conf, Seconds(1))

    ssc.socketTextStream("CentOS",9999)
      .flatMap(_.split("\\s+"))
      .map((_,1))
      .reduceByKeyAndWindow((v1:Int,v2:Int)=> v1+v2,Seconds(5),Seconds(5))
      .print()

    ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印
    ssc.start()
    ssc.awaitTermination()

Output Operations

  • foreachRDD(func)
val conf = new SparkConf().setMaster("local[2]").setAppName("KafkaStreamWordCount")
val ssc = new StreamingContext(conf, Seconds(1))

ssc.socketTextStream("CentOS",9999)
  .flatMap(_.split("\\s+"))
  .map((_,1))
  .window(Seconds(5),Seconds(5))
  .reduceByKey((v1:Int,v2:Int)=> v1+v2)
  //foreachRDD是spark stream算子
  .foreachRDD(rdd=>{
  	//foreachPartition是spark RDD算子
    rdd.foreachPartition(items=>{
        var jedisPool=new JedisPool("CentOS",6379)
        val jedis = jedisPool.getResource
        val pipeline = jedis.pipelined()//jedis批處理
        
		//將RDD-item使用map算子轉換成(Int,String)形式,封裝成scala的Map集合,再轉換成java的Map集合
        val map = items.map(t=>(t._1,t._2+"")).toMap.asJava
        pipeline.hmset("wordcount",map)

        pipeline.sync()//對Jedis批處理加鎖
        jedis.close()//將Jedis連接放回連接池
    })
  })
ssc.sparkContext.setLogLevel("FATAL")//關閉日誌打印
ssc.start()
ssc.awaitTermination()
發佈了18 篇原創文章 · 獲贊 0 · 訪問量 1878
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章