Spark Streaming的使用

一、Spark Streaming引入

集羣監控

一般的大型集羣和平臺, 都需要對其進行監控的需求。

要針對各種數據庫, 包括 MySQL, HBase 等進行監控

要針對應用進行監控, 例如 Tomcat, Nginx, Node.js 等

要針對硬件的一些指標進行監控, 例如 CPU, 內存, 磁盤 等

Spark Streaming介紹

官網:http://spark.apache.org/streaming/

Spark Streaming是一個基於Spark Core之上的實時計算框架,可以從很多數據源消費數據並對數據進行實時的處理,具有高吞吐量和容錯能力強等特點。

Spark Streaming的特點

1.易用

可以像編寫離線批處理一樣去編寫流式程序,支持java/scala/python語言。

2.容錯

SparkStreaming在沒有額外代碼和配置的情況下可以恢復丟失的工作。

3.易整合到Spark體系

流式處理與批處理和交互式查詢相結合。

實時計算所處的位置

二、Spark Streaming原理

1、SparkStreaming原理

整體流程

Spark Streaming中,會有一個接收器組件Receiver,作爲一個長期運行的task跑在一個Executor上。Receiver接收外部的數據流形成input DStream

DStream會被按照時間間隔劃分成一批一批的RDD,當批處理間隔縮短到秒級時,便可以用於處理實時數據流。時間間隔的大小可以由參數指定,一般設在500毫秒到幾秒之間

DStream進行操作就是對RDD進行操作,計算處理的結果可以傳給外部系統。

Spark Streaming的工作流程像下面的圖所示一樣,接到實時數據後,給數據分批次,然後傳給Spark Engine(引擎)處理最後生成該批次的結果。

數據抽象

Spark Streaming的基礎抽象是DStream(Discretized Stream,離散化數據流,連續不斷的數據流),代表持續性的數據流經過各種Spark算子操作後的結果數據流

可以從以下多個角度深入理解DStream

1.DStream本質上就是一系列時間上連續的RDD

2.DStream的數據的進行操作也是按照RDD爲單位來進行的

3.容錯性

底層RDD之間存在依賴關係,DStream直接也有依賴關係,RDD具有容錯性,那麼DStream也具有容錯性

如圖:每一個橢圓形表示一個RDD

橢圓形中的每個圓形代表一個RDD中的一個Partition分區

每一列的多個RDD表示一個DStream(圖中有三列所以有三個DStream)

每一行最後一個RDD則表示每一個Batch Size所產生的中間結果RDD

4.準實時性/近實時性

Spark Streaming將流式計算分解成多個Spark Job,對於每一時間段數據的處理都會經過Spark DAG圖分解以及Spark的任務集的調度過程。

對於目前版本的Spark Streaming而言,其最小的Batch Size的選取在0.5~5秒鐘之間

所以Spark Streaming能夠滿足流式準實時計算場景,對實時性要求非常高的如高頻實時交易場景則不太適合

總結       

簡單來說DStream就是對RDD的封裝,你對DStream進行操作,就是對RDD進行操作

對於DataFrame/DataSet/DStream來說本質上都可以理解成RDD

2、DStream相關操作

DStream上的操作與RDD的類似,分爲以下兩種:

Transformations(轉換)

Output Operations(輸出)/Action

Transformations

常見Transformation---無狀態轉換:每個批次的處理不依賴於之前批次的數據

Transformation

Meaning

map(func)

對DStream中的各個元素進行func函數操作,然後返回一個新的DStream

flatMap(func)

與map方法類似,只不過各個輸入項可以被輸出爲零個或多個輸出項

filter(func)

過濾出所有函數func返回值爲true的DStream元素並返回一個新的DStream

union(otherStream)

將源DStream和輸入參數爲otherDStream的元素合併,並返回一個新的DStream.

reduceByKey(func, [numTasks])

利用func函數對源DStream中的key進行聚合操作,然後返回新的(K,V)對構成的DStream

join(otherStream, [numTasks])

