Spark每日半小時(36)——Spark Streaming:(上)

DStream的轉換

與RDD類似,轉換允許修改來自輸入DStream的數據。DStream支持普通Spark RDD上可用的許多轉換。一些常見的如下。

轉換 含義
map(func) 通過將源DStream的每個元素傳遞給函數func來返回一個新的DStream
flatMap(func) 與map類似,但每個輸入項可以映射到0個或更多輸出項。
filter(func) 通過僅選擇func返回true的源DStream的記錄來返回新的DStream。
repartition(numPartitions) 通過創建更多或更少的分區來更改此DStream中的並行度級別。
union(otherStream) 返回一個新的DStream,它包含源DStream和otherDStream中元素的並集。
count() 通過計算源DStream的每個RDD中的元素數量,返回單元素RDD的新DStream。
reduce(func) 通過使用函數func(它接受兩個參數並返回一個)聚合源DStream的每個RDD中的元素,返回單元素RDD的新DStream。該函數應該是關聯和可交換的,以便可以並行計算。
countByValue() 當在類型爲K的元素的DStream上調用時,返回(K,Long)對的新DStream,其中每個鍵的值是其在源DStream的每個RDD中的頻率。
redduceByKey(func,[numTasks]) 當在(K,V)對的DStream上調用時,返回(K,V)對的新DStream,其中使用給定的reduce函數聚合每個鍵的值。注意:默認情況下,這使用Spark的默認並行任務數(本地模式爲2,在集羣模式下,數量由config屬性spark.default.parallelism確定)進行分組。我們可以傳遞可選numTasks參數來設置不同數量的任務。
join(otherStream,[numTasks]) 當在(K,V)和(K,W)對的兩個DStream上調用時,返回(K,(V,W))對的新DStream與每個鍵的所有元素對。
cogroup(otherStream,[numTasks]) 當在(K,V)和(K,W)對的DStream上調用時,返回(K,Seq[V],Seq[W])元組的新DStream。
transform(func) 通過將RDD-to-RDD函數應用於源DStream的每個RDD來返回新的DStream。這可以用於在DStream上執行任意RDD操作。
updateStateByKey(func) 返回一個新的“狀態”DStream,其中通過在鍵的先前狀態和鍵的新值上應用給定函數來更新每個鍵的狀態。這可用於維護每個密鑰的任意狀態數據。

UpdateStateByKey操作

該updateStateByKey操作允許我們在使用新信息不斷更新時保持任意狀態。要使用它,我們必須執行兩個步驟:

  1. 定義狀態:狀態可以時任意數據類型。
  2. 定義狀態更新功能:使用函數指定如何使用先前狀態和輸入流中的新值更新狀態。

在每個批處理中,Spark都會對所有現有密鑰應用狀態更新功能,無論它們是否在批處理中都有新數據。如果更新函數返回,None則將刪除鍵值對。

舉個栗子。假設我們要維護文本數據流中看到的每個單詞的運行計數。這裏,運行計數是狀態,它是一個整數。我們將更新功能定義爲:

Function2<List<Integer>, Optional<Integer>, Optional<Integer>> updateFunction =
  (values, state) -> {
    Integer newSum = ...  // add the new values with the previous running count to get the new count
    return Optional.of(newSum);
  };

這是施加在含DStream

JavaPairDStream<String, Integer> runningCounts = pairs.updateStateByKey(updateFunction);

將爲每個單詞調用更新函數,newValues其序列爲1(來自(word,1)成對)並runningCount具有前一個計數。

請注意,使用updateStateByKey需要配置檢查點目錄。

Transform操作

該transform操作允許在DStream上應用任意RDD到RDD功能。它可用於應用未在DStream API中公開的任何RDD操作。例如,將數據流中的每個批次與另一個數據集連接的功能不會直接在DStream API中公開。但是,我們可以輕鬆地使用transform來執行此操作。例如,可以通過將輸入數據流與預先計算的垃圾郵件信息(也可以使用Spark生成)連接,然後根據它進行過濾,來進行實時數據清理。

import org.apache.spark.streaming.api.java.*;
// RDD containing spam information
JavaPairRDD<String, Double> spamInfoRDD = jssc.sparkContext().newAPIHadoopRDD(...);

JavaPairDStream<String, Integer> cleanedDStream = wordCounts.transform(rdd -> {
  rdd.join(spamInfoRDD).filter(...); // join data stream with spam information to do data cleaning
  ...
});

請注意,在每個批處理間隔中都會調用提供的函數。這允許您進行時變RDD操作,即RDD操作,分區數,廣播變量等可以在批次之前進行更改。

Window操作

Spark Streaming還提供窗口計算,允許我們在滑動數據窗口上應用轉換。下圖說明了此滑動窗口。

