Spark Streaming
隨著大數(shù)據(jù)技術(shù)的不斷發(fā)展,人們對于大數(shù)據(jù)的實時性處理要求也在不斷提高,傳統(tǒng) 的 MapReduce 等批處理框架在某些特定領(lǐng)域,例如實時用戶推薦、用戶行為分析這 些應(yīng)用場景上逐漸不能滿足人們對實時性的需求,因此誕生了一批如 S3、Samza、 Storm、Flink等流式分析、實時計算框架。
Spark 由于其內(nèi)部優(yōu)秀的調(diào)度機制、快速的分布式計算能力,能夠以極快的速度進行 迭代計算。正是由于具有這樣的優(yōu)勢,Spark 能夠在某些程度上進行實時處理, Spark Streaming 正是構(gòu)建在此之上的流式框架。
Spark Streaming 概述
什么是Spark Streaming
Spark Streaming 類似于Apache Storm(來一條數(shù)據(jù)處理一條,延遲低、響應(yīng)快、吞吐量低),用于流式數(shù)據(jù)處理。Spark Streaming 具有高吞吐量和容錯能力強等特點,支持數(shù)據(jù)源有很多,例如Kafka(最重要的數(shù)據(jù)源)、Flume、Twitter和TCP套接字,數(shù)據(jù)輸入后可用高度抽象API,如map、reduce、join、window等進行運算,處理結(jié)果能保存在很多地方,如HDFS、數(shù)據(jù)庫等,Spark Streaming能與MLlib已經(jīng)Graphx融合
Spark Streaming 與Spark 基于RDD的概念比較類似,Spark Streaming使用離散流(Discretized Stream)作為抽象表示,稱為DStream,DStream是隨著時間推移而收到的數(shù)據(jù)的序列。在內(nèi)部,每個時間區(qū)間收到的數(shù)據(jù)都作為RDD存在,DStream是由這些RDD所組成的序列
DStream可以從各種輸入源創(chuàng)建,比如Flume、Kafka或者HDFS。創(chuàng)建出來的DStream支持兩種操作
- 轉(zhuǎn)化操作,會生成一個新的DStream
- 輸出操作(output operation),把數(shù)據(jù)寫入外部系統(tǒng)
DStream提供了許多與RDD所支持的操作相類似的操作支持,還增加了時間相關(guān)的新操作,比如滑動窗口。
Spark Streaming架構(gòu)
Spark Streaming 使用 mini-batch架構(gòu),把流式計算當作一系列連續(xù)的小規(guī)模批處理來對待,Spark Streaming 從各種輸入源中讀取數(shù)據(jù),并把數(shù)據(jù)分組為小的批次,新的批次按均勻的時間間隔創(chuàng)建處理,在每個時間區(qū)間開始的時候,一個新的批次就創(chuàng)建出來,在該區(qū)間內(nèi)接收到的數(shù)據(jù)都會被添加到這個批次中。在時間區(qū)間結(jié)束時,批次停止增長。時間區(qū)間的大小是有批次間隔這個參數(shù)覺得決定的,批次間隔一般設(shè)在500毫秒到幾秒之間,有開發(fā)者配置。每個輸入批次都形成一個RDD,以 Spark 作業(yè)的方式處理并生成其他的 RDD。 處理的結(jié)果可以以批處理的方式傳給外部系統(tǒng)。
Spark Streaming 的編程抽象時離散化流,也就是DStream,是一個RDD序列,每個RDD代表數(shù)據(jù)流中的一個時間片的內(nèi)的數(shù)據(jù)。
應(yīng)用于DStream上的轉(zhuǎn)換操作,都會轉(zhuǎn)換為底層RDD上的操作,如對行DStream中的每個RDD應(yīng)用flatMap操作以生成單詞DStream的RDD
這些底層的RDD轉(zhuǎn)換是由Spark引擎完成的。DStream操作隱藏了大部分這些細節(jié), 為開發(fā)人員提供了更高級別的API以方便使用。
Spark Streaming為每個輸入源啟動對應(yīng)的接收器。接收器運行在Executor中,從輸入源收集數(shù)據(jù)并保存為 RDD。默認情況下接收到的數(shù)據(jù)后會復(fù)制到另一個Executor中,進行容錯; Driver 中的 StreamingContext 會周期性地運行 Spark 作業(yè)來處理這些數(shù)據(jù)。
Spark Streaming運行流程
客戶端提交Spark Streaming 作業(yè)后啟動Driver,Driver啟動Receiver,Receiver接收數(shù)據(jù)源的數(shù)據(jù)
每個作業(yè)保護多個Executor,每個Executor以線程的方式運行task,Spark Streaming至少包含一個receiver task(一般情況下)
Receiver接收數(shù)據(jù)后生成Block,并把BlockId匯報給Driver,然后備份到另外一個Executor上
ReceiverTracker維護Reciver匯報的BlockId
Driver定時啟動JobGenerator,根據(jù)DStream的關(guān)系生成邏輯RDD,然后創(chuàng)建JobSet,叫個JobScheduler
JobScheduler負責調(diào)度JobSet,交給DAGScheduler,DAGScheduler根據(jù)邏輯RDD,生成相應(yīng)的Stages,每個stage包含一到多個Task,將TaskSet提交給TaskSchedule。
TaskScheduler負責把Task調(diào)度到Executor上,并維護Task運行狀態(tài)。
總結(jié):
- 提交完spark作業(yè)后,driver就會去啟動Receiver取接收數(shù)據(jù),Receiver接收數(shù)據(jù)的同時,會將數(shù)據(jù)備份到另一個節(jié)點,Receiver接收到數(shù)據(jù)會回報給Driver的,然后
Spark Streaming優(yōu)缺點
EOS:exactly onece semantic 處理且僅處理一次
與傳統(tǒng)流式框架相比,Spark Streaming最大的不同點在于它對待數(shù)據(jù)是粗粒度的處理方式,記一次處理一小批數(shù)據(jù),而其他框架往往采用細粒度的處理模式,即依次處理一條數(shù)據(jù),Spark Streaming這樣的設(shè)計即為其帶來了優(yōu)點,也帶來的確定
優(yōu)點
- Spark Streaming 內(nèi)部實現(xiàn)和調(diào)度方式高度依賴Spark的DAG調(diào)度器和RDD,這就決定了Spark Streaming的設(shè)計初衷必須是粗粒度方式的,同時,由于Spark 內(nèi)部調(diào)度器足夠快速和高效,可以快速地處理小批量數(shù)據(jù),這就獲得準實時的特性
- Spark Streaming 的粗粒度執(zhí)行方式使其確保“處理且僅處理一次”的特性(EOS),同時也可以方便地實現(xiàn)容錯回復(fù)機制
- 由于Spark Streaming的DStream本質(zhì)是RDD在流式數(shù)據(jù)上的抽象,因此基于RDD的各種操作也有相應(yīng)的基于DStream的版本,這樣就大大降低了用戶對于新框架的學(xué)習成本,在了解Spark的情況下,用戶將很容易使用Spark Streaming
- 由于DStream是在RDD上抽象,那么也跟容易與RDD進行交互操作,在需要將流式數(shù)據(jù)和批處理數(shù)據(jù)結(jié)合進行分析的情況下,將會變得非常方便
缺點
- Spark Streaming 的粗粒度處理方式也造成了不可避免的延遲。在細粒度處理方式下,理想情況下每一條記錄都會被實時處理,而在Spark Streaming中,數(shù)據(jù)需要匯總到一定的量后在一次性處理,這就增加了數(shù)據(jù)處理的延遲,這種延遲是由框架的設(shè)計引入的,并不是由網(wǎng)絡(luò)或其他情況造成的
Structured Streaming
Spark Streaming計算邏輯是把數(shù)據(jù)按時間劃分為DStream,存在以下問題:
- 框架自身只能根據(jù)Batch Time單元進行數(shù)據(jù)處理,很難處理基于Event time(即時間戳)的數(shù)據(jù),很難處理延遲、亂序的數(shù)據(jù)
- 流式和批量處理的API完全不一致,兩種使用場景中,程序代碼還是需要一定的轉(zhuǎn)換
- 端到端的數(shù)據(jù)容錯保障邏輯需要用戶自己構(gòu)建,難以處理增量更新和持久化存儲等一致性問題
基于以上問題,提出了下一代Structure Streaming。將數(shù)據(jù)映射為一張無界長度的表,通過表的計算,輸出結(jié)果映射為另一張表。
以結(jié)構(gòu)化的方式去操作流式數(shù)據(jù),簡化了實時計算過程,同時還復(fù)用了Catalyst引擎來優(yōu)化SQL操作此外還能支持增量計算和基于event time 的計算。
DStream基礎(chǔ)數(shù)據(jù)源
基礎(chǔ)數(shù)據(jù)源包括:文件數(shù)據(jù)流、socket數(shù)據(jù)流、RDD隊列流;這些數(shù)據(jù)源主要用于測試。
引入依賴:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
文件數(shù)據(jù)流
文件數(shù)據(jù)流:通過textFileStream(directory)方法進行讀取HDFS兼容的文件系統(tǒng)文件
/**
* Create an input stream that monitors a Hadoop-compatible filesystem
* for new files and reads them as text files (using key as LongWritable, value
* as Text and input format as TextInputFormat). Files must be written to the
* monitored directory by "moving" them from another location within the same
* file system. File names starting with . are ignored.
* @param directory HDFS directory to monitor for new file
*/
def textFileStream(directory: String): DStream[String] = withNamedScope("text file stream") {
fileStream[LongWritable, Text, TextInputFormat](directory).map(_._2.toString)
}
Spark Streaming 將會監(jiān)控directory目錄,并不斷處理移動進來的文件
- 不支持嵌套目錄
- 文件需要相同的數(shù)據(jù)格式
- 文件進入directory的方式需要通過移動或者重命名來實現(xiàn)
- 一旦文件移動進目錄,則不能在修改,即使修改了也不會讀取新數(shù)據(jù)
- 文件流不需要接收器(receiver),不需要單獨分配CPU核
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description: 只能監(jiān)控啟動后的新增的文件
* @date: 2020-11-19 10:28
**/
object FileDStream {
def main(args: Array[String]): Unit = {
//1 創(chuàng)建SparkConf
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
//2 初始化入口,設(shè)置任務(wù)5秒鐘執(zhí)行一次
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("warn")
//3、4、5
//3 讀取本地文件,創(chuàng)建DStream
val linesDStream = ssc.textFileStream("/Users/baiwang/myproject/spark/data/log/")
//4 DStream轉(zhuǎn)換
val wordDStream = linesDStream.flatMap(_.split("\\s+"))
val wordCountDStream = wordDStream.map((_, 1)).reduceByKey(_ + _)
//5 輸出DStream到屏幕
wordCountDStream.print()
//6 啟動作業(yè)
ssc.start()
//啟動作業(yè)后,等待,作業(yè)以5秒一次的頻率運行
ssc.awaitTermination()
}
}
Socket數(shù)據(jù)流
Spark Streaming 可以通過Socket端口監(jiān)聽并接受數(shù)據(jù),然后進行相應(yīng)的處理
在linux120上執(zhí)行nc程序
nc -lk 9999
# yum install nc
隨后可以在nc窗口中隨意輸入一下單詞,監(jiān)聽窗口會自動獲得單詞數(shù)據(jù)流信息,在監(jiān)聽窗口每隔x秒就會打印出詞頻的統(tǒng)計信息,可以在屏幕上出現(xiàn)結(jié)果
備注:使用local[*]可能出現(xiàn)問題
如果給虛擬機配置的cpu數(shù)為1,使用local[*]也只會啟動一個線程,該線程用于receiver task,此時沒有資源處理接受打到的數(shù)據(jù)。
【現(xiàn)象:程序正常執(zhí)行,不會打印時間戳,屏幕上也不會有其他有效信息】
源碼:
/**
* Creates an input stream from TCP source hostname:port. Data is received using
* a TCP socket and the receive bytes is interpreted as UTF8 encoded `\n` delimited
* lines.
* @param hostname Hostname to connect to for receiving data
* @param port Port to connect to for receiving data
* @param storageLevel Storage level to use for storing the received objects
* (default: StorageLevel.MEMORY_AND_DISK_SER_2)
* @see [[socketStream]]
*/
def socketTextStream(
hostname: String,
port: Int,
//監(jiān)控到的數(shù)據(jù),默認存內(nèi)存,內(nèi)存存不下放到磁盤,并對數(shù)據(jù)進行備份
storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
): ReceiverInputDStream[String] = withNamedScope("socket text stream") {
socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
}
注意:DStream的 StorageLevel 是 MEMORY_AND_DISK_SER_2;
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-19 15:35
**/
object SocketDStream {
def main(args: Array[String]): Unit = {
//初始化StreamingContext
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(5))
//接受數(shù)據(jù),轉(zhuǎn)換成socketStream
//鏈接服務(wù)器,需要在服務(wù)安裝nc 執(zhí)行命令nc -lk 9999,然后輸入數(shù)據(jù)
// val socketDStream = ssc.socketTextStream("linux120", 9999)
//鏈接本地
val socketDStream = ssc.socketTextStream("localhost", 9999)
//數(shù)據(jù)轉(zhuǎn)換
val wordDStream = socketDStream.flatMap(_.split("\\s+"))
val wordCountDStream = wordDStream.map((_, 1)).reduceByKey(_ + _)
//數(shù)據(jù)輸出
wordCountDStream.print()
//啟動
ssc.start()
ssc.awaitTermination()
}
}
SocketServer程序(單線程),監(jiān)聽本機指定端口,與socket連接后可發(fā)送信息:
package com.hhb.spark.streaming
import java.io.PrintWriter
import java.net.ServerSocket
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-19 16:14
**/
object OneThreadSocket {
def main(args: Array[String]): Unit = {
val arr: Array[String] = "Hello World Hello Hadoop Hello spark kafka hive zookeeper hbase flume sqoop".split("\\s+")
val n = arr.length
val port = 9999
val random = scala.util.Random
val server = new ServerSocket(port)
val socket = server.accept()
println("鏈接成功,鏈接地址:" + socket.getInetAddress)
while (true) {
val writer = new PrintWriter(socket.getOutputStream)
writer.println(arr(random.nextInt(n)) + " " + arr(random.nextInt(n)))
writer.flush()
Thread.sleep(100)
}
}
}
SocketServer程序(多線程)
RDD隊列流
調(diào)試Spark Streaming應(yīng)用程序的時候,可使用streamingContext.queueStream(queueOfRDD) 創(chuàng)建基于RDD隊列的DStream
源碼:
/**
* Create an input stream from a queue of RDDs. In each batch,
* it will process either one or all of the RDDs returned by the queue.
*
* @param queue Queue of RDDs. Modifications to this data structure must be synchronized.
* @param oneAtATime Whether only one RDD should be consumed from the queue in every interval
* @tparam T Type of objects in the RDD
*
* @note Arbitrary RDDs can be added to `queueStream`, there is no way to recover data of
* those RDDs, so `queueStream` doesn't support checkpointing.
*/
def queueStream[T: ClassTag](
queue: Queue[RDD[T]],
//表示一次只處理一個RDD
oneAtATime: Boolean = true
): InputDStream[T] = {
queueStream(queue, oneAtATime, sc.makeRDD(Seq.empty[T], 1))
}
備注:
- oneAtTime :缺省為true,一次處理一個RDD,設(shè)為false,一次處理全部的RDD
- RDD隊列流可以使用local[1]
- 涉及到同時出隊和入隊操作,所以需要同步
每秒創(chuàng)建一個RDD(RDD存放1-100的整數(shù)),Streaming每隔1秒就對數(shù)據(jù)進行處 理,計算RDD中數(shù)據(jù)除10取余的個數(shù)。
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable
/**
* @description:
* @date: 2020-11-19 16:20
**/
object RDDDStream {
def main(args: Array[String]): Unit = {
//初始化ssc,沒個一秒處理一次數(shù)據(jù)
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
//由于RDD是一秒產(chǎn)生一個,所以執(zhí)行實現(xiàn)需要小于一秒,否則產(chǎn)生完RDD后直接停止了,
// 例如設(shè)置10秒,還沒等執(zhí)行,程序已經(jīng)結(jié)束
val ssc = new StreamingContext(conf, Seconds(1))
//組裝RDD的對流
val queue = mutable.Queue[RDD[Int]]()
// 讀取RDD生成DStream
val rddDStream = ssc.queueStream(queue)
//具體的業(yè)務(wù)處理邏輯
val countDStream = rddDStream.map(x => (x % 10, 1)).reduceByKey(_ + _)
//輸出
countDStream.print()
//啟動
ssc.start()
//每秒產(chǎn)生一個RDD
val arr = 1 to 100
for (i <- 1 to 5) {
queue.synchronized {
queue += ssc.sparkContext.makeRDD(arr.map(_ * i))
}
Thread.sleep(1000)
}
ssc.stop()
}
}
DStream 轉(zhuǎn)換操作
DStream上的操作與RDD類似,分為Transformations(轉(zhuǎn)換) 和 Output Operations(輸出)兩種,池外轉(zhuǎn)換操作中還有一些比較特殊的方法,如:updateStateByKey、transform以及各種Window相關(guān)的操作
Transformation | Meaning |
---|---|
map(func) | 將源DStream中的每個元素通過一個函數(shù)func從 而得到新的DStreams |
flatMap(func) | 和map類似,但是每個輸入的項可以被映射為0 或更多項 |
filter(func) | 選擇源DStream中函數(shù)func判為true的記錄作為 新DStreams |
repartition(numPartitions) | 通過創(chuàng)建更多或者更少的partition來改變此 DStream的并行級別 |
union(otherStream) | 聯(lián)合源DStreams和其他DStreams來得到新 DStream |
count() | 統(tǒng)計源DStreams中每個RDD所含元素的個數(shù)得 到單元素RDD的新DStreams |
reduce(func) | 通過函數(shù)func(兩個參數(shù)一個輸出)來整合源 DStreams中每個RDD元素得到單元素RDD的 DStreams。這個函數(shù)需要關(guān)聯(lián)從而可以被并行 計算 |
countByValue() | 對于DStreams中元素類型為K調(diào)用此函數(shù),得到 包含(K,Long)對的新DStream,其中Long值表明 相應(yīng)的K在源DStream中每個RDD出現(xiàn)的頻率 |
reduceByKey(func, [numTasks]) | 對(K,V)對的DStream調(diào)用此函數(shù),返回同樣(K,V) 的新DStream,新DStream中的對應(yīng)V為使用 reduce函數(shù)整合而來。默認情況下,這個操作使 用Spark默認數(shù)量的并行任務(wù)(本地模式為2,集 群模式中的數(shù)量取決于配置參數(shù) spark.default.parallelism)。也可以傳入可選 的參數(shù)numTasks來設(shè)置不同數(shù)量的任務(wù) |
join(otherStream, [numTasks]) | 兩DStream分別為(K,V)和(K,W)對,返回(K,(V,W)) 對的新DStream |
cogroup(otherStream, [numTasks]) | 兩DStream分別為(K,V)和(K,W)對,返回(K, (Seq[V],Seq[W])對新DStreams |
transform(func) | 將RDD到RDD映射的函數(shù)func作用于源DStream 中每個RDD上得到新DStream。這個可用于在 DStream的RDD上做任意操作 |
updateStateByKey(func) | 得到”狀態(tài)”DStream,其中每個key狀態(tài)的更新是 通過將給定函數(shù)用于此key的上一個狀態(tài)和新值 而得到。這個可用于保存每個key值的任意狀態(tài) 數(shù)據(jù) |
備注:
- 在DStream與RDD上的轉(zhuǎn)換操作非常類似(無狀態(tài)操作)
- DStream有自己特殊的操作(窗口操作、追蹤狀態(tài)變化操作)
- 在DStream上的轉(zhuǎn)換操作比RDD上的轉(zhuǎn)換操作少
DStream的轉(zhuǎn)化操作可以分為無狀態(tài)(stateless)和有狀態(tài)(stateful)兩種:
- 無狀態(tài)轉(zhuǎn)換操作,每個批次處理不依賴之前批次的數(shù)據(jù),常見的RDD轉(zhuǎn)化操作,例如map、filter、reduceByKey等
- 有狀態(tài)轉(zhuǎn)化操作。需要使用之前批次的數(shù)據(jù) 或者 中間結(jié)果來計算當前批次的數(shù)據(jù)。有狀態(tài)轉(zhuǎn)化操作包括:基于滑動窗口的轉(zhuǎn)化操作 或 追蹤狀態(tài)變化的轉(zhuǎn)化操作
無狀態(tài)轉(zhuǎn)換
無狀態(tài)轉(zhuǎn)化操作就是把簡單的RDD轉(zhuǎn)化操作應(yīng)用到每個批次上,也就是轉(zhuǎn)化DStream中的每一個RDD。創(chuàng)建的無狀態(tài)轉(zhuǎn)換包括:map、flatMap、filter、repartition、reduceByKey、groupByKey;直接作用在DStream上。重要的轉(zhuǎn)換操作:transform。通過對源DStream的每個RDD應(yīng)用RDD-to-RDD函 數(shù),創(chuàng)建一個新的DStream。支持在新的DStream中做任何RDD操作。
/**
* Create a new DStream in which each RDD is generated by applying a function on RDDs of
* the DStreams.
*/
def transform[T: ClassTag](
dstreams: Seq[DStream[_]],
transformFunc: (Seq[RDD[_]], Time) => RDD[T]
): DStream[T] = withScope {
new TransformedDStream[T](dstreams, sparkContext.clean(transformFunc))
}
這是一個功能強大的函數(shù),它可以允許開發(fā)者直接操作其內(nèi)部的RDD。也就是說開 發(fā)者,可以提供任意一個RDD到RDD的函數(shù),這個函數(shù)在數(shù)據(jù)流每個批次中都被調(diào) 用,生成一個新的流。
示例:黑名單過濾
假設(shè):arr1為黑名單數(shù)據(jù)(自定義),true表示數(shù)據(jù)生效,需要被過濾掉;false表示數(shù)據(jù) 未生效
val arr1 = Array(("spark", true), ("scala", false))
假設(shè):流式數(shù)據(jù)格式為"time word",需要根據(jù)黑名單中的數(shù)據(jù)對流式數(shù)據(jù)執(zhí)行過濾操 作。如"2 spark"要被過濾掉
1 hadoop
2 spark
3 scala
4 java
5 hive
結(jié)果:"2 spark" 被過濾
方法一:使用外鏈接
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.ConstantInputDStream
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:ConstantInputDStream 主要用于流式計算的測試
* @date: 2020-11-20 10:09
**/
object BlackListFilter1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(5))
//準備數(shù)據(jù)
val arr: Array[(String, Int)] = "Hello World Hello Hadoop Hello spark kafka hive zookeeper hbase flume sqoop hello scala".split("\\s+").map(_.toLowerCase).zipWithIndex
val data = arr.map { case (k, v) => v + " " + k }
val rdd = ssc.sparkContext.makeRDD(data)
// 黑名單
val blackList = Array(("spark", true), ("scala", false),("hbase", true),("hello", true))
val blackListRDD = ssc.sparkContext.makeRDD(blackList)
//new ConstantInputDStream[String](ssc, rdd) =》 1 hello
// new ConstantInputDStream[String](ssc, rdd).map(value => (value.split("\\s+")(1), value)) => (hello,(1 hello))
val rddDStream = new ConstantInputDStream[String](ssc, rdd).map(value => (value.split("\\s+")(1), value))
val resultDStream = rddDStream.transform { rdd =>
// 通過leftOuterJoin操作既保留了左側(cè)RDD的所有內(nèi)容,又獲得了內(nèi)容是否在黑名單中
rdd.leftOuterJoin(blackListRDD)
// (hello,(hello 1,option(none)),(spark,(spark 3,option(true)))
.filter { case (_, (_, rightValue)) => rightValue.getOrElse(false) != true }
.map { case (_, (leftValue, _)) => leftValue }
}
resultDStream.print()
// 啟動流式作業(yè)
ssc.start()
ssc.awaitTermination()
}
}
方案二:使用SQL或者DSL
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.streaming.dstream.ConstantInputDStream
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:使用SQL或者DSL
* @author: huanghongbo
* @date: 2020-11-20 10:39
**/
object BlackListFilter2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
//準備數(shù)據(jù)
val arr: Array[(String, Int)] = "Hello World Hello Hadoop Hello spark kafka hive zookeeper hbase flume sqoop hello scala".split("\\s+").map(_.toLowerCase).zipWithIndex
val arrRDD = ssc.sparkContext.makeRDD(arr).map { case (k, v) => v + " " + k }
val constantDStream = new ConstantInputDStream[String](ssc, arrRDD)
// 黑名單
val blackList = Array(("spark", true), ("scala", false), ("hbase", true), ("hello", true))
val blackListRDD = ssc.sparkContext.makeRDD(blackList)
val valueDStream = constantDStream.map(line => (line.split("\\s+")(1), line))
valueDStream.transform { rdd =>
val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
import spark.implicits._
val wordDF = rdd.toDF("word", "line")
val blackListDF = blackListRDD.toDF("word1", "flag")
wordDF.join(blackListDF, $"word" === $"word1", "left")
.filter("flag == false or flag is null")
.select("line")
.rdd
}.print()
ssc.start()
ssc.awaitTermination()
}
}
方案三:直接過濾,沒有shuffle
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.ConstantInputDStream
/**
* @description: 直接過濾,沒有shuffle
* @author: huanghongbo
* @date: 2020-11-20 10:39
**/
object BlackListFilter3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
//準備數(shù)據(jù)
val arr: Array[(String, Int)] = "Hello World Hello Hadoop Hello spark kafka hive zookeeper hbase flume sqoop hello scala".split("\\s+").map(_.toLowerCase).zipWithIndex
val arrRDD = ssc.sparkContext.makeRDD(arr).map { case (k, v) => v + " " + k }
val constantDStream = new ConstantInputDStream[String](ssc, arrRDD)
// 黑名單
val blackList = Array(("spark", true), ("scala", false), ("hbase", true), ("hello", true))
.filter { case (_, v) => v }
.map { case (k, _) => k }
constantDStream.map(line => (line.split("\\s+")(1), line))
.filter { case (word, _) => !blackList.contains(word) }
.map { case (_, line) => line }
.print()
ssc.start()
ssc.awaitTermination()
}
}
有狀態(tài)轉(zhuǎn)換
有狀態(tài)轉(zhuǎn)化的主要有兩種操作:窗口操作、狀態(tài)跟蹤操作
窗口操作
Window Operations 可以設(shè)計窗口大小 和 滑動窗口間隔。來動態(tài)的獲取當前Streaming的狀態(tài)。基于窗口的操作會在一個比StreamingContext和batchDuration(批次間隔)更長的時間范圍內(nèi),通過整合多個批次的結(jié)果,計算整個窗口的結(jié)果
基于窗口的操作需要兩個參數(shù):
- 窗口長度(windowDuration)。控制每次計算最近的多少個批次的數(shù)據(jù)
- 滑動間隔(slideDuration)。用來控制對新的DStream進行計算的間隔
兩者都必須是StreamingContext中批次間隔(batchDuration)的整數(shù)倍
每秒發(fā)送一個數(shù)字:
package com.hhb.spark.streaming
import java.io.PrintWriter
import java.net.ServerSocket
/**
* @description:
* @date: 2020-11-20 14:31
**/
object NumSocket {
def main(args: Array[String]): Unit = {
val server = new ServerSocket(9999)
val socket = server.accept()
var i = 0
println("服務(wù)注冊:" + socket.getInetAddress)
while (true) {
i += 1
val writer = new PrintWriter(socket.getOutputStream)
writer.println(i)
writer.flush()
Thread.sleep(1000)
}
}
}
案例一:
觀察窗口的數(shù)據(jù)
觀察batchDuration、windowDuration、slideDuration三者之間的關(guān)系
使用窗口相關(guān)的操作
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @date: 2020-11-20 14:31
**/
object WindowDemo {
def main(args: Array[String]): Unit = {
//初始化
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
//每5秒生成一個RDD
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("error")
val lines = ssc.socketTextStream("localhost", 9999)
lines.foreachRDD { (rdd, time) =>
println(rdd.id + " time: " + time)
println("*" * 15)
rdd.foreach(x => print(x + "\t"))
}
// 窗口長度為20s,每隔10s滑動一次
lines.reduceByWindow(_ + " " + _, Seconds(20), Seconds(10))
.print()
println("=" * 15)
//對數(shù)據(jù)進行求和
lines.map(_.toInt).window(Seconds(20), Seconds(10)).reduce(_ + _).print()
//對數(shù)據(jù)進行求和
lines.map(_.toInt).reduceByWindow(_ + _, Seconds(20), Seconds(10)).print()
ssc.start()
ssc.awaitTermination()
}
}
案例二:熱點搜索詞實時統(tǒng)計。每隔 10 秒,統(tǒng)計最近20秒的詞出現(xiàn)的次數(shù)
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-20 15:12
**/
object HotWordStats {
def main(args: Array[String]): Unit = {
//初始化StreamingContext
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("error")
// 通過reduceByKeyAndWindow算子, 每隔10秒統(tǒng)計最近20秒的詞出現(xiàn)的次數(shù) ,后 3個參數(shù):窗口時間長度、滑動窗口時間、分區(qū)
val wordDStream = ssc.socketTextStream("localhost", 9999).flatMap(_.split("\\s+")).map((_, 1))
wordDStream.reduceByKeyAndWindow((x: Int, y: Int) => x + y, Seconds(20), Seconds(10)).print()
println("*" * 20)
//設(shè)置檢查點,檢查點具有容錯機制。生產(chǎn)環(huán)境中應(yīng)設(shè)置到HDFS
ssc.checkpoint("data/checkpoint/")
// 這里需要checkpoint的支持
wordDStream.reduceByKeyAndWindow(_ + _, _ - _, Seconds(20), Seconds(10)).print()
ssc.start()
ssc.awaitTermination()
}
}
狀態(tài)追蹤(updateStateByKey)
UpdateStateByKey的主要功能:
- 為Streaming中每一個key維護一份state狀態(tài),state類型可以是任意類型的,可以是自定義對象;更新函數(shù)也可以是自定義的
- 通過更新函數(shù)對該key的狀態(tài)不斷更新,對于每個新的batch而言,Spark Streaming會在使用updateStateKey的時候為已經(jīng)存在的key進行state的狀態(tài)更新
- 使用updateStateByKey時要開啟checkpoint功能
源碼:
/**
* 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.
* In every batch the updateFunc will be called for each state even if there are no new values.
* 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())
}
流式程序啟動后計算wordcount的累計值,將每個批次的結(jié)果保存到文件
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-20 17:56
**/
object StateTracker1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("error")
ssc.checkpoint("data/checkpoint")
//準備數(shù)據(jù)
val wordDStream = ssc.socketTextStream("localhost", 9999).flatMap(_.split("\\s+")).map((_, 1))
// def updateStateByKey[S: ClassTag](updateFunc: (Seq[V]value的類型, Option[S]) => Option[S] 返回值
//定義狀態(tài)更新函數(shù)
//函數(shù)常量定義,返回值類型是Some(Int),表示的含義是最新的狀態(tài)
//函數(shù)的功能是將當前時間間隔產(chǎn)生的key的value的集合,加到上一個狀態(tài),得到最新狀態(tài)
val updateFunc = (currVale: Seq[Int], preValue: Option[Int]) => {
//通過Spark內(nèi)部的reduceByKey按key規(guī)約,然后這里傳入某key當前批次的Seq,在計算當前批次的總和
val currSum = currVale.sum
//以前已經(jīng)累加的值
val preSum = preValue.getOrElse(0)
Option(currSum + preSum)
}
val resultDStream = wordDStream.updateStateByKey[Int](updateFunc)
resultDStream.cache()
resultDStream.print()
// 把DStream保存到文本文件中,會生成很多的小文件。一個批次生成一個目錄
resultDStream.repartition(1).saveAsTextFiles("data/output1/")
ssc.start()
ssc.awaitTermination()
}
}
統(tǒng)計全局的key的狀態(tài),但是就算沒有數(shù)據(jù)輸入,也會在每一個批次的時候返回之前的key的狀態(tài)。這樣的確定:如果數(shù)據(jù)量很大的話,checkpoint數(shù)據(jù)會占用較大的存儲,而且效率也不高。
mapWithState:也是用于全局統(tǒng)計key的狀態(tài),如果沒有數(shù)據(jù)輸入,便不會返回之前key的狀態(tài),有一點增量的感覺,這樣做的好處是,只關(guān)心那些已經(jīng)發(fā)生變化的key,對于沒有數(shù)據(jù)輸入,則不會返回那些沒有變化的key的數(shù)據(jù),即使數(shù)據(jù)量很大,checkpoint也不會像updateStateByKey那樣,占用太多的存儲
package com.hhb.spark.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-20 17:56
**/
object StateTracker2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("error")
ssc.checkpoint("data/checkpoint")
//準備數(shù)據(jù)
val wordDStream = ssc.socketTextStream("localhost", 9999).flatMap(_.split("\\s+")).map((_, 1))
//(KeyType, Option[ValueType], State[StateType]) => MappedType
//(key的類型,輸入的數(shù)據(jù),中間匯總)=> 返回值類型
def func(key: String, option: Option[Int], state: State[Int]): (String, Int) = {
val sum = option.getOrElse(0) + state.getOption().getOrElse(0)
state.update(sum)
(key, sum)
}
val spec = StateSpec.function(func _)
//只統(tǒng)計本批次出現(xiàn)的數(shù)據(jù),不會把所有數(shù)據(jù)匯總
val resultDStream = wordDStream.mapWithState(spec)
//.stateSnapshots() 以快照的形式,把所有的數(shù)據(jù)都顯示出來,結(jié)果類似于updateStateByKey
.stateSnapshots()
resultDStream.cache()
resultDStream.print()
// 把DStream保存到文本文件中,會生成很多的小文件。一個批次生成一個目錄
resultDStream.repartition(1).saveAsTextFiles("data/output2/")
ssc.start()
ssc.awaitTermination()
}
}
總結(jié):mapWithState按照時間線在每一個批次間隔返回之前的發(fā)生改變的或者新的key的狀態(tài),不發(fā)生變化的不返回;updateStateByKey統(tǒng)計的是全局Key的狀態(tài),就算沒有數(shù)據(jù)輸入也會在每個批次的時候返回之前的Key的狀態(tài)。也就是說,mapWithState統(tǒng)計的是一個批次的數(shù)據(jù),updateStateByKey統(tǒng)計的是全局的key
DStream 輸出操作
輸出操作定義 DStream 的輸出操作。與 RDD 中的惰性求值類似,如果一個DStream及其派生出的 DStream 都沒有被執(zhí)行輸出操作,那么這些 DStream 就都不會被求值。如果 StreamingContext 中沒有設(shè)定輸出操作,整個流式作業(yè)不會啟動。
Output Operation | Meaning |
---|---|
print() | 在運行流程序的Driver上,輸出DStream中每一批次數(shù)據(jù)最開始的10個元素,用于開發(fā)和調(diào)試 |
saveAsTestFile(prefix,[suffix]) | 以text文件形成存儲DStream的內(nèi)容,每一批次的存儲文件名基于參數(shù)中的prefix和suffix |
saveAsObjectFiles(prefix,[suffix]) | 以 Java 對象序列化的方式將Stream中的數(shù)據(jù)保 存為 Sequence Files。每一批次的存儲文件名基 于參數(shù)中的為"prefix-TIME_IN_MS[.suffix]" |
saveAsHadoopFiles(prefix, [suffix]) | 將Stream中的數(shù)據(jù)保存為 Hadoop files。每一批 次的存儲文件名基于參數(shù)中的為"prefix- TIME_IN_MS[.suffix]" |
foreachRDD(func) | 最通用的輸出操作。將函數(shù) func 應(yīng)用于 DStream 的每一個RDD上 |
通用的輸出操作foreachRDD,用來對DStream中的RDD進行任意計算,在foreachRDD中,可以重用Spark RDD 中所有的Action操作,需要注意的是:
- 鏈接不要定義在Driver
- 鏈接定義在RDD的foreach算子中,則遍歷RDD的每個元素時都創(chuàng)建鏈接,得不償失
- 應(yīng)該在RDD的foreachPartition中定義鏈接,每個分區(qū)創(chuàng)建一個鏈接
- 可以考慮使用連接池
與Kafka整合
針對不同的spark、kafka版本,集成處理數(shù)據(jù)的方式分為兩種:Receiver Approach 和Direct Approach,不同集成版本處理方式的支持,可參考下圖:
對Kafka的支持分為兩個版本08(在高版本中將被廢棄)、010,兩個版本不兼容。
Kafka-08接口
Receiver based Approach
基于Receiver的方式使用kafka舊版消費者高階API實現(xiàn)。對于所有的Receiver,通過Kafka接收的數(shù)據(jù)被存儲于Spark的Executors上,底層是寫入BlockManager中,默認200ms生成一個block(spark.streaming.blockInterval)。然后由spark Streaming提交的job構(gòu)建BlockRDD,最終以Spark Core任務(wù)的形式運行,對應(yīng)的Receiver方式,有以下幾點需要注意:
- Receiver 作為一個常駐線程調(diào)度到Executor上運行,占用一個CPU
- Receiver個數(shù)由KafkaUtil.createStream調(diào)用次數(shù)來決定,一次一個Receiver。
- Kafka中的topic分區(qū)并不能關(guān)聯(lián)產(chǎn)生在spark streaming中的rdd分區(qū),增加在KafkaUtils.createStream()中的指定的topic分區(qū)數(shù),僅僅增加了單個receiver消費的topic線程數(shù),他不會正價處理數(shù)據(jù)中并行的Spark數(shù)量【即:topicMap[topic,num_threads]中,value對應(yīng)的數(shù)值應(yīng)該是每個topic對應(yīng)的消費線程數(shù)】
- receiver默認200ms生成一個block,可根據(jù)數(shù)據(jù)量大小調(diào)整block生成周期,一個block對應(yīng)RDD分區(qū)。
- receiver接收的數(shù)據(jù)會放入到BlockManager,每個Executor都會有一個BlockManager實例,由于數(shù)據(jù)本地性,那些存在Receiver的Executor會被調(diào)度執(zhí)行更多的Task,就會導(dǎo)致某些executor比較空閑
- 默認情況下,Receiver是可能丟失數(shù)據(jù)的,可以通過設(shè)置spark.streaming.receiver.writeAheadLog.enable為true開啟預(yù)寫日志機制,將數(shù)據(jù)先寫入到一個可靠的分布式文件系統(tǒng)(如HDFS),確保數(shù)據(jù)不丟失,但會損失一定性能。
Kafka-08接口(Receiver方式):
- Offset保持在ZK中,系統(tǒng)管理
- 對應(yīng)kafka的版本0.8.2.1+
- 接口底層實現(xiàn)使用Kafka舊版消費者高階API
- DStream底層實現(xiàn)為BlockRDD
Kafka-08接口(Receiver with WAL)
- 增強了故障恢復(fù)的能力
- 接收的數(shù)據(jù)與Dirver的元數(shù)據(jù)保存到HDFS
- 增加了流式應(yīng)用處理的延遲
Direct Approach
Direct Approach是 Spark Streaming不使用Receiver集成kafka的方式,在企業(yè)生產(chǎn)環(huán)境中使用較多。相較于Receiver,有以下特點:
不使用Receiver。減少不必要的CPU占用,減少了Receiver接收數(shù)據(jù)寫入BlockManager,然后運行時再通過BlockId、網(wǎng)絡(luò)傳輸、磁盤讀取等來獲取數(shù)據(jù)的整個過程,提高了效率;無需WAL,進一步減少磁盤IO
-
Direct方式生成了RDD時KafkaRDD,他的分區(qū)數(shù)與Kafka分區(qū)數(shù)保存一致,便于控制并行度
注意:在Shuffle或Repartition操作后生成的RDD,這種對應(yīng)關(guān)系會失效
-
可以手動維護offset,實現(xiàn)Exactly once 語義(處理僅處理一次)
Direct Approach.png
Kafka-010接口
Spark Streaming與kafka 0.10的整合,和0.8版本的 Direct 方式很像。Kafka的分區(qū) 和Spark的RDD分區(qū)是一一對應(yīng)的,可以獲取 offsets 和元數(shù)據(jù),API 使用起來沒有 顯著的區(qū)別。
添加依賴:
<!-- sparkStreaming 結(jié)合 kafka -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
不要手動添加 org.apache.kafka 相關(guān)的依賴,如kafka-clients。spark-streaming- kafka-0-10已經(jīng)包含相關(guān)的依賴了,不同的版本會有不同程度的不兼容。
使用kafka010接口從Kafka中獲取數(shù)據(jù)
- Kafka集群
- Kakfa生產(chǎn)者發(fā)送數(shù)據(jù)
- Spark Streaming程序接收數(shù)據(jù)
package com.hhb.spark.streaming.kafka
import java.util.Properties
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord}
import org.apache.kafka.common.serialization.StringSerializer
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-23 11:08
**/
object KafkaProducer {
def main(args: Array[String]): Unit = {
val brokers = "linux121:9092,linux122:9092,linux123:9092"
val topic = "topicA"
val prop = new Properties()
prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[org.apache.kafka.common.serialization.StringSerializer])
prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[org.apache.kafka.common.serialization.StringSerializer])
// 創(chuàng)建生產(chǎn)者
val producer = new KafkaProducer[String, String](prop)
//向生產(chǎn)者里面發(fā)送信息
for (i <- 0 to 1000000) {
val record = new ProducerRecord[String, String](topic, i.toString, i.toString)
producer.send(record)
println(s"i -> $i")
Thread.sleep(100)
}
producer.close()
}
}
消費:
package com.hhb.spark.streaming.kafka
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-23 12:35
**/
object KafkaDStream1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
val topics = Array("topicA")
val groupId = "groupId"
//定義Kafka相關(guān)參數(shù)
val param: Map[String, Object] = getKafkaParam(groupId)
val kafkaDStream = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, param)
)
kafkaDStream.foreachRDD((rdd, time) =>
println(s"rdd.count=>${rdd.count()},time=> ${time}")
)
ssc.start()
ssc.awaitTermination()
}
/**
* 定義Kafka相關(guān)參數(shù)
*
* @return
*/
def getKafkaParam(groupId: String): Map[String, Object] = {
Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "linux121:9092,linux122:9092,linux123:9092",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[org.apache.kafka.common.serialization.StringDeserializer],
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[org.apache.kafka.common.serialization.StringDeserializer],
ConsumerConfig.GROUP_ID_CONFIG -> groupId,
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG -> "earliest",
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG -> (false: java.lang.Boolean)
)
}
}
LocationStrategies(本地策略)
- LocationStrategies.PreferBrokers :如果Executor在Kafka集群中的某些節(jié)點上,可以使用這種策略,那么Executor中的數(shù)據(jù)都會來之當前broker節(jié)點
- LocationStrategies.PreferConsistent: 大多數(shù)情況下使用的策略,將Kafka分區(qū)均勻分布在Spark集群的Executor上
- LocationStrategies.PreferFixed:如果節(jié)點之間的分區(qū)明顯分布不均,使用這種策略。通過一個Map指定將topic分區(qū)分布在哪些節(jié)點中
ConsumerStrategies(消費策略)
- ConsumerStrategies.Subscribe,用來訂閱一組Topic
- ConsumerStrategies.SubscribePattern,使用正則來指定感興趣的topic
- ConsumerStrategies.Assign,指定固定分區(qū)的集合
這三種策略都有重載構(gòu)造函數(shù),允許指定特定分區(qū)的起始偏移量;使用 Subscribe 或 SubscribePattern 在運行時能實現(xiàn)分區(qū)自動發(fā)現(xiàn)。
kafka相關(guān)命令:
# 創(chuàng)建Topic
kafka-topics.sh --create --zookeeper localhost:2181/myKafka --topic topicA --partitions 3 --replication-factor 3
# 顯示topic信息
kafka-topics.sh --zookeeper localhost:2181/myKafka --topic topicA --describe
# 檢查 topic 的最大offset
kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list linux121:9092,linux122:9092,linux123:9092 --topic topicA --time -1
# 檢查消費者的offset
kafka-consumer-groups.sh --bootstrap-server linux121:9092 --describe --group groupId
# 重置消費者offset
kafka-consumer-groups.sh --bootstrap-server linux121:9092,linux122:9092,linux123:9092 --group groupId --reset-offsets --execute --to-offset 0 --topic topicA
Offset管理
Spark Streaming集成Kafka,允許Kafka中讀取一個或者多個topic數(shù)據(jù),一個Kafka Topic包含一個或多個分區(qū),每個分區(qū)中的消息順序存儲,并使用offset來標記消息的位置,開發(fā)者可以在SparkStreaming應(yīng)用中通過offset來控制數(shù)據(jù)的讀取位置。
Offsets管理對于保證流式應(yīng)用在整個生命周期中數(shù)據(jù)的連貫性是非常重要的,如果在應(yīng)用停止或報錯退出之前沒有將offset持久化保存,該信息就會丟失,那么Spark Streaming就沒有辦法從上次停止或報錯的位置繼續(xù)消費Kafka中的消息。
獲取偏移量(Obtaining Offsets)
Spark Streaming與kafka整合時,允許獲取其消費的 offset ,具體方法如下:
package com.hhb.spark.streaming.kafka
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-23 12:35
**/
object KafkaDStream1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
val topics = Array("topicA")
val groupId = "groupId"
//定義Kafka相關(guān)參數(shù)
val param: Map[String, Object] = getKafkaParam(groupId)
val kafkaDStream = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, param)
)
//topic:topicA,partition:1,from: 0,to:90
//topic:topicA,partition:0,from: 0,to:122
//topic:topicA,partition:2,from: 0,to:98
//topic:topicA,partition:1,from: 90,to:90
//topic:topicA,partition:0,from: 122,to:122
//topic:topicA,partition:2,from: 98,to:98
kafkaDStream.foreachRDD { rdd =>
val ranges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
ranges.foreach { range =>
println(s"topic:${range.topic},partition:${range.partition},from: ${range.fromOffset},to:${range.untilOffset}")
}
}
// kafkaDStream.foreachRDD((rdd, time) =>
// println(s"rdd.count=>${rdd.count()},time=> ${time}")
// )
ssc.start()
ssc.awaitTermination()
}
/**
* 定義Kafka相關(guān)參數(shù)
*
* @return
*/
def getKafkaParam(groupId: String): Map[String, Object] = {
Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "linux121:9092,linux122:9092,linux123:9092",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[org.apache.kafka.common.serialization.StringDeserializer],
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[org.apache.kafka.common.serialization.StringDeserializer],
ConsumerConfig.GROUP_ID_CONFIG -> groupId,
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG -> "earliest",
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG -> (false: java.lang.Boolean)
)
}
}
注意:對HasOffsetRanges的類型轉(zhuǎn)換只有在對createDirectStream調(diào)用的第一個方法中完成才會成功,而不是在隨后的方法鏈中。RDD分區(qū)和Kafka分區(qū)之間的對應(yīng)關(guān)系在shuffle或重分區(qū)后丟失,如reduceByKey或Window。
存儲偏移量(Storing Offsets)
在Streaming 程序失敗的情況下,kafka交付語義取決于如何以及何時存儲偏移量。Spark 輸出操作的語義為 at-least-once
如果要失效EOS語義(Exactly Once Semantics),必須在冪等的輸出之后存儲偏移量或者將存儲偏移量與輸出放在一個事務(wù)中,可以安裝增加可靠性(和代碼復(fù)雜度)的順序使用一下選項來存儲偏移量
-
CheckPoint
CheckPoint是對Spark Steaming運行過程中的元數(shù)據(jù)和每個RDDs的數(shù)據(jù)狀態(tài)保存到一個持久化系統(tǒng)中,這里也包含了offset,一般是HDFS、S3,如果應(yīng)用程序或者集群掛了,可以迅速恢復(fù),如果Streamging 程序的代碼變了,重新打包執(zhí)行就會出翔反序列化異常的問題。這是因為CheckPoint首次持久化時會講整個jar序列化,以便重啟時恢復(fù),重新打包之后,新舊代碼邏輯不同,就會報錯或仍然執(zhí)行舊版代碼。要解決這個問題,只能將HDFS上的checkpoint文件刪除,但是這樣也會刪除Kafka的offset信息
-
Kafka
默認情況下,消費者定期自動提交偏移量,它將偏移量存儲在一個特殊的Kafka主題中(__consumer_offsets)。但在某些情況下,這將導(dǎo)致問題,因為消息可能已經(jīng) 被消費者從Kafka拉去出來,但是還沒被處理。可以將 enable.auto.commit 設(shè)置為 false ,在 Streaming 程序輸出結(jié)果之后,手動 提交偏移到kafka。與檢查點相比,使用Kafka保存偏移量的優(yōu)點是無論應(yīng)用程序代碼如何更改,偏移量 仍然有效。
stream.foreachRDD { rdd => val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges // 在輸出操作完成之后,手工提交偏移量;此時將偏移量提交到 Kafka 的消息隊列中 stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) }
與HasOffsetRanges一樣,只有在createDirectStream的結(jié)果上調(diào)用時,轉(zhuǎn)換到 CanCommitOffsets才會成功,而不是在轉(zhuǎn)換之后。commitAsync調(diào)用是線程安全 的,但必須在輸出之后執(zhí)行。
-
自定義存儲
Offsets可以通過多種方式來管理,但是一般來說遵循下面的步驟:
- 在 DStream 初始化的時候,需要指定每個分區(qū)的offset用于從指定位置讀取數(shù)據(jù)
- 讀取并處理消息
- 處理完之后存儲結(jié)果數(shù)據(jù)
- 用虛線圈存儲和提交offset,強調(diào)用戶可能會執(zhí)行一系列操作來滿足他們更加嚴格的語義要求。這包括冪等操作和通過原子操作的方式存儲offset
- 將 offsets 保存在外部持久化數(shù)據(jù)庫如 HBase、Kafka、HDFS、ZooKeeper、 Redis、MySQL ... ...
可以將 Offsets 存儲到HDFS中,但這并不是一個好的方案。因為HDFS延遲有點高, 此外將每批次數(shù)據(jù)的offset存儲到HDFS中還會帶來小文件問題;
可以將 Offset 存儲到保存ZK中,但是將ZK作為存儲用,也并不是一個明智的選擇, 同時ZK也不適合頻繁的讀寫操作;
Redis管理的Offset
要想將Offset保存到外部存儲中,關(guān)鍵要實現(xiàn)以下幾個功能:
- Streaming程序啟動時,從外部存儲獲取保存的Offsets(執(zhí)行一次)
- 在foreachRDD中,每個批次數(shù)據(jù)處理之后,更新外部存儲的offsets(多次執(zhí)行)
案例一:使用自定義的offsets,從kafka讀數(shù)據(jù);處理完數(shù)據(jù)后打印offsets
package com.hhb.spark.streaming.kafka
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @description: 使用自定義offset,從kafka讀數(shù)據(jù),處理完數(shù)據(jù)后打印offsets
* @date: 2020-11-23 16:07
**/
object KafkaDStream2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
val topics = Array("topicA")
val groupId = "groupId"
val param: Map[String, Object] = getKafkaMap(groupId)
val offsets: Map[TopicPartition, Long] = Map(
new TopicPartition(topics(0), 0) -> 50,
new TopicPartition(topics(0), 1) -> 60,
new TopicPartition(topics(0), 2) -> 80,
)
val kafkaDStream = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, param, offsets)
)
kafkaDStream.foreachRDD { (rdd, times) =>
println(s"rdd.count = ${rdd.count()},time=>${times}")
val ranges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
ranges.foreach { range =>
println(s"partition=>${range.partition},topic => ${range.topic},from=> ${range.fromOffset} ,to=>${range.untilOffset}")
}
}
ssc.start()
ssc.awaitTermination()
}
def getKafkaMap(groupId: String): Map[String, Object] = {
Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "linux121:9092,linux122:9092,linux123:9092",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
ConsumerConfig.GROUP_ID_CONFIG -> groupId,
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG -> (false: java.lang.Boolean)
)
}
}
案例二:根據(jù) key 從 Redis 獲取offsets,根據(jù)該offsets從kafka讀數(shù)據(jù);處理完數(shù)據(jù) 后將offsets保存到 Redis
Redis管理的Offsets:
1、數(shù)據(jù)結(jié)構(gòu)選擇:Hash;key、field、value
Key:kafka:topic:TopicName:groupid
Field:partition
Value:offset
2、從 Redis 中獲取保存的offsets
3、消費數(shù)據(jù)后將offsets保存到redis
工具類(Redis讀取、保存offset)
package com.hhb.spark.streaming.kafka
import java.util
import java.util.{HashSet, Set}
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
import redis.clients.jedis.{HostAndPort, Jedis, JedisCluster, JedisPool, JedisPoolConfig}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-23 16:59
**/
object OffsetsRedisUtils {
private val config = new JedisPoolConfig
val jedisClusterNode: util.Set[HostAndPort] = new util.HashSet[HostAndPort]
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7001))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7002))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7003))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7004))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7005))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7006))
jedisClusterNode.add(new HostAndPort("59.110.241.53", 7007))
//最大鏈接
config.setMaxTotal(30)
//最大空閑
config.setMaxIdle(10)
private val topicKeyPrefix = "kafka:topic"
private def getJcd() = new JedisCluster(jedisClusterNode, config)
//獲取key信息
private def getKey(topic: String, groupId: String): String = {
topicKeyPrefix + ":" + topic + ":" + groupId
}
def getOffset(topics: Array[String], groupId: String): Map[TopicPartition, Long] = {
val cluster = getJcd()
val result = topics.map { topic =>
import scala.collection.JavaConverters._
//獲取該topic、groupId下所有的信息
cluster.hgetAll(getKey(topic, groupId))
//轉(zhuǎn)換成scala
.asScala
//再次遍歷,將分區(qū),offset 轉(zhuǎn)換成(new TopicPartition(topic, k.toInt), v.toLong)結(jié)構(gòu)
.map { case (k, v) => (new TopicPartition(topic, k.toInt), v.toLong) }
}
cluster.close()
//壓平數(shù)據(jù)后轉(zhuǎn)map
result.flatten.toMap
}
//自己的方法
def saveOffset(ranges: Array[OffsetRange], groupId: String): Unit = {
val cluster = getJcd()
ranges.foreach { range =>
val key = getKey(range.topic, groupId)
println(s"key=>${key},part=>${range.partition.toString},offset=>${range.untilOffset.toString}")
cluster.hset(key, range.partition.toString, range.untilOffset.toString)
}
cluster.close()
}
//老師的方法
def saveOffset2(ranges: Array[OffsetRange], groupId: String): Unit = {
val cluster = getJcd()
val result: Map[String, Array[(String, String)]] = ranges.map(range => (range.topic, (range.partition.toString -> range.untilOffset.toString)))
.groupBy(_._1)
.map { case (topic, buffer) => (topic, buffer.map(_._2)) }
result.map { r =>
import scala.collection.JavaConverters._
val offsets = r._2.toMap.asJava
cluster.hmset(getKey(r._1, groupId), offsets)
}
}
}
KafkaDStream(從kafka獲取數(shù)據(jù),使用 Redis 保存offsets)
package com.hhb.spark.streaming.kafka
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
/**
* @description:
* @author: huanghongbo
* @date: 2020-11-23 16:59
**/
object KafkaDStream3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("warn")
val topics = Array("topicA")
val groupId = "groupId"
val param: Map[String, Object] = getKafkaMap(groupId)
val offsets: Map[TopicPartition, Long] = OffsetsRedisUtils.getOffset(topics, groupId)
val kafkaDStream = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, param, offsets)
)
kafkaDStream.foreachRDD { (rdd, times) =>
if(!rdd.isEmpty()){
//數(shù)據(jù)處理邏輯,這里只是輸出
println(s"rdd.count = ${rdd.count()},time=>${times}")
val ranges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
OffsetsRedisUtils.saveOffset(ranges, groupId)
}
}
ssc.start()
ssc.awaitTermination()
}
def getKafkaMap(groupId: String): Map[String, Object] = {
Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "linux121:9092,linux122:9092,linux123:9092",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
ConsumerConfig.GROUP_ID_CONFIG -> groupId,
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG -> (false: java.lang.Boolean)
)
}
}