輸入爲(K,V)(K,W)類型的DStream,返回一個新的(K(V,W)類型的DStream

transform(func)

通過RDD-to-RDD函數作用於DStream中的各個RDD,可以是任意的RDD操作,從而返回一個新的RDD

特殊的Transformations---有狀態轉換:當前批次的處理需要使用之前批次的數據或者中間結果

有狀態轉換包括基於追蹤狀態變化的轉換(updateStateByKey)和滑動窗口的轉換

1.UpdateStateByKey(func)

2.Window Operations 窗口操作

Output/Action

Output Operations可以DStream的數據輸出到外部的數據庫或文件系統

當某個Output Operations被調用時,spark streaming程序纔會開始真正的計算過程(與RDD的Action類似)

Output Operation

Meaning

print()

打印到控制檯

saveAsTextFiles(prefix, [suffix])

保存流的內容爲文本文件,文件名爲"prefix-TIME_IN_MS[.suffix]".

saveAsObjectFiles(prefix,[suffix])

保存流的內容爲SequenceFile,文件名爲 "prefix-TIME_IN_MS[.suffix]".

saveAsHadoopFiles(prefix,[suffix])

保存流的內容爲hadoop文件,文件名爲"prefix-TIME_IN_MS[.suffix]".

foreachRDD(func)

對Dstream裏面的每個RDD執行func

總結

三、Spark Streaming實戰

1、WordCount

需求&準備

首先在linux服務器上安裝nc工具

nc是netcat的簡稱,原本是用來設置路由器,我們可以利用它向某個端口發送數據

yum install -y nc

●啓動一個服務端並開放9999端口,等一下往這個端口發數據

nc -lk 9999

●發送數據

import org.apache.spark.sql.catalyst.expressions.Second
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

object Demo01_WC {

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

    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WC")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
    //監聽socket接收數據
    val dataDStrem: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
    //操作數據
    dataDStrem.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).print()
    ssc.start()
    ssc.awaitTermination()
  }

}

執行

1.先執行nc -lk 9999

2.然後執行代碼

3.不斷的在1中輸入不同的單詞

hadoop spark sqoop hadoop spark hive hadoop

4.觀察IDEA控制檯輸出

sparkStreaming每隔5s計算一次當前5s內的數據,然後將每個批次的數據輸出

2、updateStateByKey

在上面的那個案例中存在這樣一個問題:

每個批次的單詞次數都被正確的統計出來,但是結果不能累加!

如果需要累加需要使用updateStateByKey(func)來更新狀態

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

object Demo02_updateStateByKey {

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

    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WC")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
    //歷史數據存在的目錄
    ssc.checkpoint("./wc")
    //監聽socket接收數據
    val dataDStrem: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
    //操作數據
    dataDStrem.flatMap(_.split(" ")).map((_,1)).updateStateByKey(updateFunc).print()
    ssc.start()
    ssc.awaitTermination()
  }
  def updateFunc(currentValues:Seq[Int],historyValue:Option[Int])={
    //currentValues當前值
    //historyValue歷史值
    val result: Int = currentValues.sum+historyValue.getOrElse(0)
    Some(result)
  }

}

 執行

1.先執行nc -lk 9999

2.然後執行以上代碼

3.不斷的在1中輸入不同的單詞,

hadoop spark sqoop hadoop spark hive hadoop

4.觀察IDEA控制檯輸出

sparkStreaming每隔5s計算一次當前5s內的數據,然後將每個批次的結果數據累加輸出。

3、reduceByKeyAndWindow

圖解

滑動窗口轉換操作的計算過程如下圖所示,

我們可以事先設定一個滑動窗口的長度(也就是窗口的持續時間),並且設定滑動窗口的時間間隔(每隔多長時間執行一次計算),

比如設置滑動窗口的長度(也就是窗口的持續時間)爲24H,設置滑動窗口的時間間隔(每隔多長時間執行一次計算)爲1H

那麼意思就是:每隔1H計算最近24H的數據

代碼演示

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

object Demo03_reduceByKeyAndWindow {

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

    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WC")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
    //歷史數據存在的目錄
    ssc.checkpoint("./wc")
    //監聽socket接收數據
    val dataDStrem: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
    //操作數據
    val wordAndOneDStream: DStream[(String, Int)] = dataDStrem.flatMap(_.split(" ")).map((_,1))
    //使用窗口函數進行WordCount計數
    wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5)).print()
    ssc.start()
    ssc.awaitTermination()
  }
  def updateFunc(currentValues:Seq[Int],historyValue:Option[Int])={
    //currentValues當前值
    //historyValue歷史值
    val result: Int = currentValues.sum+historyValue.getOrElse(0)
    Some(result)
  }

}

