在前面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對象。整體流程如下
由于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里面的沒有什么區別,多了下面兩個:
在后面實戰中通過代碼詳細講解。
Output Operations on DStreams
輸出操作允許將DStream的數據推送到外部系統,如數據庫或文件系統。 由于輸出操作實際上允許外部系統使用轉換后的數據,因此它們會觸發所有DStream轉換的實際執行(這里的Transform Operation也就是RDD中的action。)。 這里有一個不同的:
案列
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之前不久可以解決多次獲取連接的問題了嗎?這里絕對不能放在外面,因為放在外面會報序列化錯誤
原因是放在外面的代碼執行在 執行在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還提供了窗口化計算,這些計算允許您在滑動的數據窗口上應用變換,主要用于每隔一個時間段計算一個時間段數據這種場景。 那到底什么時滑動窗口呢,我看先看一幅圖
如圖所示,每當窗口在源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()
}
}