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()
}
}
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()