執行

1.先執行nc -lk 9999

2.然後執行以上代碼

3.不斷的在1中輸入不同的單詞

hadoop spark sqoop hadoop spark hive hadoop

4.觀察IDEA控制檯輸出

現象:sparkStreaming每隔5s計算一次當前在窗口大小爲10s內的數據,然後將結果數據輸出。

4、統計一定時間內的熱門詞彙TopN

需求

模擬百度熱搜排行榜

統計最近10s的熱搜詞Top3,每隔5秒計算一次

WindowDuration = 10s

SlideDuration = 5s

代碼演示

import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

 /*
  * Desc 我們要模擬百度熱搜排行榜統計最近10s的熱搜詞Top3,每隔5秒計算一次
  */
object WordCount4 {
  def main(args: Array[String]): Unit = {
    //1.創建StreamingContext
    //spark.master should be set as local[n], n > 1
    val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中對數據進行切分形成一個RDD
    //2.監聽Socket接收數據
    //ReceiverInputDStream就是接收到的所有的數據組成的RDD,封裝成了DStream,接下來對DStream進行操作就是對RDD進行操作
    val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
    //3.操作數據
    val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))
    val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_,1))
    //4.使用窗口函數進行WordCount計數
    val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5))
    val sorteDStream: DStream[(String, Int)] = wordAndCount.transform(rdd => {
      val sortedRDD: RDD[(String, Int)] = rdd.sortBy(_._2, false) //逆序/降序
      println("===============top3==============")
      sortedRDD.take(3).foreach(println)
      println("===============top3==============")
      sortedRDD
    }
    )
    //No output operations registered, so nothing to execute
    sorteDStream.print
    ssc.start()//開啓
    ssc.awaitTermination()//等待優雅停止
  }
}

執行

1.先執行nc -lk 9999

2.然後在執行以上代碼

3.不斷的在1中輸入不同的單詞

hadoop spark sqoop hadoop spark hive hadoop

4.觀察IDEA控制檯輸出

四、整合kafka

1、Kafka快速回顧

Broker : 安裝Kafka服務的機器就是一個broker

Producer :消息的生產者,負責將數據寫入到broker中(push)

Consumer:消息的消費者,負責從kafka中拉取數據(pull),老版本的消費者需要依賴zk,新版本的不需要

Topic: 主題,相當於是數據的一個分類,不同topic存放不同業務的數據 --主題:區分業務

Replication:副本,數據保存多少份(保證數據不丟失) --副本:數據安全

Partition:分區,是一個物理的分區,一個分區就是一個文件,一個Topic可以有1~n個分區,每個分區都有自己的副本 --分區:併發讀寫

Consumer Group:消費者組,一個topic可以有多個消費者/組同時消費,多個消費者如果在一個消費者組中,那麼他們不能重複消費數據 --消費者組:提高消費者消費速度、方便統一管理

注意:一個Topic可以被多個消費者或者組訂閱,一個消費者/組也可以訂閱多個主題

注意:讀數據只能從Leader讀, 寫數據也只能往Leader寫,Follower會從Leader那裏同步數據過來做副本!!!

常用命令
#啓動kafka
/export/servers/kafka/bin/kafka-server-start.sh -daemon /export/servers/kafka/config/server.properties 

#停止kafka
/export/servers/kafka/bin/kafka-server-stop.sh 

#查看topic信息
/export/servers/kafka/bin/kafka-topics.sh --list --zookeeper node01:2181
 
#創建topic
/export/servers/kafka/bin/kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 3 --partitions 3 --topic test
 
#查看某個topic信息
/export/servers/kafka/bin/kafka-topics.sh --describe --zookeeper node01:2181 --topic test
 
#刪除topic
/export/servers/kafka/bin/kafka-topics.sh --zookeeper node01:2181 --delete --topic test
 
#啓動生產者--控制檯的生產者一般用於測試
/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092 --topic spark_kafka
 
