Spark Streaming進階

在前面Spark Streaming入門的基礎上繼續深入學習Spark Streaming

StreamingContext

初始化一個Spark Streaming程序時必須要創建StreamingContext作為程序的入口。
一旦StreamingContext定義好以后,就可以做如下的事情

  • 定義輸入源通過創建輸入DStreams
  • 定義流的操作使用transformation輸出操作到Dsteams
  • 開始接收數據和進行啟動streamingContext.start()
  • 等待進程的停止streamingContext.awaitTermination()或者手動停止streamingContext.stop().
    注意事項:
  • StreamingContext啟動后,新的流計算將部能被添加設置
  • StreamingContext停止在后,不能重啟,可以把整個作業停掉,在重啟。
  • 只有一個StreamingContext被激活在JVM同一個時間點

輸入DStream和Receiver

輸入DStream(InputDStream/ReceiverInputDStream)

輸入DStream代表了來自數據源的輸入數據流。在之前的wordcount例子中,lines就是一個輸入DStream(JavaReceiverInputDStream),代表了從netcat(nc)服務接收到的數據流。輸入DStream分為InputDStream和ReceiverInputDStream兩種,其中文件數據流(FileInputDStream)即是一個InputDStream,它監聽本地或者HDFS上的新文件,然后生成RDD,其它輸入DStream為ReceiverInputDStream類型,都會綁定一個Receiver對象。輸入DStream是一個關鍵的組件,用來從數據源接收數據,并將其存儲在Spark的內存中,以供后續處理。
Spark Streaming提供了兩種內置的數據源支持;

  • 基礎數據源:StreamingContext API中直接提供了對這些數據源的支持,比如文件、socket、Akka Actor等。
  • 高級數據源:諸如Kafka、Flume、Kinesis、Twitter等數據源,通過第三方工具類提供支持。這些數據源的使用,需要引用其依賴。
  • 自定義數據源:我們可以自己定義數據源,來決定如何接受和存儲數據。

Receiver

ReceiverInputDStream類型的輸入流,都會綁定一個Receiver對象。整體流程如下


image.png

由于Receiver獨占一個cpu core,所以ReceiverInputDStream類型的作業在本地啟動時絕對不能用local或者local[1],因為那樣的話,只會給執行輸入DStream的executor分配一個線程。而Spark Streaming底層的原理是,至少要有兩條線程,一條線程用來分配給Receiver接收數據,一條線程用來處理接收到的數據。正確的做法時local[n],n>Receiver的數量。

Transformations on DStreams

Transformed DStream 是由其他DStream 通過非Output算子裝換而來的DStream
例如例子中的lines通過flatMap算子轉換生成了FlatMappedDStream:

    val words = lines.flatMap(_.split(" "))

其他的方法這里就不寫了,不會的可以看看官網

Transformation其實和rdd里面的沒有什么區別,多了下面兩個:


image.png

在后面實戰中通過代碼詳細講解。

Output Operations on DStreams

輸出操作允許將DStream的數據推送到外部系統,如數據庫或文件系統。 由于輸出操作實際上允許外部系統使用轉換后的數據,因此它們會觸發所有DStream轉換的實際執行(這里的Transform Operation也就是RDD中的action。)。 這里有一個不同的:


image.png

案列

UpdateStateByKey算子的使用

這個算子的意思就是:統計你的streaming啟動到現在為止的信息。
回顧入門課程中的wordcount案列,我們若第一次輸入 a b c。經過處理后輸入[a:1,b:1,c:1],第二次在輸入a b c,同樣輸出[a:1,b:1,c:1]。那么怎么樣實現累計加,輸出[a:2,b:2,c:2]呢?此時就需要UpdateStateByKey來解決,如何使用?下面兩步走

  • 定義狀態 :狀態可以是任意數據類型。
  • 定義狀態更新函數 :使用函數指定如何使之前的狀態更新為現在的狀態。
    代碼如下
object UpdateStateByKey {
  def main(args: Array[String]): Unit = {
    //創建SparkConf
    val conf=new SparkConf().setAppName("UpdateStateByKey").setMaster("local[2]")
    //通過conf 得到StreamingContext,底層就是創建了一個SparkContext
    val ssc=new StreamingContext(conf,Seconds(10))
//啟用checkpoint(用戶存儲中間數據),需要設置一個支持容錯 的、可靠的文件系統(如 HDFS、s3 等)目錄來保存 checkpoint 數據,
ssc.checkpoint("/root/data/sparkStreaming_UpdateStateByKey_out")
    //通過socketTextStream創建一個DSteam
    val DStream=ssc.socketTextStream("192.168.30.130",9999)

    DStream.flatMap(_.split(",")).map((_,1))
      .updateStateByKey(updateFunction).print()

    ssc.start()  // 一定要寫
    ssc.awaitTermination()
  }

  def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = {
    val curr = currentValues.sum
    val pre = preValues.getOrElse(0)
    Some(curr + pre)
  }
}

這里的updateFunction方法就是需要我們自己去實現的狀態跟新的邏輯,currValues就是當前批次的所有值,preValue是歷史維護的狀態,updateStateByKey返回的是包含歷史所有狀態信息的DStream。

通過socket監聽一個端口收集數據,存儲到mysql中

數據庫建表語句

create table wc(
word char(10),
count int
);

程序代碼

