一 概述
spark是近實時的流處理框架,支持的數據源有kafka、flume、kinesis、tcp sockets、文件系統等。流式讀取數據後,可以用類似map、reduce、join和window等高層函數進行處理。最終,處理後的數據可以寫入文件系統、數據庫、實時儀表盤等。這裏其實已經把流式數據抽象成了一個個小批次的分佈式數據集,因此,你也可以在這些數據之上進行機器學習以及圖計算。
內部實現如下圖(把流式數據分成一個個小的批次數據):
spark streaming提供一個DStream的抽象概念,代表一個源源不斷的數據流。DStream可以從上面提到的數據源得到也可以從其他的DStream得到,DSteam其實就是代表很多RDD的集合。
二 一個小例子
監聽tcp socket,接收流式數據,做單詞統計。
首先要導入一些包,StreamingContext是spark streaming主要的入口類,下面我們創建了一個本地的StreamingContext(兩個執行線程),每間隔1秒一個批次。
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Create a local StreamingContext with two working thread and batch interval of 1 second.
// The master requires 2 cores to prevent a starvation scenario.
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
然後可以指定監聽tcp端口(地址和端口)創建DStream,例如(本地9999端口):
// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)
lines其實就是從這個端口接收的文本數據,一行一個記錄(多個RDD),然後把行記錄分隔成一個單詞一個記錄:
// Split each line into words
val words = lines.flatMap(_.split(" "))
然後就是對這些RDD集進行處理,得到單詞計數:
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()
以上是一個批次的單詞計數,也就是上面我們創建的時候指定的1秒。
但僅僅上面那些代碼,運行後不會進行計算,要通過下面代碼啓動計數:
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
三 基本概念
3.1 添加依賴
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.3.0</version>
</dependency>
僅僅添加上面的依賴,是不支持類似kafka、flume、kinesis等數據源的,需要添加類似spark-streaming-xyz_2.11這樣的依賴到自己的工程中:
3.2 初始化StreamingContext
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、mesos或yarn集羣方式的url,或者用local[*]字符串來指定爲本地模式。實際上不會硬編碼到程序裏,通常會通過spark-submit命令行進行指定。如果你想要使用SparkContext,那麼可以通過ssc.sparkContext來訪問。
流數據的批次的時間間隔,需要根據你應用的實際需求以及集羣資源來確定,詳細可參考性能調節章節。
同樣StreamingContext也可以通過一個已經存在的SparkContext來創建:
import org.apache.spark.streaming._
val sc = ... // existing SparkContext
val ssc = new StreamingContext(sc, Seconds(1))
在context創建之後,你還要做如下步驟:
1、定義創建DStream的數據數據源
2、在輸入DStream上進行transformations和輸出操作。
3、調用streamingContext.start()來啓動上面的程序處理。
4、調用streamingContext.awaitTermination來等待程序結束(人爲停止或程序出錯)
5、可以通過streamingContext.stop()來人工停止程序。
需要記住的是:
1、一旦context已經開始(streamingContext.start()),新的流計算就不能被創建並加入到程序中了。
2、context被stop後,不能restart。
3、一個jvm中同一時刻只有一個streamingContext是active的。
4、streamingContext的stop函數也會把SparkContext給stop掉。如果僅僅想把streamingContext停掉,那麼就要給stop函數傳入stopSparkContext=false的參數。
5、SparkContext可以重複利用來創建多個SteamingContext,只要之前的SteamingContext先被stop(只停streamingContext不停SparkContext)。
3.3 離散數據流(DStreams)
DStreams代表源源不斷的RDD的集合,每個rdd是一段時間內的流數據,如下圖所示(參考上面的例子,一秒一個RDD):
在這些RDD上面可以做各種的高層函數處理,如下:
3.4 輸入DStreams和接收器
輸入DStream是從數據源得到的一個數據流抽象概念(源源不斷的RDD集合),在上面的例子中,lines就是一個從tcp socket接收數據的一個輸入DStream。每個一輸入DStream(除文件系統),都與一個Receiver接收器密切相關,這個接收器負責從數據源接收數據到內存等待處理。
spark streaming提供兩種類型的內建流數據源:
1、基礎類型數據源:直接在SteamingContext API中就支持的(文件系統和socket連接),無需導入別的maven依賴。
2、高級數據源:類似kafka、flume、kinesis等,需要導入額外的依賴包才能夠使用。
需要注意的是:如果你想在你的流應用中並行接收多個流式數據,你可以創建多個輸入DStream。這樣會同時創建多個接收器來並行接收多個數據流的數據。但是要注意,一個spark worker/executor是一個長時間運行的任務,會佔用一個你的應用分配的內核,所以你需要分配足夠的內核(或者線程(本地運行的話))給你的流應用,不但用於接收器接收流數據,還要用戶處理接收到的數據。
記住:
1、如果你本地運行spark streaming程序,不要設置master爲“local”或“local[1]”(代表只會有一個本地線程來跑你的程序)。這時候如果你使用基於接收器的輸入DStream(例如sockets,kafka、flume等),這個唯一的線程就會被接收器佔用,那麼就沒有空餘線程去處理接收到的數據了。所以本地運行,需要設置master爲“local[n]”,n要大於接收器的數量。
2、如果集羣模式,應用分配的內核數量必須多餘接收器的數量,否則,系統就只能接收數據,但不能去處理它了。
3.4.1 基礎數據源
上面的例子我們已經看過了TCP socket的基礎數據源的使用方式,現在看一下文件系統基礎數據源的使用。
文件系統:
能夠從兼容hdfs api的文件系統(hdfs、s3、nfs等)中讀取文件,可以通過StreamingContext.fileSteam[KeyClass,ValueClass,InputFormatClass]來創建輸入DStream。因爲文件系統數據源不需要接收器,所以不需要爲接收器分配內核來接收數據。api如下:
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)
對於文本文件,可以使用如下api:
streamingContext.textFileStream(dataDirectory)
上面api的文件目錄如何監聽?
spark streaming會監聽dataDirectory目錄,並處理在這個目錄中創建的任何一個文件。
1、例如“hdfs://namenode:8040/logs/”這種簡單的目錄會被監控,這個目錄中的文件會被發現並處理。(經驗證,只會監控logs下的文件,子目錄裏不會監控處理)
2、可以使用通配符匹配,例如:“hdfs://namenode:8040/logs/2017/*”匹配上的目錄都會被處理。這個是目錄的匹配模式,而不是文件的。(經驗證,是隻會匹配目錄,文件忽略)
3、所有的文件必須是同一數據類型。(這個很好理解,fileSteam函數需指定InputFormatClass,所以如果不同類型,就不對了)
4、文件是根據它的修改時間,而不是創建時間來監控處理的。(比如一個文件cp到監控目錄,會被處理,然後你又去修改了這個文件,那麼這些修改是被忽略的)
5、一旦文件被處理了,那麼修改文件將不會觸發重新讀取,也就是說更新會被忽略。(比如一個文件cp到監控目錄,會被處理,然後你又去修改了這個文件,那麼這些修改是被忽略的)
5、一個目錄下的文件越多,將會花費更多的時間來掃描更新,即使沒有文件被修改。(這個也很好理解,監控目錄的時候,是需要一個個文件去看它的修改時間的,所以即使文件沒有更新,也是要一個個掃描的)
6、如果使用了通配符,那麼如果你修改了整個目錄的名稱,而這個名稱恰好能夠被匹配到,這個目錄就會被監控處理。目錄中的文件,只有修改時間在目前的窗口內纔會被讀取處理。
7、可以調用FileSystem.setTimes()函數來修改文件時間戳,然後稍後的時間窗口就可以被處理了,儘管這個文件沒有改變。
使用類似hdfs這種文件系統,比如你代碼中寫數據到文件的時候,一旦output stream創建了,那麼你的文件的修改時間也就是output stream的創建時間,但你的數據會在這個時刻之後被寫入(有可能時間較長),那麼根據目錄監控只能根據文件修改時間的規則,在你創建output stream的時間窗口內的數據會被處理(也就是上面說的1秒),1秒之後的數據會被忽略。
所以你可以先在別的目錄中創建文件並寫入數據,然後等寫完之後拷貝或者重命名到監控目錄就可以了。
相比之下,類似Amazon S3和Azure這種存儲系統,一般會保證數據寫完纔會確定修改時間。而且,如果要重命名文件,那麼修改時間就是這個重命名的時間(本地文件系統重命名文件是不會被監控到的)。
可以自定義receiver來接收數據
3.4.2 高級數據源
類似kafka、flume這種,需要導入額外的依賴包。所以如果spark shell是不支持這些數據源的,如果你想在spark shell中使用,那麼就需要下載這些maven依賴包,然後把這些包加到classpath中。
3.4.3 自定義數據源
你也可以自定義數據源,你所要做的就是自定義自己的receiver,從自定義數據源中能夠接收數據即可。
3.4.4 接收器可靠性
1、可靠的接收器:當收到數據並容錯存儲後,接收器需要發送消息已收到的確認給數據源。
2、不可靠的接收器:接收到數據後不會發確認消息,不支持消息確認或者支持但不想確認的,都可以使用這種方式。
3.5 DStreams轉換操作
和RDDs類似,支持普通Rdd的很多轉換操作,詳見官方文檔。下面是一些比較特殊的操作:
3.5.1 UpdateStateByKey操作
updateStateByKey操作允許你使用流數據的新數據來維持一個持續不斷的狀態更新。步驟如下:
1.定義一個狀態(可任意數據類型)
2.定義狀態更新函數(指定怎麼用之前的狀態和新到的數值來更新這個狀態)
每個batch(其實就是一個RDD)裏,更新函數會被應用到所有存在的key上,不管在這個批次裏這個key是否有新數據。如果更新函數返回None,那麼這個key的鍵值對會被移除。
讓我們驗證下,統計文本數據中單詞個數,狀態count是integer型,更新函數定義如下(newValues是流數據的一個個新到數據,runningCount是新到數據之前的狀態,返回值是更新後的狀態):
def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
val newCount = ... // add the new values with the previous running count to get the new count
Some(newCount)
}
然後下面就是使用這個跟新函數:
val runningCounts = pairs.updateStateByKey[Int](updateFunction _)(pairs是之前例子中的DStream,其實內部就是(word,1)類型的RDD)
然後newValues就是(word,1),word就是key,就會根據單詞進行加1計數。
需要注意的是,使用updateStateKey需要配置checkpoint目錄(因爲要維持一個源源不斷的狀態計數,所以如果程序出現問題,這個狀態就沒辦法維持了,丟失了之前的狀態)
3.5.2 transform操作
transform操作(還有它的變種transformWith操作),允許你在DStream上應用任意的RDD-to-RDD轉換函數。如果DStream api中沒有暴露一些RDD的操作函數,這裏就可以用這個方法進行處理。例如,DStream的api中沒有把數據流的每個批次和另一個數據集關聯的功能。但這裏你就可以用transform進行了。例如,
待續·······