Spark 2.3.0 Spark Streaming Programming Guide 學習筆記

一 概述

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 添加依賴

maven依賴:

<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 接收器可靠性

根據消息可靠性,有兩種數據源,像kafka、flume這種提供消息確認機制的數據源,在你接收到數據後需要確認消息已送達,這樣就會保證數據沒有丟失:

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進行了。例如,

待續·······


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章