#啓動消費者--控制檯的消費者一般用於測試
/export/servers/kafka/bin/kafka-console-consumer.sh --zookeeper node01:2181 --topic spark_kafka--from-beginning
 
# 消費者連接到borker的地址
/export/servers/kafka/bin/kafka-console-consumer.sh --bootstrap-server node01:9092,node02:9092,node03:9092 --topic spark_kafka --from-beginning

2、整合Kafka兩種模式說明

面試題:Receiver & Direct

開發中我們經常會利用SparkStreaming實時地讀取kafka中的數據然後進行處理,在spark1.3版本後,kafkaUtils裏面提供了兩種創建DStream的方法:

1.Receiver接收方式:

KafkaUtils.createDstream(開發中不用,瞭解即可,但是面試可能會問)

Receiver作爲常駐的Task運行在Executor等待數據,但是一個Receiver效率低,需要開啓多個,再手動合併數據(union),再進行處理,很麻煩

Receiver哪臺機器掛了,可能會丟失數據,所以需要開啓WAL(預寫日誌)保證數據安全,那麼效率又會降低!

Receiver方式是通過zookeeper來連接kafka隊列,調用Kafka高階API,offset存儲在zookeeper,由Receiver維護,

spark在消費的時候爲了保證數據不丟也會在Checkpoint中存一份offset,可能會出現數據不一致

所以不管從何種角度來說,Receiver模式都不適合在開發中使用了,已經淘汰了

2.Direct直連方式:

KafkaUtils.createDirectStream(開發中使用,要求掌握)

Direct方式是直接連接kafka分區來獲取數據,從每個分區直接讀取數據大大提高了並行能力

Direct方式調用Kafka低階API(底層API),offset自己存儲和維護,默認由Spark維護在checkpoint中,消除了與zk不一致的情況 

當然也可以自己手動維護,把offset存在mysql、redis中

所以基於Direct模式可以在開發中使用,且藉助Direct模式的特點+手動操作可以保證數據的Exactly once 精準一次

 總結:

Receiver接收方式

  1. 多個Receiver接受數據效率高,但有丟失數據的風險。
  2. 開啓日誌(WAL)可防止數據丟失,但寫兩遍數據效率低。
  3. Zookeeper維護offset有重複消費數據可能。
  4. 使用高層次的API

Direct直連方式

  1. 不使用Receiver,直接到kafka分區中讀取數據
  2. 不使用日誌(WAL)機制。
  3. Spark自己維護offset
  4. 使用低層次的API

擴展:關於消息語義

實現方式

消息語義

存在的問題

Receiver

at most once

最多被處理一次

會丟失數據

Receiver+WAL

at least once

最少被處理一次

不會丟失數據,但可能會重複消費,且效率低

Direct+手動操作

exactly once

只被處理一次/精準一次

不會丟失數據,也不會重複消費,且效率高

●注意:

開發中SparkStreaming和kafka集成有兩個版本:0.8及0.10+

0.8版本有Receiver和Direct模式(但是0.8版本生產環境問題較多,在Spark2.3之後不支持0.8版本了)

0.10以後只保留了direct模式(Reveiver模式不適合生產環境),並且0.10版本API有變化(更加強大)

3、spark-streaming-kafka-0-8(瞭解)

Receiver

KafkaUtils.createDstream使用了receivers來接收數據,利用的是Kafka高層次的消費者api,偏移量由Receiver維護在zk中,對於所有的receivers接收到的數據將會保存在Spark executors中,然後通過Spark Streaming啓動job來處理這些數據,默認會丟失,可啓用WAL日誌,它同步將接受到數據保存到分佈式文件系統上比如HDFS。保證數據在出錯的情況下可以恢復出來。儘管這種方式配合着WAL機制可以保證數據零丟失的高可靠性,但是啓用了WAL效率會較低,且無法保證數據被處理一次且僅一次,可能會處理兩次。因爲Spark和ZooKeeper之間可能是不同步的。

官方現在已經不推薦這種整合方式

準備工作

1.啓動zookeeper集羣

zkServer.sh start

2.啓動kafka集羣

kafka-server-start.sh  /export/servers/kafka/config/server.properties

3.創建topic

kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 1 --partitions 3 --topic spark_kafka

4.通過shell命令向topic發送消息

kafka-console-producer.sh --broker-list node01:9092 --topic  spark_kafka

hadoop spark sqoop hadoop spark hive hadoop

5.添加kafka的pom依賴

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

API

通過receiver接收器獲取kafka中topic數據,可以並行運行更多的接收器讀取kafak topic中的數據,這裏爲3個

    val receiverDStream: immutable.IndexedSeq[ReceiverInputDStream[(String, String)]] = (1 to 3).map(x => {

      val stream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)

      stream

    })

如果啓用了WAL(spark.streaming.receiver.writeAheadLog.enable=true)可以設置存儲級別(默認StorageLevel.MEMORY_AND_DISK_SER_2)

代碼演示

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.immutable

object SparkKafka {
  def main(args: Array[String]): Unit = {
    //1.創建StreamingContext
    val config: SparkConf = 
new SparkConf().setAppName("SparkStream").setMaster("local[*]")
      .set("spark.streaming.receiver.writeAheadLog.enable", "true")
//開啓WAL預寫日誌,保證數據源端可靠性
    val sc = new SparkContext(config)
    sc.setLogLevel("WARN")
    val ssc = new StreamingContext(sc,Seconds(5))
    ssc.checkpoint("./kafka")
//==============================================
    //2.準備配置參數
    val zkQuorum = "node01:2181,node02:2181,node03:2181"
    val groupId = "spark"
    val topics = Map("spark_kafka" -> 2)//2表示每一個topic對應分區都採用2個線程去消費,
//ssc的rdd分區和kafka的topic分區不一樣,增加消費線程數,並不增加spark的並行處理數據數量
    //3.通過receiver接收器獲取kafka中topic數據,可以並行運行更多的接收器讀取kafak topic中的數據,這裏爲3個
    val receiverDStream: immutable.IndexedSeq[ReceiverInputDStream[(String, String)]] = (1 to 3).map(x => {
      val stream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)
      stream
    })
    //4.使用union方法,將所有receiver接受器產生的Dstream進行合併
    val allDStream: DStream[(String, String)] = ssc.union(receiverDStream)
    //5.獲取topic的數據(String, String) 第1個String表示topic的名稱,第2個String表示topic的數據
    val data: DStream[String] = allDStream.map(_._2)
//==============================================
    //6.WordCount
    val words: DStream[String] = data.flatMap(_.split(" "))
    val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
    val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
    result.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

Direct

Direct方式會定期地從kafka的topic下對應的partition中查詢最新的偏移量,再根據偏移量範圍在每個batch裏面處理數據,Spark通過調用kafka簡單的消費者API讀取一定範圍的數據。

  • Direct的缺點是無法使用基於zookeeper的kafka監控工具
  • Direct相比基於Receiver方式有幾個優點: 
  • 簡化並行

不需要創建多個kafka輸入流,然後union它們,sparkStreaming將會創建和kafka分區數一樣的rdd的分區數,而且會從kafka中並行讀取數據,sparkRDD的分區數和kafka中的分區數據是一一對應的關係。

  • 高效

Receiver實現數據的零丟失是將數據預先保存在WAL中,會複製一遍數據,會導致數據被拷貝兩次,第一次是被kafka複製,另一次是寫到WAL中。而Direct不使用WAL消除了這個問題。 

  • 恰好一次語義(Exactly-once-semantics)

Receiver讀取kafka數據是通過kafka高層次api把偏移量寫入zookeeper中,雖然這種方法可以通過數據保存在WAL中保證數據不丟失,但是可能會因爲sparkStreaming和ZK中保存的偏移量不一致而導致數據被消費了多次。

Direct的Exactly-once-semantics(EOS)通過實現kafka低層次api,偏移量僅僅被ssc保存在checkpoint中,消除了zk和ssc偏移量不一致的問題。

API

KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)

代碼演示

import kafka.serializer.StringDecoder
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}


