一、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接收方式
- 多個Receiver接受數據效率高,但有丟失數據的風險。
- 開啓日誌(WAL)可防止數據丟失,但寫兩遍數據效率低。
- Zookeeper維護offset有重複消費數據可能。
- 使用高層次的API
Direct直連方式
- 不使用Receiver,直接到kafka分區中讀取數據
- 不使用日誌(WAL)機制。
- Spark自己維護offset
- 使用低層次的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中並行讀取數據,spark中RDD的分區數和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()
}
}
}