object ForEachRDD {
    def main(args: Array[String]): Unit = {
      val conf=new SparkConf().setAppName("ForEacheRDD").setMaster("local[2]")
      val ssc=new StreamingContext(conf,Seconds(10))

      val DStream=ssc.socketTextStream("192.168.30.130",9999)
      //wc
      val result=DStream.flatMap(x=>x.split(",")).map(x=>(x,1)).reduceByKey(_+_)

      //把結果寫入到mysql
      //foreachRDD把函數作用在每個rdd上
      dstream.foreachRDD { rdd =>
           rdd.foreachPartition { partitionOfRecords =>
          // ConnectionPool is a static, lazily initialized pool of connections
          val con=getConnection()
          partitionOfRecords.foreach(record => {
             val word=record ._1
             val count=record ._2.toInt
            //sql
            val sql=s"insert into wc values('$word',$count)"
            //插入數據
            val pstmt=con.prepareStatement(sql)
            pstmt.executeUpdate()
            //關閉
           pstmt.close()
            con.close()
        })
    }
  }
      ssc.start()
      ssc.awaitTermination()
  }
    def getConnection(): Connection={
      //加載驅動
      Class.forName("com.mysql.jdbc.Driver")
      //準備參數
      val url="jdbc:mysql://localhost:3306/spark"
      val username="root"
      val password="root"
      val con=DriverManager.getConnection(url,username,password)
       con
    }
}

可以把數據庫插入部分替換為

     result.foreachRDD(rdd=>{
        rdd.foreach(x=>{
          val con=getConnection()
          val word=x._1
          val count=x._2.toInt
          //sql
          val sql=s"insert into wc values('$word',$count)"
          //插入數據
          val pstmt=con.prepareStatement(sql)
          pstmt.executeUpdate()
          //關閉
          pstmt.close()
          con.close()
        })
      })

這么做程序雖然也能執行成功,但針對的是每一個rdd都創建一個數據庫連接,非常的消耗資源。用foreachPartition的好處是每一個分區創建一個連接,性能大大提升。
此時會有疑問,把獲取數據庫連接的代碼 放在rdd.foreach之前不久可以解決多次獲取連接的問題了嗎?這里絕對不能放在外面,因為放在外面會報序列化錯誤


image.png

原因是放在外面的代碼執行在 執行在driver端,數據庫插入操作執行在worker端。這就涉及到了跨網絡傳輸,肯定會出現序列化的問題。
程序優化:
這里把數據庫連接放在連接池中更佳。

transform實現黑名單

上面提到transform的含義就是DStream和RDD之間的轉換。
我們以模擬黑名單為例對數據進行過濾

object Transform {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("Transform").setMaster("local[2]")
    val ssc=new StreamingContext(conf,Seconds(10))

    //黑名單
    val black=Array(
      "laowang",
      "lisi"
    )

    //讀取數據生成rdd,方便rdd的join
    val blackRDD=ssc.sparkContext.parallelize(black).map(x=>(x,true))

    //輸入數據
    //"1,zhangsan,20","2,lisi,30", "3,wangwu,40", "4,laowang,50"
    val DStream=ssc.socketTextStream("192.168.30.130",9999)
    val output=DStream.map(x=>(x.split(",")(1),x)).transform(rdd=>{
          rdd.leftOuterJoin(blackRDD).
            filter(x=>x._2._2.getOrElse(false)!=true).map(x=>x._2._1)
    })
    //輸出
    output.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

窗口函數的使用

Spark Streaming還提供了窗口化計算,這些計算允許您在滑動的數據窗口上應用變換,主要用于每隔一個時間段計算一個時間段數據這種場景。 那到底什么時滑動窗口呢,我看先看一幅圖


image.png

如圖所示,每當窗口在源DStream上滑動時,該窗口內的RDD被組合,每一次對三個time(自己設置的)進行計算,間隔兩個time進行一次計算。所以要設置兩個參數:

  • 窗口長度 - 窗口的持續時間(圖中的小框框)。
  • 滑動間隔 - 執行窗口操作的時間間隔(圖中每個框的間隔時間)。
    如圖所示就可能出現一個問題,設置的間隔太短就可能出現重復計算的可能,或者某些數據沒有計算,這些也是很正常的。

需求:每隔3s計算過去5s字符出現的次數
此時把窗口長度設置為5,滑動間隔設置為3即可。

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
  * Created by grace on 2018/6/7.
  */
object WindowOperations {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("WindowOperations").setMaster("local[2]")
    val ssc=new StreamingContext(conf,Seconds(1))

    val DStream=ssc.socketTextStream("192.168.30.130",9999)

    //wc
 DStream.flatMap(_.split(",")).map((_,1)).reduceByKeyAndWindow((a:Int,b:Int)=>(a+b),Seconds(5),Seconds(3)).print
    ssc.start()
    ssc.awaitTermination()
  }
}

整合spark sql進行操作

我們可以使用DataFrame和SQL來操作流式數據,但是你必須使用StreamingContext正在使用的SparkContext來創建SparkSession,如果driver出現了故障,只有這樣才能重新啟動。

object DataFrameAndSQLOperations {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("DataFrameAndSQLOperations").setMaster("local[2]")
    val ssc=new StreamingContext(conf,Seconds(10))
    val DStream=ssc.socketTextStream("192.168.30.130",9999)
    val result=DStream.flatMap(_.split(","))
    result.foreachRDD(rdd=>{
      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()

    })


    ssc.start()
    ssc.awaitTermination()
  }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容