實戰之updateStateByKey算子的使用
updateStateByKey操作允許您在使用新信息不斷更新狀態的同時維護任意狀態。要使用它,您需要執行兩個步驟。
1、定義狀態——狀態可以是任意數據類型。
2、定義狀態更新函數——用函數指定如何使用以前的狀態和輸入流中的新值更新狀態。
在每個批處理中,Spark將爲所有現有鍵應用狀態更新功能,而不管它們是否在批處理中有新數據。如果update函數不返回任何值,那麼鍵-值對將被消除。
讓我們用一個例子來說明這一點。
需求:統計到目前爲止累積出現的單詞的個數(需要保持住以前的狀態)
package com.imooc.spark
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 使用Spark Streaming完成有狀態統計
*/
object StatefulWordCount {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("StatefulWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(5))
//java.lang.IllegalArgumentException: requirement failed:
//The checkpoint directory has not been set.
//Please set it by StreamingContext.checkpoint().
// 如果使用了stateful的算子,必須要設置checkpoint
// (目前在當前目錄下)在生產環境中,建議大家把checkpoint設置到HDFS的某個文件夾中
ssc.checkpoint(".")
val lines = ssc.socketTextStream("localhost", 6789)
val result = lines.flatMap(_.split(" ")).map((_,1))
val state = result.updateStateByKey[Int](updateFunction _)
state.print()
ssc.start()
ssc.awaitTermination()
}
/**
* 把當前的數據去更新已有的或者是老的數據
* @param currentValues 當前的
* @param preValues 老的
* @return
*/
def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = {
val current = currentValues.sum
val pre = preValues.getOrElse(0)
Some(current + pre)
}
}
Checkpointing
流應用程序必須全天候運行,因此必須能夠適應與應用程序邏輯無關的故障(例如,系統故障、JVM崩潰等等)。爲了實現這一點,Spark流需要將足夠的信息檢查點到容錯存儲系統,以便能夠從故障中恢復。有兩種類型的數據是檢查點。
- Metadata checkpointing——將定義流計算的信息保存到容錯存儲(如HDFS)。它用於從運行流應用程序驅動程序的節點的故障中恢復(稍後將詳細討論)。元數據包括:
- Configuration——用於創建流應用程序的配置。
- DStream operations——定義流應用程序的DStream操作集。
- Incomplete batches—作業已排隊但尚未完成的批處理。
- Data checkpointing——將生成的RDDs保存到可靠的存儲中。在一些跨多個批處理組合數據的有狀態轉換中,這是必要的。在這種轉換中,生成的rdd依賴於前幾個批次的rdd,這使得依賴鏈的長度隨着時間的推移而不斷增加。爲了避免恢復時間的無界增長(與依賴鏈成比例),有狀態轉換的中間rdd定期檢查可靠存儲(例如HDFS),以切斷依賴鏈。
總之,元數據檢查點主要用於從驅動程序故障中恢復,而數據或RDD檢查點甚至對於使用有狀態轉換的基本功能也是必要的。
何時啓用檢查點
必須爲有下列任何要求的應用程式啓用檢查點:
- 有狀態轉換的使用——如果在應用程序中使用updateStateByKey或reduceByKeyAndWindow(具有逆函數),那麼必須提供檢查點目錄來允許週期性的RDD檢查點。
- 從運行應用程序的驅動程序的故障中恢復——元數據檢查點用於恢復進度信息。
注意,沒有上述有狀態轉換的簡單流應用程序可以在不啓用檢查點的情況下運行。在這種情況下,從驅動程序故障中恢復也是部分的(一些接收到但未處理的數據可能會丟失)。這通常是可以接受的,許多人以這種方式運行Spark流應用程序。對非hadoop環境的支持有望在未來得到改進。
如何配置請參考官網
http://spark.apache.org/docs/2.2.0/streaming-programming-guide.html#how-to-configure-checkpointing
實戰之將統計結果寫入到MySQL數據庫中
需求:計算到目前爲止累積出現的單詞個數寫入到MySQL
添加依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
編寫:
需求:將統計結果寫入到MySQL
create table wordcount(
word varchar(50) default null,
wordcount int(10) default null
);
通過該sql將統計結果寫入到MySQL
insert into wordcount(word, wordcount) values(’" + record._1 + “’,” + record._2 + “)”
package com.imooc.spark
import java.sql.DriverManager
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 使用Spark Streaming完成詞頻統計,並將結果寫入到MySQL數據庫中
*/
object ForeachRDDApp {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("ForeachRDDApp").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(5))
val lines = ssc.socketTextStream("localhost", 6789)
val result = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
//result.print() //此處僅僅是將統計結果輸出到控制檯
//TODO... 將結果寫入到MySQL(以下代碼會報錯)
// 這是不正確的,因爲這需要將連接對象序列化並從驅動程序發送到工作程序。這樣的連接對象很少能夠跨機器進行傳輸。此錯誤可能表現爲序列化錯誤
// result.foreachRDD(rdd =>{
// val connection = createConnection() // executed at the driver
// rdd.foreach { record =>
// val sql = "insert into wordcount(word, wordcount) values('"+record._1 + "'," + record._2 +")"
// connection.createStatement().execute(sql)
// }
// })
result.print()
result.foreachRDD(rdd => {
rdd.foreachPartition(partitionOfRecords => {
val connection = createConnection()
partitionOfRecords.foreach(record => {
val sql = "insert into wordcount(word, wordcount) values('" + record._1 + "'," + record._2 + ")"
connection.createStatement().execute(sql)
})
connection.close()
})
})
ssc.start()
ssc.awaitTermination()
}
/**
* 獲取MySQL的連接
*/
def createConnection() = {
Class.forName("com.mysql.jdbc.Driver")
DriverManager.getConnection("jdbc:mysql://localhost:3306/imooc_spark", "root", "root")
}
}
存在的問題:
-
對於已有的數據做更新,而是所有的數據均爲insert
改進思路:
a) 在插入數據前先判斷單詞是否存在,如果存在就update,不存在則insert
b) 工作中:HBase/Redis -
每個rdd的partition創建connection,建議大家改成連接池
foreachrdd的操作官方詳解:
http://spark.apache.org/docs/2.2.0/streaming-programming-guide.html#design-patterns-for-using-foreachrdd
實戰之窗口函數的使用
window:定時的進行一個時間段內的數據處理
Spark流還提供了窗口計算,允許您在滑動的數據窗口上應用轉換。上圖演示了這個滑動窗口。
如圖所示,每當窗口滑過源DStream時,位於該窗口內的源rdd被組合起來並對其進行操作,從而生成窗口化DStream的rdd。在這個特定的例子中,操作應用於過去3個時間單元的數據,並以2個時間單元進行幻燈片演示。這表明任何窗口操作都需要指定兩個參數。
- window length窗口長度-窗口的持續時間。
- sliding interval滑動間隔-執行窗口操作的間隔。
這兩個參數必須是源DStream批處理間隔的倍數。
讓我們用一個例子來說明窗口操作。例如,您希望通過在最後30秒的數據中每10秒生成單詞計數來擴展前面的示例。爲此,我們必須在過去30秒的數據中對(word, 1)鍵值對的DStream應用reduceByKey操作。這是使用reduceByKeyAndWindow操作完成的。
// Reduce last 窗口長度30 seconds of data, every 滑動間隔10 seconds
//這2個參數和我們的batch size有關係:倍數
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
一些常見的窗口操作如下。所有這些操作都使用上述兩個參數——windowLength和slideInterval
Transformation | Meaning |
---|---|
window(windowLength, slideInterval) | 返回一個新的DStream,它是根據源DStream的窗口批次計算的。 |
countByWindow(windowLength, slideInterval) | 返回流中元素的滑動窗口數。 |
reduceByWindow(func, windowLength, slideInterval) | 返回一個新的單元素流,通過使用func在滑動間隔內聚合流中的元素而創建。該函數應該是關聯的和可交換的,以便可以並行正確計算。 |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | 當在(K,V)對的DStream上調用時,返回(K,V)對的新DStream,其中使用給定的reduce函數func 在滑動窗口中的批次聚合每個鍵的值。注意:默認情況下,這使用Spark的默認並行任務數(本地模式爲2,在羣集模式下,數量由config屬性確定spark.default.parallelism)進行分組。您可以傳遞可選 numTasks參數來設置不同數量的任務。 |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | 上述更有效的版本,reduceByKeyAndWindow()其中使用前一窗口的reduce值逐步計算每個窗口的reduce值。這是通過減少進入滑動窗口的新數據,並“反向減少”離開窗口的舊數據來完成的。一個例子是當窗口滑動時“添加”和“減去”鍵的計數。但是,它僅適用於“可逆減少函數”,即那些具有相應“反向減少”函數的減函數(作爲參數invFunc)。同樣reduceByKeyAndWindow,reduce任務的數量可通過可選參數進行配置。請注意,必須啓用檢查點才能使用此操作。 |
countByValueAndWindow(windowLength, slideInterval, [numTasks]) | 當在(K,V)對的DStream上調用時,返回(K,Long)對的新DStream,其中每個鍵的值是其在滑動窗口內的頻率。同樣 reduceByKeyAndWindow,reduce任務的數量可通過可選參數進行配置。 |
需求:每隔多久計算某個範圍內的數據:每隔10秒計算前30分鐘的wc
==> 每隔sliding interval統計前window length的值
只是算子的不同;其他都是和上面代碼邏輯一樣的。
實戰之黑名單過濾
訪問日誌 ==> DStream
20180808,zs
20180808,ls
20180808,ww
==> (zs: 20180808,zs)(ls: 20180808,ls)(ww: 20180808,ww)
黑名單列表 ==> RDD
zs
ls
==>(zs: true)(ls: true)
==> 20180808,ww
leftjoin
(zs: [<20180808,zs>, ]) x
(ls: [<20180808,ls>, ]) x
(ww: [<20180808,ww>, ]) ==> tuple 1
package com.imooc.spark
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 黑名單過濾
*/
object TransformApp {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
/**
* 創建StreamingContext需要兩個參數:SparkConf和batch interval
*/
val ssc = new StreamingContext(sparkConf, Seconds(5))
/**
* 構建黑名單
*/
val blacks = List("zs", "ls")
val blacksRDD = ssc.sparkContext.parallelize(blacks).map(x => (x, true))
val lines = ssc.socketTextStream("localhost", 6789)
val clicklog = lines.map(x => (x.split(",")(1), x)).transform(rdd => {
rdd.leftOuterJoin(blacksRDD)
.filter(x=> x._2._2.getOrElse(false) != true)
.map(x=>x._2._1)
})
clicklog.print()
ssc.start()
ssc.awaitTermination()
}
}
實戰之Spark Streaming整合Spark SQL操作
您可以輕鬆地在流數據上使用DataFrames和SQL操作。您必須使用StreamingContext使用的SparkContext創建一個SparkSession。此外,這樣做可以在驅動程序失敗時重新啓動。這是通過創建一個延遲實例化的SparkSession單例實例來實現的。如下面的例子所示。它修改了前面的單詞計數示例,以使用DataFrames和SQL生成單詞計數。每個RDD都被轉換爲一個DataFrame,註冊爲一個臨時表,然後使用SQL查詢。
<!-- Spark SQL 依賴-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
package com.imooc.spark
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.spark.streaming.{Seconds, StreamingContext, Time}
/**
* Spark Streaming整合Spark SQL完成詞頻統計操作
*/
object SqlNetworkWordCount {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("ForeachRDDApp").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(5))
val lines = ssc.socketTextStream("localhost", 6789)
val words = lines.flatMap(_.split(" "))
// Convert RDDs of the words DStream to DataFrame and run SQL query
words.foreachRDD { (rdd: RDD[String], time: Time) =>
val spark = SparkSessionSingleton.getInstance(rdd.sparkContext.getConf)
import spark.implicits._
// Convert RDD[String] to RDD[case class] to DataFrame
val wordsDataFrame = rdd.map(w => Record(w)).toDF()
// Creates a temporary view using the DataFrame
wordsDataFrame.createOrReplaceTempView("words")
// Do word count on table using SQL and print it
val wordCountsDataFrame =
spark.sql("select word, count(*) as total from words group by word")
println(s"========= $time =========")
wordCountsDataFrame.show()
}
ssc.start()
ssc.awaitTermination()
}
/** Case class for converting RDD to DataFrame */
case class Record(word: String)
/** Lazily instantiated singleton instance of SparkSession */
object SparkSessionSingleton {
@transient private var instance: SparkSession = _
def getInstance(sparkConf: SparkConf): SparkSession = {
if (instance == null) {
instance = SparkSession
.builder
.config(sparkConf)
.getOrCreate()
}
instance
}
}
}