一、Spark Streaming 概述
1.1 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
所組成的序列(因此得名“離散化”),可以理解為 DStream
是對多個 RDD
的再封裝。
1.2 Spark Streaming特點
- 易用
- 容錯
- 易整合到
Spark
體系
1.3 Spark Streaming架構
1. 架構圖
整體架構圖
架構實現圖
2. 背壓機制
Spark 1.5
以前版本,使用者如果要限制Receiver
的資料接收速率,可以通過設定靜態配製引數“spark.streaming.receiver.maxRate”的值來實現,此舉雖然可以通過限制接收速率,來適配當前的處理能力,防止記憶體溢位,但也會引入其它問題。比如:producer
資料生產高於maxRate
,當前叢集處理能力也高於maxRate
,這就會造成資源利用率下降等問題。
為了更好的協調資料接收速率與資源處理能力,1.5版本開始Spark Streaming
可以動態控制資料接收速率來適配叢集資料處理能力。背壓機制(即Spark Streaming Backpressure
): 根據JobScheduler
反饋作業的執行資訊來動態調整Receiver
資料接收率。
通過屬性“spark.streaming.backpressure.enabled”來控制是否啟用backpressure
機制,預設值false
,即不啟用。
二、Dstream 入門
2.1 WordCount 案例實操
需求:使用netcat
工具向9999
埠不斷的傳送資料,通過Spark Streaming
讀取埠資料並統計不同單詞出現的次數
1. 安裝 netcat 工具
netcat(nc)
是一個簡單而有用的工具,不僅可以通過使用TCP
或UDP
協議的網路連線讀寫資料,同時還是一個功能強大的網路除錯和探測工具,能夠建立你需要的幾乎所有型別的網路連線。
在Linux
終端視窗可以直接使用yum
工具進行安裝:
然後測試,開啟一個埠輸入資料
另起一個 Terminal
接受資料
測試 ok~
2. 編寫 WC 案例
新建一個模組 Spark Streaming
,新增依賴
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
複製程式碼
編寫程式碼如下
/**
* 一個簡單的使用 Spark Streaming 統計埠傳送資料的 WC 程式
*
* @author cris
* @version 1.0
**/
object Main {
def main(args: Array[String]): Unit = {
// 1. 初始化 SparkConf 物件
val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")
// 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
val context = new StreamingContext(conf, Seconds(3))
// 3. 通過監控埠建立DStream,讀進來的資料為一行行
val receiver: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
// 將單詞分割,並統計結果
val dStream: DStream[String] = receiver.flatMap(_.split(" "))
val dStream2: DStream[(String, Int)] = dStream.map((_, 1))
val dStream3: DStream[(String, Int)] = dStream2.reduceByKey(_ + _)
// 將結果列印
dStream3.print()
// 4. 啟動 Spark Streaming 程式
context.start()
context.awaitTermination()
}
}
複製程式碼
注意:如果程式執行時,log
日誌太多,可以將日誌級別改成 ERROR
log4j.rootLogger=ERROR, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
複製程式碼
然後啟動 9999
埠
[cris@hadoop101 ~]$ nc -lk 9999
複製程式碼
在啟動 IDEA
中的 Main
程式,此時控制檯如下
如果往 9999
埠輸入資料
2.2 WordCount 案例解析
Discretized Stream
是Spark Streaming
的基礎抽象,代表持續性的資料流和經過各種Spark
運算元操作後的結果資料流。在內部實現上,DStream
是一系列連續的RDD
來表示。每個RDD
含有一段時間間隔內的資料,如下圖
三、Dstream 建立
3.1 RDD 佇列
1. 用法及說明
測試過程中,可以通過使用 ssc.queueStream(queueOfRDDs)
來建立DStream
,每一個推送到這個佇列中的RDD
,都會作為一個DStream
處理。
2. 案例實操
需求:迴圈建立幾個RDD
,將RDD
放入佇列。通過SparkStream
建立Dstream
,計算WordCount
程式碼如下:
object Main2 {
def main(args: Array[String]): Unit = {
//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: InputDStream[Int] = ssc.queueStream(rddQueue, oneAtATime = false)
//5.處理佇列中的RDD資料
val mappedStream: DStream[(Int, Int)] = inputStream.map((_, 1))
val reducedStream: DStream[(Int, Int)] = mappedStream.reduceByKey(_ + _)
//6.列印結果
reducedStream.print()
ssc.start()
//7.迴圈建立並向RDD佇列中放入RDD
for (i <- 1 to 5) {
rddQueue += ssc.sparkContext.makeRDD(1 to 5, 10)
Thread.sleep(2000)
}
ssc.awaitTermination()
}
}
複製程式碼
結果展示
3.2 自定義資料來源
需要繼承Receiver
,並實現onStart
、onStop
方法來自定義資料來源採集。
實質上就是自定義 Spark Streaming
的資料接受器
程式碼如下:
class CustomerReceiver(hostName: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {
// 開啟資料接收器
override def onStart(): Unit = {
new Thread(new Runnable {
// 開啟一個執行緒執行資料接受的方法
override def run(): Unit = receive()
}).start()
}
def receive(): Unit = {
var socket: Socket = null
var reader: BufferedReader = null
try {
socket = new Socket(hostName, port)
reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
var str: String = reader.readLine()
while (str != null) {
// 儲存資料到 Spark Streaming
store(str)
str = reader.readLine()
}
} catch {
case e: Exception => {
reader.close()
socket.close()
println("獲取資料失敗,請除錯!")
}
}
}
override def onStop(): Unit = {}
}
object Main3 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("customer receiver").setMaster("local[*]")
val streamingContext = new StreamingContext(conf, Seconds(2))
// 從自定義的接收器去接收資料
val dStream: ReceiverInputDStream[String] = streamingContext.receiverStream(new CustomerReceiver("hadoop101", 9999))
dStream.print()
streamingContext.start()
streamingContext.awaitTermination()
}
}
複製程式碼
核心就是 Receiver
這個類,以及 onStart
和 store
核心方法
3.3 Kafka資料來源(開發重點)
1. 用法及說明
在工程中需要引入Maven
工件spark-streaming-kafka-0-8_2.11
來使用它。包內提供的 KafkaUtils
物件可以在 StreamingContext
和 JavaStreamingContext
中以你的Kafka
訊息建立出 DStream
。
兩個核心類:KafkaUtils
、KafkaCluster
需求:通過SparkStreaming
從Kafka
讀取資料,並將讀取過來的資料做簡單計算(WordCount
),最終列印到控制檯。
2. 程式碼完成如下
首先匯入依賴
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
<version>2.1.1</version>
</dependency>
複製程式碼
然後使用 Kafka
的低階 API
手動完成offset
的獲取和儲存
- 首先完成
Spark Streaming
和Kafka
的對接程式
def main(args: Array[String]): Unit = {
//1.初始化Spark配置資訊
val conf = new SparkConf().setMaster("local[*]").setAppName("KafkaStream")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(conf, Seconds(4))
//3. kafka引數宣告
val brokers = "hadoop101:9092,hadoop102:9092,hadoop103:9092"
val topic = "first"
val group = "cris"
val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
val kafkaPropsMap: Map[String, String] = Map(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
ConsumerConfig.GROUP_ID_CONFIG -> group,
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization)
//4. 獲取 KafkaCluster 物件
val kafkaCluster = new KafkaCluster(kafkaPropsMap)
//5. 獲取上一次讀取結束後的 offset
val fromOffset: Map[TopicAndPartition, Long] = getOffset(topic, group, kafkaCluster).toMap
//6. 讀取 Kafka 資料為 DStream 物件
val kafkaStream: InputDStream[String] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder,
String](ssc,
kafkaPropsMap,
fromOffset,
(message: MessageAndMetadata[String, String]) => message.message())
//7. DStream 資料處理
kafkaStream.map((_, 1)).reduceByKey(_ + _).print()
//8. 儲存 offset
saveOffsets(kafkaCluster, kafkaStream, group)
//9. 開啟 Spark Streaming 程式
ssc.start()
ssc.awaitTermination()
}
複製程式碼
- 然後是手動維護
offset
的兩個方法(讀取和更新)
/**
* 獲取消費者組上一次資料消費的 offset 位置
*
* @param topic 主題
* @param group 消費者組
* @param kafkaCluster Kafka 叢集抽象
*/
def getOffset(topic: String, group: String, kafkaCluster: KafkaCluster): mutable.HashMap[TopicAndPartition, Long] = {
// 定義一個存放主題分割槽 offset 資訊的 map
val topicAndPartitionToLong = new mutable.HashMap[TopicAndPartition, Long]()
// 獲取主題的分割槽資訊
val partionsInfo: Either[Err, Set[TopicAndPartition]] = kafkaCluster.getPartitions(Set(topic))
// 如果主題分割槽資訊有資料
if (partionsInfo.isRight) {
// 取出主題分割槽資訊物件
val infos: Set[TopicAndPartition] = partionsInfo.right.get
// 獲取該消費者組消費 topic 分割槽資料的 offset 資訊
val offsetInfo: Either[Err, Map[TopicAndPartition, Long]] = kafkaCluster.getConsumerOffsets(group, infos)
// 如果 offset 資訊有該消費者組消費 topic 分割槽資料的 offset 資料
if (offsetInfo.isRight) {
val offsets: Map[TopicAndPartition, Long] = offsetInfo.right.get
for (offset <- offsets) {
topicAndPartitionToLong += offset
}
} else {
// 手動初始化該消費者組消費該主題分割槽資料 offset 資訊
for (topicAndPartition <- infos) {
topicAndPartitionToLong += (topicAndPartition -> 0)
}
}
}
topicAndPartitionToLong
}
/**
* 每批次 Spark Streaming 消費資訊完畢都要進行 offset 的更新
*
* @param kafkaCluster Kafka 叢集抽象
* @param kafkaStream Spark Streaming 消費 Kafka 資料的抽象
* @param group
*/
def saveOffsets(kafkaCluster: KafkaCluster, kafkaStream: InputDStream[String], group: String): Unit = {
// 將 KafkaStream 物件中的每個 rdd 物件中的 offset 取出來
kafkaStream.foreachRDD(rdd => {
// 從 rdd 中取出 offsets
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 遍歷每個 rdd 得到的所有分割槽資料的 offsets
for (offset <- ranges) {
val untilOffset: Long = offset.untilOffset
/// 儲存 offset
val result: Either[Err, Map[TopicAndPartition, Short]] = kafkaCluster.setConsumerOffsets(group, Map(offset.topicAndPartition() -> untilOffset))
if (result.isLeft) {
println(s"${result.left.get}")
} else {
println(s"${result.right.get} + $untilOffset")
}
}
})
}
複製程式碼
最後測試如下
啟動 Kafka
的生產者
kafka_producer_topic first
# 實質上是 kafka-console-producer.sh --broker-list hadoop101:9092 --topic first
# Cris 這裡使用了別名代替繁瑣的編寫
複製程式碼
然後啟動 IDEA
程式
生產資料
IDEA
控制檯列印資訊
證明 Spark Streaming
和 Kafka
對接成功~
四、DStream 轉換(重點)
DStream
上的操作與RDD
的類似,分為Transformations
(轉換)和Output Operations
(輸出)兩種,此外轉換操作中還有一些比較特殊的原語,如:updateStateByKey
、transform
以及各種Window
相關的原語。
4.1 無狀態轉化操作
無狀態轉化操作就是把簡單的RDD
轉化操作應用到每個批次上,也就是轉化DStream
中的每一個RDD
。部分無狀態轉化操作列在了下表中。注意,針對鍵值對的DStream
轉化操作(比如 reduceByKey
要新增import StreamingContext._
才能在Scala
中使用。
需要記住的是,儘管這些函式看起來像作用在整個流上一樣,但事實上每個DStream
在內部是由許多RDD
(批次)組成,且無狀態轉化操作是分別應用到每個RDD
上的。例如,reduceByKey
會歸約每個時間區間中的資料,但不會歸約不同時間區間之間的資料。
Transform
Transform
允許DStream
上執行任意的RDD-to-RDD
函式。即使這些函式並沒有在DStream
的API
中暴露出來,通過該函式可以方便的擴充套件Spark API
。該函式每一批次排程一次。其實也就是對DStream
中的RDD
應用轉換。
比如之前使用 DStream
完成 WC
案例,我們可以對 DStream
中的每個 RDD
執行 WC
操作,通過 transform
運算元
/**
* 將 DStream 通過 transform 運算元轉換為一系列的 RDD 進行操作
*
* @author cris
* @version 1.0
**/
object Main4 {
def main(args: Array[String]): Unit = {
// 1. 初始化 SparkConf 物件
val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")
// 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
val context = new StreamingContext(conf, Seconds(3))
// 3. 通過監控埠建立DStream,讀進來的資料為一行行
val Dstream: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
val result: DStream[(String, Int)] = Dstream.transform(rdd => {
val words: RDD[String] = rdd.flatMap(_.split(" "))
val wordsToTuple: RDD[(String, Int)] = words.map((_, 1))
val wordsCount: RDD[(String, Int)] = wordsToTuple.reduceByKey(_ + _)
wordsCount
})
result.print()
context.start()
context.awaitTermination()
}
}
複製程式碼
4.2 有狀態轉換操作
UpdateStateByKey
UpdateStateByKey
運算元用於記錄歷史記錄,有時,我們需要在DStream
中跨批次維護狀態(例如流計算中累加wordcount
)。針對這種情況,updateStateByKey
為我們提供了對一個狀態變數的訪問,用於鍵值對形式的DStream
。給定一個由(鍵,事件)對構成的 DStream
,並傳遞一個指定如何根據新的事件更新每個鍵對應狀態的函式,它可以構建出一個新的 DStream
,其內部資料為(鍵,狀態) 對。
updateStateByKey
的結果會是一個新的DStream
,其內部的RDD
序列是由每個時間區間對應的(鍵,狀態)對組成的。
updateStateByKey
操作使得我們可以在用新資訊進行更新時保持任意的狀態。為使用這個功能,需要做下面兩步:
-
定義狀態,狀態可以是一個任意的資料型別。
-
定義狀態更新函式,用此函式闡明如何使用之前的狀態和來自輸入流的新值對狀態進行更新。
使用updateStateByKey
需要對檢查點目錄進行配置,會使用檢查點來儲存狀態。
更新版的wordcount
/**
* 通過儲存上一批次的計算結果和當前批次計算結果整合完成資料狀態的更新
*
* @author cris
* @version 1.0
**/
object StatusWC {
def main(args: Array[String]): Unit = {
// 1. 初始化 SparkConf 物件
val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")
// 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
val context = new StreamingContext(conf, Seconds(3))
// 2.1 需要設定 CheckPoint 來儲存每批次計算的狀態,以便於和下一批次計算的結果做整合
context.sparkContext.setCheckpointDir("./checkpoint")
// 3. 使用 DStream 完成 WC
val words: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
val wordsSeparated: DStream[String] = words.flatMap(_.split(" "))
val wordsTuple: DStream[(String, Int)] = wordsSeparated.map((_, 1))
// 3.1 定義每批次計算結果和上批次計算結果的整合函式
val updateStateFunc: (Seq[Int], Option[Int]) => Option[Int] = (values: Seq[Int], state: Option[Int]) => {
val sum: Int = values.sum
val result: Int = state.getOrElse(0) + sum
Some(result)
}
// 4. 批次計算並列印
val result: DStream[(String, Int)] = wordsTuple.updateStateByKey(updateStateFunc)
result.print()
context.start()
context.awaitTermination()
}
}
複製程式碼
測試結果
總結:所謂的有狀態轉換就是通過儲存上一批次計算結果,然後和下一批次計算結果整合得到新的計算結果,依次類推~
Window Operations
Window Operations
可以設定視窗的大小和滑動視窗的間隔來動態的獲取當前Steaming
的允許狀態。所有基於視窗的操作都需要兩個引數,分別為視窗時長以及滑動步長。
(1)視窗時長:計算內容的時間範圍;
(2)隔多久觸發一次計算。
注意:這兩者都必須為批次大小的整數倍。
如下圖所示WordCount
案例:視窗大小為計算批次的2倍,滑動步等於批次大小。
程式碼如下:
/**
* 使用視窗函式,根據步長(計算間隔)來統計視窗長度(計算批次個數)的資料
*
* @author cris
* @version 1.0
**/
object WindowWC {
def main(args: Array[String]): Unit = {
// 1. 初始化 SparkConf 物件
val conf: SparkConf = new SparkConf().setAppName("Spark Streaming").setMaster("local[*]")
// 2. 建立 StreamingContext 物件,Spark Streaming 流程的上下文物件
val context = new StreamingContext(conf, Seconds(3))
// 3. 使用 DStream 完成 WC
val words: ReceiverInputDStream[String] = context.socketTextStream("hadoop101", 9999)
val wordsTuple: DStream[(String, Int)] = words.flatMap(_.split(" ")).map((_, 1))
// 使用視窗函式,每經過 3 秒就計算當前時刻前 6 秒的所有資料
val result: DStream[(String, Int)] = wordsTuple.reduceByKeyAndWindow((x: Int, y: Int) => x + y, Seconds(6), Seconds(3))
result.print()
context.start()
context.awaitTermination()
}
}
複製程式碼
測試結果如下
關於Window
的操作還有如下方法:
(1)window(windowLength, slideInterval): 基於對源DStream
窗化的批次進行計算返回一個新的Dstream
;
(2)countByWindow(windowLength, slideInterval): 返回一個滑動視窗計數流中的元素個數;
(3)reduceByWindow(func, windowLength, slideInterval): 通過使用自定義函式整合滑動區間流元素來建立一個新的單元素流;
(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]): 當在一個(K,V)
對的DStream
上呼叫此函式,會返回一個新(K,V)
對的DStream
,此處通過對滑動視窗中批次資料使用reduce
函式來整合每個key
的value
值。
(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]): 這個函式是上述函式的更高效版本,每個視窗的reduce
值都是通過用前一個窗的reduce
值來遞增計算。通過reduce
進入到滑動視窗資料並”反向reduce”離開視窗的舊資料來實現這個操作。
一個例子是隨著視窗滑動對keys
的“加”“減”計數。通過前邊介紹可以想到,這個函式只適用於”可逆的reduce函式”,也就是這些reduce
函式有相應的”反reduce”函式(以引數invFunc
形式傳入)。如前述函式,reduce
任務的數量通過可選引數來配置。(這個方法可以用於視窗函式的優化)
五、DStream 輸出
輸出操作指定了對流資料經轉化操作得到的資料所要執行的操作(例如把結果推入外部資料庫或輸出到螢幕上)
與RDD
中的惰性求值類似,如果一個DStream
及其派生出的DStream
都沒有被執行輸出操作,那麼這些DStream
就都不會被求值。如果StreamingContext
中沒有設定輸出操作,整個context
就都不會啟動。
輸出操作如下:
(1)print()
:在執行流程式的驅動結點上列印DStream
中每一批次資料的最開始 10
個元素。這用於開發和除錯。在Python API
中,同樣的操作叫print
。
(2)saveAsTextFiles(prefix, [suffix])
:以text
檔案形式儲存這個DStream
的內容。每一批次的儲存檔名基於引數中的prefix
和suffix
。”prefix-Time_IN_MS[.suffix]”。
(3)saveAsObjectFiles(prefix, [suffix])
:以Java
物件序列化的方式將Stream
中的資料儲存為 SequenceFiles
. 每一批次的儲存檔名基於引數中的為"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。
(4)saveAsHadoopFiles(prefix, [suffix])
:將Stream
中的資料儲存為 Hadoop files
. 每一批次的儲存檔名基於引數中的為"prefix-TIME_IN_MS[.suffix]"。Python API 中目前不可用。
(5)foreachRDD(func)
:這是最通用的輸出操作,即將函式 func
用於產生於 stream
的每一個RDD
。其中引數傳入的函式func
應該實現將每一個RDD
中資料推送到外部系統,如將RDD
存入檔案或者通過網路將其寫入資料庫。
通用的輸出操作foreachRDD
,它用來對DStream
中的RDD
執行任意計算。這和transform
有些類似,都可以讓我們訪問任意RDD
。在foreachRDD()
中,可以重用我們在Spark
中實現的所有行動操作。比如,常見的用例之一是把資料寫到諸如MySQL
的外部資料庫中。
注意:
(1)連線不能寫在driver
層面(序列化);
(2)如果寫在 RDD
的foreach
方法則每個RDD
都建立,得不償失;
(3)增加foreachPartition
,在分割槽建立。