Spark Streaming

如該圖所示,每一個窗口時間的幻燈片在源DStream,落入窗口內的源RDDs被組合及操作,以產生加窗DStream的RDDs。在這種特定情況下,操作應用於最後3個時間單位的數據,並按2個時間單位滑動。這表明任何窗口操作都需要指定兩個參數。

  • 窗口長度:窗口的持續時間(圖中的3)。
  • 滑動間隔:執行窗口操作的間隔(圖中的2)。

這兩個參數必須是源DStream的批處理間隔的倍數(圖中的1)。

讓我們用一個例子來說明窗口操作。比如說,我們希望通過每隔10秒生成最後30秒數據的字數來擴展前面的示例。爲此我們必須在最後30秒的數據reduceByKey上對pairsDstream(word , 1)對應操作。這是使用該操作完成的reduceByKeyAndWindow。

// Reduce last 30 seconds of data, every 10 seconds
JavaPairDStream<String, Integer> windowedWordCounts = pairs.reduceByKeyAndWindow((i1, i2) -> i1 + i2, Durations.seconds(30), Durations.seconds(10));

一些常見的窗口操作如下。所有這些操作都採用上述兩個參數:windowLength 和 slideInterval。

轉換 含義
windowwindowLengthslideInterval 返回一個新的DStream,它是根據源DStream的窗口批次計算的。
countByWindowwindowLengthslideInterval 返回流中元素的滑動窗口數。
reduceByWindowfuncwindowLengthslideInterval 返回一個新的單元素流,通過使用func在滑動間隔內聚合流中的元素而創建。該函數應該是關聯的和可交換的,以便可以並行正確計算。
reduceByKeyAndWindowfuncwindowLengthslideInterval,[ numTasks ]) 當在(K,V)對的DStream上調用時,返回(K,V)對的新DStream,其中使用給定的reduce函數func在滑動窗口中的批次聚合每個鍵的值。注意:默認情況下,這使用Spark的默認並行任務數(本地模式爲2,在集羣模式下,數量由config屬性spark.default.parallelism確定)進行分組。我們可以傳遞可選參數numTask來設置不同數量的任務。
reduceByKeyAndWindowfuncinvFuncwindowLengthslideInterval,[ numTasks ]) 上述更有效的版本,reduceByKeyAndWindow()其中使用前一窗口的reduce值逐步計算每個窗口的reduce值。這是通過減少進入滑動窗口的新數據和“反向減少”離開窗口的舊數據來完成的。一個例子是當窗口滑動時“添加”和“減少”鍵的計數。但是,它僅適用於“可逆減少函數”,即哪些具有想用“反向減少”函數的減函數(作爲參數invFunc)。
countByValueAndWindowwindowLength, slideInterval,[numTasks ]) 當在(K,V)對的DStream上調用時,返回(K,Long)對的新DStream,其中每個鍵的值是其在滑動窗口內的頻率。同樣reduceByKeyAndWindow,reduce任務的數量可通過可選參數進行配置

Join操作

最後,值得強調的是,我們可以輕鬆地在Spark Streaming中執行不同類型地連接。

流連接

Streams可以很容易地與其他流連接。

JavaPairDStream<String, String> stream1 = ...
JavaPairDStream<String, String> stream2 = ...
JavaPairDStream<String, Tuple2<String, String>> joinedStream = stream1.join(stream2);

這裏,在每個批處理間隔中,生成地RDD stream1將與生成的RDD連接stream2.你也可以做leftOuterJoin,rightOuterJoin,fullOuterJoin。此外,在流的窗口上進行連接通常非常有用,同時也很容易。

JavaPairDStream<String, String> windowedStream1 = stream1.window(Durations.seconds(20));
JavaPairDStream<String, String> windowedStream2 = stream2.window(Durations.minutes(1));
JavaPairDStream<String, Tuple2<String, String>> joinedStream = windowedStream1.join(windowedStream2);

流數據集連接

在解釋DStream.transform操作時已經顯示了這一點。這是將窗口流與數據集連接的另一個示例。

JavaPairRDD<String, String> dataset = ...
JavaPairDStream<String, String> windowedStream = stream.window(Durations.seconds(20));
JavaPairDStream<String, String> joinedStream = windowedStream.transform(rdd -> rdd.join(dataset));

實際上,我們還可以動態更改要加入的數據集。提供給的函數在transform每個批處理間隔進行評估,因此將使用dataset引用指向的當前數據集。

API文檔中提供了完整的DStream轉換列表。我們在使用Spark時,可以參考Spark官方API文檔進行查詢參閱。

DStreams的輸出操作