object SparkKafka2 {
  def main(args: Array[String]): Unit = {
    //1.創建StreamingContext
    val config: SparkConf = 
new SparkConf().setAppName("SparkStream").setMaster("local[*]")
    val sc = new SparkContext(config)
    sc.setLogLevel("WARN")
    val ssc = new StreamingContext(sc,Seconds(5))
    ssc.checkpoint("./kafka")
    //==============================================
    //2.準備配置參數
    val kafkaParams = Map("metadata.broker.list" -> "node01:9092,node02:9092,node03:9092", "group.id" -> "spark")
    val topics = Set("spark_kafka")
    val allDStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)
    //3.獲取topic的數據
    val data: DStream[String] = allDStream.map(_._2)
    //==============================================
    //WordCount
    val words: DStream[String] = data.flatMap(_.split(" "))
    val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
    val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
    result.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

4/spark-streaming-kafka-0-10

spark-streaming-kafka-0-10版本中,API有一定的變化,操作更加靈活,開發中使用

pom.xml

<!--<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
    <version>${spark.version}</version>
</dependency>-->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
    <version>${spark.version}</version>
</dependency>

API

http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html

創建topic

/export/servers/kafka/bin/kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 3 --partitions 3 --topic spark_kafka

啓動生產者

/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092,node01:9092,node01:9092 --topic spark_kafka

代碼演示

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object Demo05_SparkKafka {

  def main(args: Array[String]): Unit = {
    //創建sparkConf
    val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
    //創建sparkContext
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    //
    val ssc: StreamingContext = new StreamingContext(sc, Seconds(5))

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "node01:9092,node02:9092,node03:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "SparkKafkaDemo",
      //earliest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費
      //latest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分區下的數據
      //none:topic各分區都存在已提交的offset時,從offset後開始消費;只要有一個分區不存在已提交的offset,則拋出異常
      //這裏配置latest自動重置偏移量爲最新的偏移量,即如果有偏移量從偏移量位置開始消費,沒有偏移量從新來的數據開始消費
      "auto.offset.reset" -> "latest",
      //false表示關閉自動提交.由spark幫你提交到Checkpoint或程序員手動維護
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )
//    val topics = Array("spark_kafka")
//    val recordDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](ssc,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String, String](Array("spark_kafka"), kafkaParams))
       val kafkaDatas: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](ssc,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String, String](Array("spark_kafka"), kafkaParams))
    val wordOne: DStream[(String, Int)] = kafkaDatas.flatMap(x=>x.value().split(" ")).map((_,1))
    val wordAndOneDStream: DStream[(String, Int)] = wordOne.reduceByKeyAndWindow((a:Int, b:Int)=>a+b,Seconds(10),Seconds(5))

    val result: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
    result.print()
    ssc.start()//開啓
    ssc.awaitTermination()//等待優雅停止
  }
}

5、擴展:Kafka手動維護偏移量

API

http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html

啓動生產者

/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092,node01:9092,node01:9092 --topic spark_kafka

代碼演示

import java.sql.{DriverManager, ResultSet}

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{OffsetRange, _}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable

object SparkKafkaDemo2 {
  def main(args: Array[String]): Unit = {
    //1.創建StreamingContext
    //spark.master should be set as local[n], n > 1
    val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中對數據進行切分形成一個RDD
    //準備連接Kafka的參數
    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "node01:9092,node02:9092,node03:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "SparkKafkaDemo",
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )
    val topics = Array("spark_kafka")
    //2.使用KafkaUtil連接Kafak獲取數據
    //注意:
    //如果MySQL中沒有記錄offset,則直接連接,從latest開始消費
    //如果MySQL中有記錄offset,則應該從該offset處開始消費
    val offsetMap: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap("SparkKafkaDemo","spark_kafka")
    val recordDStream: InputDStream[ConsumerRecord[String, String]] = if(offsetMap.size > 0){//有記錄offset
      println("MySQL中記錄了offset,則從該offset處開始消費")
      KafkaUtils.createDirectStream[String, String](ssc,
      LocationStrategies.PreferConsistent,//位置策略,源碼強烈推薦使用該策略,會讓Spark的Executor和Kafka的Broker均勻對應
      ConsumerStrategies.Subscribe[String, String](topics, kafkaParams,offsetMap))//消費策略,源碼強烈推薦使用該策略
    }else{//沒有記錄offset
      println("沒有記錄offset,則直接連接,從latest開始消費")
      // /export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092 --topic  spark_kafka
      KafkaUtils.createDirectStream[String, String](ssc,
      LocationStrategies.PreferConsistent,//位置策略,源碼強烈推薦使用該策略,會讓Spark的Executor和Kafka的Broker均勻對應
      ConsumerStrategies.Subscribe[String, String](topics, kafkaParams))//消費策略,源碼強烈推薦使用該策略
    }
    //3.操作數據
    //注意:我們的目標是要自己手動維護偏移量,也就意味着,消費了一小批數據就應該提交一次offset
    //而這一小批數據在DStream的表現形式就是RDD,所以我們需要對DStream中的RDD進行操作
    //而對DStream中的RDD進行操作的API有transform(轉換)和foreachRDD(動作)
    recordDStream.foreachRDD(rdd=>{
      if(rdd.count() > 0){//當前這一時間批次有數據
        rdd.foreach(record => println("接收到的Kafk發送過來的數據爲:" + record))
        //接收到的Kafk發送過來的數據爲:ConsumerRecord(topic = spark_kafka, partition = 1, offset = 6, CreateTime = 1565400670211, checksum = 1551891492, serialized key size = -1, serialized value size = 43, key = null, value = hadoop spark ...)
        //注意:通過打印接收到的消息可以看到,裏面有我們需要維護的offset,和要處理的數據
        //接下來可以對數據進行處理....或者使用transform返回和之前一樣處理
        //處理數據的代碼寫完了,就該維護offset了,那麼爲了方便我們對offset的維護/管理,spark提供了一個類,幫我們封裝offset的數據
        val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        for (o <- offsetRanges){
          println(s"topic=${o.topic},partition=${o.partition},fromOffset=${o.fromOffset},untilOffset=${o.untilOffset}")
        }
        //手動提交offset,默認提交到Checkpoint中
        //recordDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
        //實際中偏移量可以提交到MySQL/Redis中
        OffsetUtil.saveOffsetRanges("SparkKafkaDemo",offsetRanges)
      }
    })

   /* val lineDStream: DStream[String] = recordDStream.map(_.value())//_指的是ConsumerRecord
    val wrodDStream: DStream[String] = lineDStream.flatMap(_.split(" ")) //_指的是發過來的value,即一行數據
    val wordAndOneDStream: DStream[(String, Int)] = wrodDStream.map((_,1))
    val result: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
    result.print()*/
    ssc.start()//開啓
    ssc.awaitTermination()//等待優雅停止
  }

  /*
  手動維護offset的工具類
  首先在MySQL創建如下表
    CREATE TABLE `t_offset` (
      `topic` varchar(255) NOT NULL,
      `partition` int(11) NOT NULL,
      `groupid` varchar(255) NOT NULL,
      `offset` bigint(20) DEFAULT NULL,
      PRIMARY KEY (`topic`,`partition`,`groupid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
   */
  object OffsetUtil {

    //從數據庫讀取偏移量
    def getOffsetMap(groupid: String, topic: String) = {
      val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
      val pstmt = connection.prepareStatement("select * from t_offset where groupid=? and topic=?")
      pstmt.setString(1, groupid)
      pstmt.setString(2, topic)
      val rs: ResultSet = pstmt.executeQuery()
      val offsetMap = mutable.Map[TopicPartition, Long]()
      while (rs.next()) {
        offsetMap += new TopicPartition(rs.getString("topic"), rs.getInt("partition")) -> rs.getLong("offset")
      }
      rs.close()
      pstmt.close()
      connection.close()
      offsetMap
    }

    //將偏移量保存到數據庫
    def saveOffsetRanges(groupid: String, offsetRange: Array[OffsetRange]) = {
      val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
      //replace into表示之前有就替換,沒有就插入
      val pstmt = connection.prepareStatement("replace into t_offset (`topic`, `partition`, `groupid`, `offset`) values(?,?,?,?)")
      for (o <- offsetRange) {
        pstmt.setString(1, o.topic)
        pstmt.setInt(2, o.partition)
        pstmt.setString(3, groupid)
        pstmt.setLong(4, o.untilOffset)
        pstmt.executeUpdate()
      }
      pstmt.close()
      connection.close()
    }
  }
}

 

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