標籤:
本節課程主要分二個部分:
一、Spark Streaming updateStateByKey案例實戰
二、Spark Streaming updateStateByKey源碼解密
第一部分:
updateStateByKey的主要功能是隨著時間的流逝,在Spark Streaming中可以為每一個可以通過CheckPoint來維護一份state狀態,通過更新函數對該key的狀態不斷更新;對每一個新批次的資料(batch)而言,Spark Streaming通過使用updateStateByKey為已經存在的key進行state的狀態更新(對每個新出現的key,會同樣執行state的更新函數操作);但是如果通過更新函數對state更新後返回none的話,此時刻key對應的state狀態被刪除掉,需要特別說明的是state可以是任意類型的資料結構,這就為我們的計算帶來無限的想象空間;
非常重要:
如果要不斷的更新每個key的state,就一定會涉及到狀態的儲存和容錯,這個時候就需要開啟checkpoint機制和功能,需要說明的是checkpoint的資料可以儲存一些儲存在檔案系統上的內容,例如:程式未處理的但已經擁有狀態的資料。
補充說明:
關於串流對曆史狀態進行儲存和更新具有重大實用意義,例如進行廣告(投放廣告和運營廣告效果評估的價值意義,熱點隨時追蹤、熱力圖)
案例實戰源碼:
1.編寫源碼:
ackage org.apache.spark.examples.streaming;
import java.util.Arrays;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import com.google.common.base.Optional;
import scala.Tuple2;
public class UpdateStateByKeyDemo {
public static void main(String[] args) {
/*
* 第一步:配置SparkConf:
* 1,至少2條線程:因為Spark Streaming應用程式在啟動並執行時候,至少有一條
* 線程用於不斷的迴圈接收資料,並且至少有一條線程用於處理接受的資料(否則的話無法
* 有線程用於處理資料,隨著時間的推移,記憶體和磁碟都會不堪重負);
* 2,對於叢集而言,每個Executor一般肯定不止一個Thread,那對於處理Spark Streaming的
* 應用程式而言,每個Executor一般分配多少Core比較合適?根據我們過去的經驗,5個左右的
* Core是最佳的(一個段子分配為奇數個Core表現最佳,例如3個、5個、7個Core等);
*/
SparkConf conf = new SparkConf().setMaster("local[2]").
setAppName("UpdateStateByKeyDemo");
/*
* 第二步:建立SparkStreamingContext:
* 1,這個是SparkStreaming應用程式所有功能的起始點和程式調度的核心
* SparkStreamingContext的構建可以基於SparkConf參數,也可基於持久化的SparkStreamingContext的內容
* 來恢複過來(典型的情境是Driver崩潰後重新啟動,由於Spark Streaming具有連續7*24小時不間斷啟動並執行特徵,
* 所有需要在Driver重新啟動後繼續上衣系的狀態,此時的狀態恢複需要基於曾經的Checkpoint);
* 2,在一個Spark Streaming應用程式中可以建立若干個SparkStreamingContext對象,使用下一個SparkStreamingContext
* 之前需要把前面正在啟動並執行SparkStreamingContext對象關閉掉,由此,我們獲得一個重大的啟發SparkStreaming架構也只是
* Spark Core上的一個應用程式而已,只不過Spark Streaming架構箱啟動並執行話需要Spark工程師寫商務邏輯處理代碼;
*/
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));
//報錯解決辦法做checkpoint,開啟checkpoint機制,把checkpoint中的資料放在這裡設定的目錄中,
//生產環境下一般放在HDFS中
jsc.checkpoint("/usr/local/tmp/checkpoint");
/*
* 第三步:建立Spark Streaming輸入資料來源input Stream:
* 1,資料輸入來源可以基於File、HDFS、Flume、Kafka、Socket等
* 2, 在這裡我們指定資料來源於網路Socket連接埠,Spark Streaming串連上該連接埠並在啟動並執行時候一直監聽該連接埠
* 的資料(當然該連接埠服務首先必須存在),並且在後續會根據業務需要不斷的有資料產生(當然對於Spark Streaming
* 應用程式的運行而言,有無資料其處理流程都是一樣的);
* 3,如果經常在每間隔5秒鐘沒有資料的話不斷的啟動空的Job其實是會造成調度資源的浪費,因為並沒有資料需要發生計算,所以
* 執行個體的企業級產生環境的代碼在具體提交Job前會判斷是否有資料,如果沒有的話就不再提交Job;
*/
JavaReceiverInputDStream lines = jsc.socketTextStream("hadoop100", 9999);
/*
* 第四步:接下來就像對於RDD編程一樣基於DStream進行編程!!!原因是DStream是RDD產生的模板(或者說類),在Spark Streaming具體
* 發生計算前,其實質是把每個Batch的DStream的操作翻譯成為對RDD的操作!!!
*對初始的DStream進行Transformation層級的處理,例如map、filter等高階函數等的編程,來進行具體的資料計算
* 第4.1步:講每一行的字串拆分成單個的單詞
*/
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() { //如果是Scala,由於SAM轉換,所以可以寫成val words = lines.flatMap { line => line.split(" ")}
@Override
public Iterable<String> call(String line) throws Exception {
return Arrays.asList(line.split(" "));
}
});
/*
* 第四步:對初始的DStream進行Transformation層級的處理,例如map、filter等高階函數等的編程,來進行具體的資料計算
* 第4.2步:在單詞拆分的基礎上對每個單詞執行個體計數為1,也就是word => (word, 1)
*/
JavaPairDStream<String, Integer> pairs = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String word) throws Exception {
return new Tuple2<String, Integer>(word, 1);
}
});
/*
* 第四步:對初始的DStream進行Transformation層級的處理,例如map、filter等高階函數等的編程,來進行具體的資料計算
*第4.3步:在這裡是通過updateStateByKey來以Batch Interval為單位來對曆史狀態進行更新,
* 這是功能上的一個非常大的改進,否則的話需要完成同樣的目的,就可能需要把資料儲存在Redis、
* Tagyon或者HDFS或者HBase或者資料庫中來不斷的完成同樣一個key的State更新,如果你對效能有極為苛刻的要求,
* 且資料量特別大的話,可以考慮把資料放在分布式的Redis或者Tachyon記憶體檔案系統中;
* 當然從Spark1.6.x開始可以嘗試使用mapWithState,Spark2.X後mapWithState應該非常穩定了。
*/
JavaPairDStream<String, Integer> wordsCount = pairs.updateStateByKey(new Function2<List<Integer>, Optional<Integer>, Optional<Integer>>() { //對相同的Key,進行Value的累計(包括Local和Reducer層級同時Reduce)
@Override
public Optional<Integer> call(List<Integer> values, Optional<Integer> state)
throws Exception {
Integer updatedValue = 0 ;
if(state.isPresent()){
updatedValue = state.get();
}
for(Integer value: values){
updatedValue += value;
}
return Optional.of(updatedValue);
}
});
/*
*此處的print並不會直接出發Job的執行,因為現在的一切都是在Spark Streaming架構的控制之下的,對於Spark Streaming
*而言具體是否觸發真正的Job運行是基於設定的Duration時間間隔的
*諸位一定要注意的是Spark Streaming應用程式要想執行具體的Job,對Dtream就必須有output Stream操作,
*output Stream有很多類型的函數觸發,類print、saveAsTextFile、saveAsHadoopFiles等,最為重要的一個
*方法是foraeachRDD,因為Spark Streaming處理的結果一般都會放在Redis、DB、DashBoard等上面,foreachRDD
*主要就是用用來完成這些功能的,而且可以隨意的自訂具體資料到底放在哪裡!!!
*/
wordsCount.print();
/*
* Spark Streaming執行引擎也就是Driver開始運行,Driver啟動的時候是位於一條新的線程中的,當然其內部有訊息迴圈體,用於
* 接受應用程式本身或者Executor中的訊息;
*/
jsc.start();
jsc.awaitTermination();
jsc.close();
}
2.建立checkpoint目錄:
jsc.checkpoint("/usr/local/tmp/checkpoint");
3. 在eclipse中通過run 方法啟動main函數:
4.啟動hdfs服務並發送nc -lk 9999請求:
5.查看checkpoint目錄輸出:
源碼解析:
1.PairDStreamFunctions類:
/**
* 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 of each key.
* Hash partitioning is used to generate the RDDs with Spark‘s default number of partitions.
* @param updateFunc State update function. If `this` function returns None, then
* corresponding state key-value pair will be eliminated.
* @tparam S State type
*/
def updateStateByKey[S: ClassTag](
updateFunc: (Seq[V], Option[S]) => Option[S]
): DStream[(K, S)] = ssc.withScope {
updateStateByKey(updateFunc, defaultPartitioner())
}
/**
* 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 of the key.
* org.apache.spark.Partitioner is used to control the partitioning of each RDD.
* @param updateFunc State update function. If `this` function returns None, then
* corresponding state key-value pair will be eliminated.
* @param partitioner Partitioner for controlling the partitioning of each RDD in the new
* DStream.
* @tparam S State type
*/
def updateStateByKey[S: ClassTag](
updateFunc: (Seq[V], Option[S]) => Option[S],
partitioner: Partitioner
): DStream[(K, S)] = ssc.withScope {
val cleanedUpdateF = sparkContext.clean(updateFunc)
val newUpdateFunc = (iterator: Iterator[(K, Seq[V], Option[S])]) => {
iterator.flatMap(t => cleanedUpdateF(t._2, t._3).map(s => (t._1, s)))
}
updateStateByKey(newUpdateFunc, partitioner, true)
}
/**
* 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 of each key.
* org.apache.spark.Partitioner is used to control the partitioning of each RDD.
* @param updateFunc State update function. Note, that this function may generate a different
* tuple with a different key than the input key. Therefore keys may be removed
* or added in this way. It is up to the developer to decide whether to
* remember the partitioner despite the key being changed.
* @param partitioner Partitioner for controlling the partitioning of each RDD in the new
* DStream
* @param rememberPartitioner Whether to remember the paritioner object in the generated RDDs.
* @tparam S State type
*/
def updateStateByKey[S: ClassTag](
updateFunc: (Iterator[(K, Seq[V], Option[S])]) => Iterator[(K, S)],
partitioner: Partitioner,
rememberPartitioner: Boolean
): DStream[(K, S)] = ssc.withScope {
new StateDStream(self, ssc.sc.clean(updateFunc), partitioner, rememberPartitioner, None)
}
override def compute(validTime: Time): Option[RDD[(K, S)]] = {
// Try to get the previous state RDD
getOrCompute(validTime - slideDuration) match {
case Some(prevStateRDD) => { // If previous state RDD exists
// Try to get the parent RDD
parent.getOrCompute(validTime) match {
case Some(parentRDD) => { // If parent RDD exists, then compute as usual
computeUsingPreviousRDD (parentRDD, prevStateRDD)
}
case None => { // If parent RDD does not exist
// Re-apply the update function to the old state RDD
val updateFuncLocal = updateFunc
val finalFunc = (iterator: Iterator[(K, S)]) => {
val i = iterator.map(t => (t._1, Seq[V](), Option(t._2)))
updateFuncLocal(i)
}
val stateRDD = prevStateRDD.mapPartitions(finalFunc, preservePartitioning)
Some(stateRDD)
}
}
}
case None => { // If previous session RDD does not exist (first input data)
// Try to get the parent RDD
parent.getOrCompute(validTime) match {
case Some(parentRDD) => { // If parent RDD exists, then compute as usual
initialRDD match {
case None => {
// Define the function for the mapPartition operation on grouped RDD;
// first map the grouped tuple to tuples of required type,
// and then apply the update function
val updateFuncLocal = updateFunc
val finalFunc = (iterator : Iterator[(K, Iterable[V])]) => {
updateFuncLocal (iterator.map (tuple => (tuple._1, tuple._2.toSeq, None)))
}
val groupedRDD = parentRDD.groupByKey (partitioner)
val sessionRDD = groupedRDD.mapPartitions (finalFunc, preservePartitioning)
// logDebug("Generating state RDD for time " + validTime + " (first)")
Some (sessionRDD)
}
case Some (initialStateRDD) => {
computeUsingPreviousRDD(parentRDD, initialStateRDD)
}
}
}
case None => { // If parent RDD does not exist, then nothing to do!
// logDebug("Not generating state RDD (no previous state, no parent)")
None
}
}
}
}
總結:
使用Spark Streaming可以處理各種資料來源類型,如:資料庫、HDFS,伺服器log日誌、網路流,其強大超越了你想象不到的情境,只是很多時候大家不會用,其真正原因是對Spark、spark streaming本身不瞭解。
編寫人:IMF-Spark Steaming企業級開發實戰小組(薑偉等)
主編輯:王家林
備忘:
資料來源於:DT_大資料夢工廠(IMF傳奇行動絕密課程)
更多私密內容,請關注公眾號:DT_Spark
如果您對大資料Spark感興趣,可以免費聽由王家林老師每天晚上20:00開設的Spark永久免費公開課,地址YY房間號:68917580
Life is short,you need to Spark!
第93課:Spark Streaming updateStateByKey案例實戰和內幕源碼解密