Spark Streaming是將流式計算分解成一系列短小的批處理作業。這里的批處理引擎是Spark,也就是把Spark Streaming的輸入數據按照batch size(如1秒)分成一段一段的數據(Discretized Stream),每一段數據都轉換成Spark中的RDD(Resilient Distributed Dataset),然后將Spark Streaming中對DStream的Transformation操作變為針對Spark中對RDD的Transformation操作,將RDD經過操作變成中間結果保存在內存中。整個流式計算根據業務的需求可以對中間的結果進行疊加,或者存儲到外部設備。
圖2顯示了Spark Streaming的整個流程。
容錯性:對于流式計算來說,容錯性至關重要。首先我們要明確一下Spark中RDD的容錯機制。每一個RDD都是一個不可變的分布式可重算的數據集,其記錄著確定性的操作繼承關系(lineage),所以只要輸入數據是可容錯的,那么任意一個RDD的分區(Partition)出錯或不可用,都是可以利用原始輸入數據通過轉換操作而重新算出的。
對于Spark Streaming來說,其RDD的傳承關系如圖3所示,圖中的每一個橢圓形表示一個RDD,橢圓形中的每個圓形代表一個RDD中的一個Partition,圖中的每一列的多個RDD表示一個DStream(圖中有三個DStream),而每一行最后一個RDD則表示每一個Batch Size所產生的中間結果RDD。我們可以看到圖中的每一個RDD都是通過lineage相連接的,由于Spark Streaming輸入數據可以來自于磁盤,例如HDFS(多份拷貝)或是來自于網絡的數據流(Spark Streaming會將網絡輸入數據的每一個數據流拷貝兩份到其他的機器)都能保證容錯性。所以RDD中任意的Partition出錯,都可以并行地在其他機器上將缺失的Partition計算出來。這個容錯恢復方式比連續計算模型(如Storm)的效率更高。
實時性:對于實時性的討論,會牽涉到流式處理框架的應用場景。Spark Streaming將流式計算分解成多個Spark Job,對于每一段數據的處理都會經過Spark DAG圖分解,以及Spark的任務集的調度過程。對于目前版本的Spark Streaming而言,其最小的Batch Size的選取在0.5~2秒鐘之間(Storm目前最小的延遲是100ms左右),所以Spark Streaming能夠滿足除對實時性要求非常高(如高頻實時交易)之外的所有流式準實時計算場景。
擴展性與吞吐量:Spark目前在EC2上已能夠線性擴展到100個節點(每個節點4Core),可以以數秒的延遲處理6GB/s的數據量(60M records/s),其吞吐量也比流行的Storm高2~5倍,圖4是Berkeley利用WordCount和Grep兩個用例所做的測試,在Grep這個測試中,Spark Streaming中的每個節點的吞吐量是670k records/s,而Storm是115k records/s。
1.Spark Streaming的編程模型
Spark Streaming的編程和Spark的編程如出一轍,對于編程的理解也非常類似。對于Spark來說,編程就是對于RDD的操作;而對于Spark Streaming來說,就是對DStream的操作。下面將通過一個大家熟悉的WordCount的例子來說明Spark Streaming中的輸入操作、轉換操作和輸出操作。
(1)Spark Streaming初始化:
在開始進行DStream操作之前,需要對Spark Streaming進行初始化生成StreamingContext。參數中比較重要的是第一個和第三個,第一個參數是指定Spark Streaming運行的集群地址,而第三個參數是指定Spark Streaming運行時的batch窗口大小。在這個例子中就是將1秒鐘的輸入數據進行一次Spark Job處理。
val ssc = new StreamingContext(“Spark://…”, “WordCount”,
Seconds(1), [Homes], [Jars])
(2) Spark Streaming的輸入操作:
目前Spark Streaming已支持了豐富的輸入接口,大致分為兩類:
一類是磁盤輸入,如以batch size作為時間間隔監控HDFS文件系統的某個目錄,將目錄中內容的變化作為Spark Streaming的輸入;
另一類就是網絡流的方式,目前支持Kafka、Flume、Twitter和TCP socket。在WordCount例子中,假定通過網絡socket作為輸入流,監聽某個特定的端口,最后得出輸入DStream(lines)。
val lines = ssc.socketTextStream(“localhost”,8888)
(3)Spark Streaming的轉換操作:
與Spark RDD的操作極為類似,Spark Streaming也就是通過轉換操作將一個或多個DStream轉換成新的DStream。常用的操作包括map、filter、flatmap和join,以及需要進行shuffle操作的groupByKey/reduceByKey等。在WordCount例子中,我們首先需要將DStream(lines)切分成單詞,然后將相同單詞的數量進行疊加, 最終得到的wordCounts就是每一個batch size的(單詞,數量)中間結果。
val words = lines.flatMap(_.split(“ ”))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
map(func)
Return a new DStream by passing each element of the source DStream through a function func.
flatMap(func)
Similar to map, but each input item can be mapped to 0 or more output items.
filter(func)
Return a new DStream by selecting only the records of the source DStream on which func returns true.
repartition(numPartitions)
Changes the level of parallelism in this DStream by creating more or fewer partitions.
union(otherStream)
Return a new DStream that contains the union of the elements in the source DStream and otherDStream.
count()
Return a new DStream of single-element RDDs by counting the number of elements in each RDD of the source DStream.
reduce(func)
Return a new DStream of single-element RDDs by aggregating the elements in each RDD of the source DStream using a function func (which takes two arguments and returns one). The function should be associative and commutative so that it can be computed in parallel.
countByValue()
When called on a DStream of elements of type K, return a new DStream of (K, Long) pairs where the value of each key is its frequency in each RDD of the source DStream.
reduceByKey(func, [numTasks])
When called on a DStream of (K, V) pairs, return a new DStream of (K, V) pairs where the values for each key are aggregated using the given reduce function. Note: By default, this uses Spark's default number of parallel tasks (2 for local mode, and in cluster mode the number is determined by the config propertyspark.default.parallelism
) to do the grouping. You can pass an optional numTasks
argument to set a different number of tasks.
join(otherStream, [numTasks])
When called on two DStreams of (K, V) and (K, W) pairs, return a new DStream of (K, (V, W)) pairs with all pairs of elements for each key.
cogroup(otherStream, [numTasks])
When called on a DStream of (K, V) and (K, W) pairs, return a new DStream of (K, Seq[V], Seq[W]) tuples.
transform(func)
Return a new DStream by applying a RDD-to-RDD function to every RDD of the source DStream. This can be used to do arbitrary RDD operations on the DStream.
updateStateByKey(func)
Return a new "state" DStream where the state for each key is updated by applying the given function on the previous state of the key and the new values for the key. This can be used to maintain arbitrary state data for each key.
有時候我們需要在DStream中跨批次維護狀態(例如跟蹤用戶訪問網站的會話)。updateStateByKey提供了一個鍵值對的訪問。用于鍵值對形式的DStream.
如單詞計數:第二個參數為oldState
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 _)
(4)Spark Streaming有特定的窗口操作,窗口操作涉及兩個參數:
一個是滑動窗口的寬度(Window Duration);
一個是窗口滑動的頻率(Slide Duration),
這兩個參數必須是batch size的倍數。例如以過去5秒鐘為一個輸入窗口,每1秒統計一下WordCount,那么我們會將過去5秒鐘的每一秒鐘的WordCount都進行統計,然后進行疊加,得出這個窗口中的單詞統計。
val wordCounts = words.map(x => (x, 1)).
reduceByKeyAndWindow(_ + _, Seconds(5s),seconds(1))
但上面這種方式還不夠高效。如果我們以增量的方式來計算就更加高效,例如,計算t+4秒這個時刻過去5秒窗口的WordCount,那么我們可以將t+3時刻過去5秒的統計量加上[t+3,t+4]的統計量,在減去[t-2,t-1]的統計量(如圖5所示),這種方法可以復用中間三秒的統計量,提高統計的效率。
val wordCounts = words.map(x => (x, 1))
.reduceByKeyAndWindow(_ + _, _ - _, Seconds(5s),seconds(1))
// Reduce last 30 seconds of data, every 10 secondsval
windowedWordCounts =
pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
window(windowLength, slideInterval)
Return a new DStream which is computed based on windowed batches of the source DStream.
countByWindow(windowLength,slideInterval)
Return a sliding window count of elements in the stream.
reduceByWindow(func, windowLength,slideInterval)
Return a new single-element stream, created by aggregating elements in the stream over a sliding interval using func. The function should be associative and commutative so that it can be computed correctly in parallel.
reduceByKeyAndWindow(func,windowLength, slideInterval, [numTasks])
When called on a DStream of (K, V) pairs, returns a new DStream of (K, V) pairs where the values for each key are aggregated using the given reduce function func over batches in a sliding window.Note: By default, this uses Spark's default number of parallel tasks (2 for local mode, and in cluster mode the number is determined by the config property spark.default.parallelism
) to do the grouping. You can pass an optional numTasks
argument to set a different number of tasks.
reduceByKeyAndWindow(func, invFunc,windowLength, slideInterval, [numTasks])
A more efficient version of the above reduceByKeyAndWindow()
where the reduce value of each window is calculated incrementally using the reduce values of the previous window. This is done by reducing the new data that enters the sliding window, and “inverse reducing” the old data that leaves the window. An example would be that of “adding” and “subtracting” counts of keys as the window slides. However, it is applicable only to “invertible reduce functions”, that is, those reduce functions which have a corresponding “inverse reduce” function (taken as parameter invFunc). Like in reduceByKeyAndWindow
, the number of reduce tasks is configurable through an optional argument. Note that checkpointing must be enabled for using this operation.
countByValueAndWindow(windowLength,slideInterval, [numTasks])
When called on a DStream of (K, V) pairs, returns a new DStream of (K, Long) pairs where the value of each key is its frequency within a sliding window. Like in reduceByKeyAndWindow
, the number of reduce tasks is configurable through an optional argument.
(5)對于輸出操作
Spark提供了將數據打印到屏幕及輸入到文件中。在WordCount中我們將DStream wordCounts輸入到HDFS文件中。
wordCounts = saveAsHadoopFiles(“WordCount”)
Spark Streaming啟動:經過上述的操作,Spark Streaming還沒有進行工作,我們還需要調用Start操作,Spark Streaming才開始監聽相應的端口,然后收取數據,并進行統計。
ssc.start()
ssc.awaitTermination() // Wait for the computation to terminate
foreachRDD是一個強大的處理工具,允許data發送到外部的系統。
下面有問題,可能會報錯
serialization errors (connection object not serializable), initialization errors (connection object needs to be initialized at the workers), etc.
dstream.foreachRDD { rdd =>
val connection = createNewConnection() // executed at the driver
rdd.foreach { record =>
connection.send(record) // executed at the worker
}
}
可以這么做,但是沒有效率
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
也可以這么做,每一個partition共用一個連接
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
共用一個連接池, reusing connection objects across multiple RDDs/batches.
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
// ConnectionPool is a static, lazily initialized pool of connections
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
ConnectionPool.returnConnection(connection) // return to the pool for future reuse
}
}
處理流程實例
**Spark Streaming案例分析 **
在互聯網應用中,網站流量統計作為一種常用的應用模式,需要在不同粒度上對不同數據進行統計,既有實時性的需求,又需要涉及到聚合、去重、連接等較為復雜的統計需求。傳統上,若是使用Hadoop MapReduce框架,雖然可以容易地實現較為復雜的統計需求,但實時性卻無法得到保證;反之若是采用Storm這樣的流式框架,實時性雖可以得到保證,但需求的實現復雜度也大大提高了。Spark Streaming在兩者之間找到了一個平衡點,能夠以準實時的方式容易地實現較為復雜的統計需求。 下面介紹一下使用Kafka和Spark Streaming搭建實時流量統計框架。
--》數據暫存:Kafka作為分布式消息隊列,既有非常優秀的吞吐量,又有較高的可靠性和擴展性,在這里采用Kafka作為日志傳遞中間件來接收日志,抓取客戶端發送的流量日志,同時接受Spark Streaming的請求,將流量日志按序發送給Spark Streaming集群。
--》數據處理:將Spark Streaming集群與Kafka集群對接,Spark Streaming從Kafka集群中獲取流量日志并進行處理。Spark Streaming會實時地從Kafka集群中獲取數據并將其存儲在內部的可用內存空間中。當每一個batch窗口到來時,便對這些數據進行處理。
--》結果存儲:為了便于前端展示和頁面請求,處理得到的結果將寫入到數據庫中。
**相比于傳統的處理框架,Kafka+Spark Streaming的架構有以下幾個優點。 **
--》Spark框架的高效和低延遲保證了Spark Streaming操作的準實時性。
--》利用Spark框架提供的豐富API和高靈活性,可以精簡地寫出較為復雜的算法。
--》編程模型的高度一致使得上手Spark Streaming相當容易,同時也可以保證業務邏輯在實時處理和批處理上的復用。
在基于Kafka+Spark Streaming的流量統計應用運行過程中,有時會遇到內存不足、GC阻塞等各種問題。下面介紹一下如何對Spark Streaming應用程序進行調優來減少甚至避免這些問題的影響。
**性能調優 **
優化運行時間
--》增加并行度。確保使用整個集群的資源,而不是把任務集中在幾個特定的節點上。對于包含shuffle的操作,增加其并行度以確保更為充分地使用集群資源。
--》減少數據序列化、反序列化的負擔。Spark Streaming默認將接收到的數據序列化后存儲以減少內存的使用。但序列化和反序列化需要更多的CPU時間,因此更加高效的序列化方式(Kryo)和自定義的序列化接口可以更高效地使用CPU。
--》設置合理的batch窗口。在Spark Streaming中,Job之間有可能存在著依賴關系,后面的Job必須確保前面的Job執行結束后才能提交。若前面的Job執行時間超出了設置的batch窗口,那么后面的Job就無法按時提交,這樣就會進一步拖延接下來的Job,造成后續Job的阻塞。因此,設置一個合理的batch窗口確保Job能夠在這個batch窗口中結束是必須的。
--》減少任務提交和分發所帶來的負擔。通常情況下Akka框架能夠高效地確保任務及時分發,但當batch窗口非常小(500ms)時,提交和分發任務的延遲就變得不可接受了。使用Standalone模式和Coarse-grained Mesos模式通常會比使用Fine-Grained Mesos模式有更小的延遲。
優化內存使用
--》控制batch size。Spark Streaming會把batch窗口內接收到的所有數據存放在Spark內部的可用內存區域中,因此必須確保當前節點Spark的可用內存至少能夠容納這個batch窗口內所有的數據,否則必須增加新的資源以提高集群的處理能力。
--》及時清理不再使用的數據。上面說到Spark Streaming會將接收到的數據全部存儲于內部的可用內存區域中,因此對于處理過的不再需要的數據應及時清理以確保Spark Streaming有富余的可用內存空間。通過設置合理的spark.cleaner.ttl時長來及時清理超時的無用數據。
--》觀察及適當調整GC策略。GC會影響Job的正常運行,延長Job的執行時間,引起一系列不可預料的問題。觀察GC的運行情況,采取不同的GC策略以進一步減小內存回收對Job運行的影響。