輸出操作允許將DStream的數據推送到外部系統,如數據庫或文件系統。由於輸出操作實際上允許外部系統使用轉換後的數據,因此它們會觸發所有DStream轉換的實際執行(類似於RDD的操作)。目前,定義了以下輸出操作:

輸出操作 含義
print() 在運行流應用程序的驅動程序節點上打印DStream中每批數據的前是個元素。這對開發和調試很有用。
saveAsTextFiles(prefix,[suffix]) 將此DStream的內容保存爲文本文件。每個批處理間隔的文件名基於前綴和後綴生成:"prefix-TIME_IN_MS[.suffix]"。
saveAsObjectFiles(prefix,[suffix]) 將此DStream的內容保存爲SequenceFiles序列化Java對象。每個批處理間隔的文件名基於前綴和後綴生成:"prefix-TIME_IN_MS[.suffix]"。
saveAsHadoopFiles(prefix,[suffix]) 將此DStream的內容保存爲Hadoop文件。每個批處理間隔的文件名基於前綴和後綴生成:"prefix-TIME_IN_MS[.suffix]"。
foreachRDD(func) 最通用的輸出運算符,它將函數func應用於從流生成的每個RDD。此函數應將每個RDD中的數據推送到外部系統,例如將RDD保存到文件,或通過網絡將其寫入數據庫。請注意,函數func在運行流應用數據的驅動程序進程中執行,並且通常會在其中執行RDD操作,這將強制計算流式RDD。

使用foreachRDD的設計模式

dstream.foreachRDD是一個功能強大的原語,允許將數據發送到外部系統。但是,瞭解如何正確有效地使用此原語非常重要。一些常見地錯誤要避免如下操作。

通常將數據寫入外部系統需要創建連接對象(例如,與遠程服務器地TCP連接)並使用它將數據發送到遠程系統。爲此,開發人員可能無意中嘗試在Spark驅動程序中創建連接對象,然後嘗試在Spark工作程序中使用它來保存RDD中地記錄。

dstream.foreachRDD(rdd -> {
  Connection connection = createNewConnection(); // executed at the driver
  rdd.foreach(record -> {
    connection.send(record); // executed at the worker
  });
});

這是不正確地,因爲這需要連接對象被序列化並從驅動程序發送到worker。這種連接對象很少跨機器傳輸。此錯誤可能表現爲序列化錯誤(連接對象不可序列化),初始化錯誤(需要在worker處初始化連接對象)等。正確地解決方案是在worker出創建連接對象。

但是,這可能會導致另一個常見錯誤:爲每條記錄創建一個新連接。例如:

dstream.foreachRDD(rdd -> {
  rdd.foreach(record -> {
    Connection connection = createNewConnection();
    connection.send(record);
    connection.close();
  });
});

通常,創建連接對象會產生時間和資源開銷。因此,爲每個記錄創建和銷燬連接對象可能會產生不必要地高開銷,並且可能會顯著降低系統地吞吐量。更好地解決方案是使用rdd.foreachPartition:創建單個連接對象並使用該連接發送RDD分區中地所有記錄。

dstream.foreachRDD(rdd -> {
  rdd.foreachPartition(partitionOfRecords -> {
    Connection connection = createNewConnection();
    while (partitionOfRecords.hasNext()) {
      connection.send(partitionOfRecords.next());
    }
    connection.close();
  });
});

這會分攤許多記錄地連接創建開銷。

最後,通過多個RDDs/batches中重用連接對象,可以進一步優化這一點。由於多個批次的RDD被推送到外部系統,因此可以維護連接對象的靜態池,而不是可以重用的連接對象,從而進一步減少了開銷。

dstream.foreachRDD(rdd -> {
  rdd.foreachPartition(partitionOfRecords -> {
    // ConnectionPool is a static, lazily initialized pool of connections
    Connection connection = ConnectionPool.getConnection();
    while (partitionOfRecords.hasNext()) {
      connection.send(partitionOfRecords.next());
    }
    ConnectionPool.returnConnection(connection); // return to the pool for future reuse
  });
});

請注意,池中的連接應根據需要延遲創建,如果暫時不使用,則會超時。這實現了最有效的數據發送到外部系統。

要記住的其他要點:

  • DStream由輸出操作延遲執行,就像RDD由RDD操作延遲執行一樣。具體而言,DStream輸出操作中的RDD操作會強制處理接收到的數據。因此,如果您的應用程序沒有任何輸出操作,或者輸出操作dstream.foreachRDD()沒有任何RDD操作,那麼就不會執行任何操作。系統將簡單地接收數據並將其丟棄。
  • 默認情況下,輸出操作一次執行一次。它們按照應用程序中定義的順序執行。

 

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