1. Spark Streaming概述
1.1 什么是Spark Streaming
Spark Streaming類似于Apache Storm,用于流式數據的處理。根據其官方文檔介紹,Spark Streaming有高吞吐量和容錯能力強等特點。Spark Streaming支持的數據輸入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和簡單的TCP套接字等等。數據輸入后可以用Spark的高度抽象語言的語法如:map、reduce、join、window等進行運算。而結果也能保存在很多地方,如HDFS,數據庫等。另外Spark Streaming也能和MLlib(機器學習)以及Graphx完美融合。
和Spark基于RDD的概念很相似,Spark Streaming使用離散化流(discretized stream)作為抽象表示,叫作DStream。DStream 是隨時間推移而收到的數據的序列。在內部,每個時間區間收到的數據都作為 RDD 存在,而 DStream 是由這些 RDD 所組成的序列(因此 得名“離散化”)。?
DStream 可以從各種輸入源創建,比如 Flume、Kafka 或者 HDFS。創建出來的DStream 支持兩種操作,一種是轉化操作(transformation),會生成一個新的DStream,另一種是輸出操作(output operation),可以把數據寫入外部系統中。DStream 提供了許多與 RDD 所支持的操作相類似的操作支持,還增加了與時間相關的新操作,比如滑動窗口。
1.2 Spark Streaming的特點
易用?
容錯?
易整合到Spark體系?
1.3 Spark 與 Storm 對比
1.3.1 對比
對比點StormSpark Streaming
實時計算模型純實時,來一條數據,處理一條數據準實時,對一個時間段內的數據收集起來,作為一個RDD,再處理
實時計算延遲度毫秒級秒級
吞吐量低高
事務機制支持完善支持,但不夠完善
健壯性 / 容錯性ZooKeeper,Acker,非常強Checkpoint,WAL,一般
動態調整并行度支持不支持
1.3.2 Spark Streaming與Storm的應用場景
Storm
建議在那種需要純實時,不能忍受1秒以上延遲的場景下使用,比如實時金融系統,要求純實時進行金融交易和分析
此外,如果對于實時計算的功能中,要求可靠的事務機制和可靠性機制,即數據的處理完全精準,一條也不能多,一條也不能少,也可以考慮使用Storm
如果還需要針對高峰低峰時間段,動態調整實時計算程序的并行度,以最大限度利用集群資源(通常是在小型公司,集群資源緊張的情況),也可以考慮用Storm
如果一個大數據應用系統,它就是純粹的實時計算,不需要在中間執行SQL交互式查詢、復雜的transformation算子等,那么用Storm是比較好的選擇
Spark Streaming
如果對上述適用于Storm的三點,一條都不滿足的實時場景,即,不要求純實時,不要求強大可靠的事務機制,不要求動態調整并行度,那么可以考慮使用Spark Streaming
考慮使用Spark Streaming最主要的一個因素,應該是針對整個項目進行宏觀的考慮,即,如果一個項目除了實時計算之外,還包括了離線批處理、交互式查詢等業務功能,而且實時計算中,可能還會牽扯到高延遲批處理、交互式查詢等功能,那么就應該首選Spark生態,用Spark Core開發離線批處理,用Spark SQL開發交互式查詢,用Spark Streaming開發實時計算,三者可以無縫整合,給系統提供非常高的可擴展性
1.3.3 Spark Streaming與Storm的優劣分析
事實上,Spark Streaming絕對談不上比Storm優秀。這兩個框架在實時計算領域中,都很優秀,只是擅長的細分場景并不相同。?
Spark Streaming僅僅在吞吐量上比Storm要優秀,而吞吐量這一點,也是歷來挺Spark Streaming,貶Storm的人著重強調的。但是問題是,是不是在所有的實時計算場景下,都那么注重吞吐量?不盡然。因此,通過吞吐量說Spark Streaming強于Storm,不靠譜。?
事實上,Storm在實時延遲度上,比Spark Streaming就好多了,前者是純實時,后者是準實時。而且,Storm的事務機制、健壯性 / 容錯性、動態調整并行度等特性,都要比Spark Streaming更加優秀。?
Spark Streaming,有一點是Storm絕對比不上的,就是:它位于Spark生態技術棧中,因此Spark Streaming可以和Spark Core、Spark SQL無縫整合,也就意味著,我們可以對實時處理出來的中間數據,立即在程序中無縫進行延遲批處理、交互式查詢等操作。這個特點大大增強了Spark Streaming的優勢和功能。
1.4 Spark Streaming關鍵抽象
Discretized Stream或DStream是Spark Streaming提供的基本抽象。它表示連續的數據流,可以是從源接收的輸入數據流,也可以是通過轉換輸入流生成的已處理數據流。在內部,DStream由一系列連續的RDD表示,這是Spark對不可變分布式數據集的抽象。DStream中的每個RDD都包含來自特定時間間隔的數據,如下圖所示。?
應用于DStream的任何操作都轉換為底層RDD上的操作。例如,在先前將行流轉換為字的示例中,flatMap操作應用于linesDStream中的每個RDD 以生成DStream的 wordsRDD。如下圖所示。?
Spark Streaming接收實時輸入數據流并將數據分成批處理,然后由Spark引擎處理,以批量生成最終結果流。?
1.5 Spark Streaming 架構
Spark Streaming使用“微批次”的架構,把流式計算當作一系列連續的小規模批處理來對待。Spark Streaming從各種輸入源中讀取數據,并把數據分組為小的批次。新的批次按均勻的時間間隔創建出來。在每個時間區間開始的時候,一個新的批次就創建出來,在該區間內收到的數據都會被添加到這個批次中。在時間區間結束時,批次停止增長。時間區間的大小是由批次間隔這個參數決定的。批次間隔一般設在500毫秒到幾秒之間,由應用開發者配置。每個輸入批次都形成一個RDD,以 Spark 作業的方式處理并生成其他的 RDD。 處理的結果可以以批處理的方式傳給外部系統。高層次的架構如圖?
Spark Streaming在Spark的驅動器程序—工作節點的結構的執行過程如下圖所示。Spark Streaming為每個輸入源啟動對 應的接收器。接收器以任務的形式運行在應用的執行器進程中,從輸入源收集數據并保存為 RDD。它們收集到輸入數據后會把數據復制到另一個執行器進程來保障容錯性(默 認行為)。數據保存在執行器進程的內存中,和緩存 RDD 的方式一樣。驅動器程序中的 StreamingContext 會周期性地運行 Spark 作業來處理這些數據,把數據與之前時間區間中的 RDD 進行整合。?
1.6 背壓機制
???????默認情況下,Spark Streaming通過Receiver以生產者生產數據的速率接收數據,計算過程中會出現batch processing time > batch interval的情況,其中batch processing time 為實際計算一個批次花費時間, batch interval為Streaming應用設置的批處理間隔。這意味著Spark Streaming的數據接收速率高于Spark從隊列中移除數據的速率,也就是數據處理能力低,在設置間隔內不能完全處理當前接收速率接收的數據。如果這種情況持續過長的時間,會造成數據在內存中堆積,導致Receiver所在Executor內存溢出等問題(如果設置StorageLevel包含disk, 則內存存放不下的數據會溢寫至disk, 加大延遲)。Spark 1.5以前版本,用戶如果要限制Receiver的數據接收速率,可以通過設置靜態配制參數“spark.streaming.receiver.maxRate”的值來實現,此舉雖然可以通過限制接收速率,來適配當前的處理能力,防止內存溢出,但也會引入其它問題。比如:producer數據生產高于maxRate,當前集群處理能力也高于maxRate,這就會造成資源利用率下降等問題。為了更好的協調數據接收速率與資源處理能力,Spark Streaming 從v1.5開始引入反壓機制(back-pressure),通過動態控制數據接收速率來適配集群數據處理能力。
???????背壓機制: 根據JobScheduler反饋作業的執行信息來動態調整Receiver數據接收率。通過屬性“spark.streaming.backpressure.enabled”來控制是否啟用backpressure機制,默認值false,即不啟用。
在原架構的基礎上加上一個新的組件RateController,這個組件負責監聽“OnBatchCompleted ”事件,然后從中抽取processingDelay 及schedulingDelay信息. Estimator依據這些信息估算出最大處理速度(rate),最后由基于Receiver的Input Stream將rate通過ReceiverTracker與ReceiverSupervisorImpl轉發給BlockGenerator(繼承自RateLimiter).?
流量控制點
當Receiver開始接收數據時,會通過supervisor.pushSingle()方法將接收的數據存入currentBuffer等待BlockGenerator定時將數據取走,包裝成block. 在將數據存放入currentBuffer之時,要獲取許可(令牌)。如果獲取到許可就可以將數據存入buffer, 否則將被阻塞,進而阻塞Receiver從數據源拉取數據。?
其令牌投放采用令牌桶機制進行, 原理如下圖所示:?
令牌桶機制
大小固定的令牌桶可自行以恒定的速率源源不斷地產生令牌。如果令牌不被消耗,或者被消耗的速度小于產生的速度,令牌就會不斷地增多,直到把桶填滿。后面再產生的令牌就會從桶中溢出。最后桶中可以保存的最大令牌數永遠不會超過桶的大小。當進行某操作時需要令牌時會從令牌桶中取出相應的令牌數,如果獲取到則繼續操作,否則阻塞。用完之后不用放回。
2. Spark Streaming 簡單應用
2.1 安裝Telnet向端口發送消息
2.2 使用SparkStreaming監控端口數據展示到控制臺
3. DStream 的輸入
Spark Streaming原生支持一些不同的數據源。一些“核心”數據源已經被打包到Spark Streaming 的 Maven 工件中,而其他的一些則可以通過 spark-streaming-kafka 等附加工件獲取。每個接收器都以 Spark 執行器程序中一個長期運行的任務的形式運行,因此會占據分配給應用的 CPU 核心。此外,我們還需要有可用的 CPU 核心來處理數據。這意味著如果要運行多個接收器,就必須至少有和接收器數目相同的核心數,還要加上用來完成計算所需要的核心數。例如,如果我們想要在流計算應用中運行 10 個接收器,那么至少需要為應用分配 11 個 CPU 核心。所以如果在本地模式運行,不要使用local或者local[1]。?
3.1 文件數據源
文件數據流:能夠讀取所有HDFS API兼容的文件系統文件,通過fileStream方法進行讀取?
Spark Streaming 將會監控 dataDirectory 目錄并不斷處理移動進來的文件,記住目前不支持嵌套目錄。
文件需要有相同的數據格式
文件進入 dataDirectory的方式需要通過移動或者重命名來實現。
一旦文件移動進目錄,則不能再修改,即便修改了也不會讀取新數據。?
如果文件比較簡單,則可以使用 streamingContext.textFileStream(dataDirectory)方法來讀取文件。文件流不需要接收器,不需要單獨分配CPU核。
案例實操
## 導入相應的jar包
scala> import org.apache.spark.streaming._
## 創建StreamingContext操作對象
scala> val ssc = new StreamingContext(sc,Seconds(5))
scala> val lines = ssc.textFileStream("hdfs://master:9000/spark/data")
scala> val wordCount = lines.flatMap(_.split("\t")).map(x=>(x,1)).reduceByKey(_+_)
scala> wordCount.print
scala> ssc.start
3.2 自定義數據源
通過繼承Receiver,并實現onStart、onStop方法來自定義數據源采集。
案例實操
3.3 RDD隊列
可以通過使用streamingContext.queueStream(queueOfRDDs)來創建DStream,每一個推送到這個隊列中的RDD,都會作為一個DStream處理。
案例實操
3.4 Kafka
Spark 與 Kafka集成指南?
下面我們進行一個實例,演示SparkStreaming如何從Kafka讀取消息,如果通過連接池方法把消息處理完成后再寫會Kafka?
整合
1.引入jar包依賴
? ? org.apache.spark
? ? spark-streaming-kafka-0-10_2.11
? ? ${spark.version}
2.代碼編寫
//Stream2Kafka
import kafka.serializer.StringDecoder
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.clients.producer.ProducerRecord
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}
object Stream2Kafka extends App {
? //創建配置對象
? val conf = new SparkConf().setAppName("kafka").setMaster("local[3]")
? //創建SparkStreaming操作對象
? val ssc = new StreamingContext(conf,Seconds(5))
? //連接Kafka就需要Topic
? //輸入的topic
? val fromTopic = "source"
? //輸出的Topic
? val toTopic = "target"
? //創建brokers的地址
? val brokers = "master:9092,slave1:9092,slave3:9092,slave2:9092"
? //Kafka消費者配置對象
? val kafkaParams = Map[String, Object](
? ? //用于初始化鏈接到集群的地址
? ? ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
? ? //Key與VALUE的序列化類型
? ? ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG->classOf[StringDeserializer],
? ? ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG->classOf[StringDeserializer],
? ? //用于標識這個消費者屬于哪個消費團體
? ? ConsumerConfig.GROUP_ID_CONFIG->"kafka",
? ? //如果沒有初始化偏移量或者當前的偏移量不存在任何服務器上,可以使用這個配置屬性
? ? //可以使用這個配置,latest自動重置偏移量為最新的偏移量
? ? ConsumerConfig.AUTO_OFFSET_RESET_CONFIG->"latest",
? ? //如果是true,則這個消費者的偏移量會在后臺自動提交
? ? ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG->(false: java.lang.Boolean)
? )
? //創建DStream,連接到Kafka,返回接收到的輸入數據
? val inputStream = {
? ? KafkaUtils.createDirectStream[String, String](
? ? ? ssc,
? ? ? //位置策略(可用的Executor上均勻分配分區)
? ? ? LocationStrategies.PreferConsistent,
? ? ? //消費策略(訂閱固定的主題集合)
? ? ? ConsumerStrategies.Subscribe[String, String](Array(fromTopic), kafkaParams))
? }
? inputStream.map{record => "hehe--"+record.value}.foreachRDD { rdd =>
? ? //在這里將RDD寫回Kafka,需要使用Kafka連接池
? ? rdd.foreachPartition { items =>
? ? ? val kafkaProxyPool = KafkaPool(brokers)
? ? ? val kafkaProxy = kafkaProxyPool.borrowObject()
? ? ? for (item <- items) {
? ? ? ? //使用這個連接池
? ? ? ? kafkaProxy.kafkaClient.send(new ProducerRecord[String, String](toTopic, item))
? ? ? }
? ? ? kafkaProxyPool.returnObject(kafkaProxy)
? ? }
? }
? ssc.start()
? ssc.awaitTermination()
}
//Kafka連接池
import org.apache.commons.pool2.impl.{DefaultPooledObject, GenericObjectPool}
import org.apache.commons.pool2.{BasePooledObjectFactory, PooledObject}
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig}
import org.apache.kafka.common.serialization.StringSerializer
//因為要將Scala的集合類型轉換成Java的
import scala.collection.JavaConversions._
class KafkaProxy(broker:String){
? val conf = Map(
? ? //用于初始化鏈接到集群的地址
? ? ProducerConfig.BOOTSTRAP_SERVERS_CONFIG -> broker,
? ? //Key與VALUE的序列化類型
? ? ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG->classOf[StringSerializer],
? ? ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG->classOf[StringSerializer]
? )
? val kafkaClient = new KafkaProducer[String,String](conf)
}
//創建一個創建KafkaProxy的工廠
class KafkaProxyFactory(broker:String) extends? BasePooledObjectFactory[KafkaProxy]{
? //創建實例
? override def create(): KafkaProxy = new KafkaProxy(broker)
? //包裝實例
? override def wrap(t: KafkaProxy): PooledObject[KafkaProxy] = new DefaultPooledObject[KafkaProxy](t)
}
object KafkaPool {
? private var kafkaPool:GenericObjectPool[KafkaProxy]=null
? def apply(broker:String): GenericObjectPool[KafkaProxy] ={
? ? if(kafkaPool == null){
? ? ? this.kafkaPool = new GenericObjectPool[KafkaProxy](new KafkaProxyFactory(broker))
? ? }
? ? kafkaPool
? }
}
3.啟動zookeeper
4.啟動kafka
kafka-server-start.sh /opt/apps/Kafka/kafka_2.11_2.0.0/config/server.properties &
5.創建兩個主題
[root@master ~]# kafka-topics.sh --create --zookeeper master:2181,slave1:2181,slave2:2181,slave3:2181,slave4:2181 --replication-factor 2 --partitions 2 --topic source
[root@master ~]# kafka-topics.sh --create --zookeeper master:2181,slave1:2181,slave2:2181,slave3:2181,slave4:2181 --replication-factor 2 --partitions 2 --topic target
6.啟動producer 寫入數據到source
[root@master ~]# kafka-console-producer.sh --broker-list master:9092,slave1:9092,slave2:9092,slave3:9092,slave4:9092 --topic source
7.啟動consumer 監聽target的數據
[root@master ~]# kafka-console-consumer.sh --bootstrap-server master:9092,slave1:9092,slave2:9092,slave3:9092,slave4:9092 --topic target
手動設置offset
4. DStream 轉換
DStream上的原語與RDD的類似,分為Transformations(轉換)和Output Operations(輸出)兩種,此外轉換操作中還有一些比較特殊的語法,如:updateStateByKey()、transform()以及各種Window相關的語法。
TransformationMeaning
map(func)將源DStream中的每個元素通過一個函數func從而得到新的DStreams。
flatMap(func)和map類似,但是每個輸入的項可以被映射為0或更多項。
filter(func)選擇源DStream中函數func判為true的記錄作為新DStreams
repartition(numPartitions)通過創建更多或者更少的partition來改變此DStream的并行級別。
union(otherStream)聯合源DStreams和其他DStreams來得到新DStream
count()統計源DStreams中每個RDD所含元素的個數得到單元素RDD的新DStreams。
reduce(func)通過函數func(兩個參數一個輸出)來整合源DStreams中每個RDD元素得到單元素RDD的DStreams。這個函數需要關聯從而可以被并行計算。
countByValue()對于DStreams中元素類型為K調用此函數,得到包含(K,Long)對的新DStream,其中Long值表明相應的K在源DStream中每個RDD出現的頻率。
reduceByKey(func, [numTasks])對(K,V)對的DStream調用此函數,返回同樣(K,V)對的新DStream,但是新DStream中的對應V為使用reduce函數整合而來。Note:默認情況下,這個操作使用Spark默認數量的并行任務(本地模式為2,集群模式中的數量取決于配置參數spark.default.parallelism)。你也可以傳入可選的參數numTaska來設置不同數量的任務。
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映射的函數func作用于源DStream中每個RDD上得到新DStream。這個可用于在DStream的RDD上做任意操作。
updateStateByKey(func)得到”狀態”DStream,其中每個key狀態的更新是通過將給定函數用于此key的上一個狀態和新值而得到。這個可用于保存每個key值的任意狀態數據。
DStream 的轉化操作可以分為無狀態(stateless)和有狀態(stateful)兩種。
在無狀態轉化操作中,每個批次的處理不依賴于之前批次的數據。常見的 RDD 轉化操作,例如 map()、filter()、reduceByKey() 等,都是無狀態轉化操作。
相對地,有狀態轉化操作需要使用之前批次的數據或者是中間結果來計算當前批次的數據。有狀態轉化操作包括基于滑動窗口的轉化操作和追蹤狀態變化的轉化操作。
4.1 無狀態轉化操作
無狀態轉化操作就是把簡單的 RDD 轉化操作應用到每個批次上,也就是轉化 DStream 中的每一個 RDD。部分無狀態轉化操作列在了下表中。 注意,針對鍵值對的 DStream 轉化操作(比如 reduceByKey())要添加import StreamingContext._ 才能在 Scala中使用。?
需要記住的是,盡管這些函數看起來像作用在整個流上一樣,但事實上每個 DStream 在內部是由許多 RDD(批次)組成,且無狀態轉化操作是分別應用到每個 RDD 上的。例如, reduceByKey() 會歸約每個時間區間中的數據,但不會歸約不同區間之間的數據。?
舉個例子,在之前的wordcount程序中,我們只會統計1秒內接收到的數據的單詞個數,而不會累加。?
無狀態轉化操作也能在多個 DStream 間整合數據,不過也是在各個時間區間內。例如,鍵 值對 DStream 擁有和 RDD 一樣的與連接相關的轉化操作,也就是 cogroup()、join()、 leftOuterJoin() 等。我們可以在 DStream 上使用這些操作,這樣就對每個批次分別執行了對應的 RDD 操作。?
我們還可以像在常規的 Spark 中一樣使用 DStream 的 union() 操作將它和另一個 DStream 的內容合并起來,也可以使用 StreamingContext.union() 來合并多個流。
4.2 有狀態轉化操作
4.2.1 追蹤狀態變化UpdateStateByKey
UpdateStateByKey原語用于記錄歷史記錄,有時,我們需要在 DStream 中跨批次維護狀態(例如流計算中累加wordcount)。針對這種情況,updateStateByKey() 為我們提供了對一個狀態變量的訪問,用于鍵值對形式的 DStream。給定一個由(鍵,事件)對構成的 DStream,并傳遞一個指定如何根據新的事件 更新每個鍵對應狀態的函數,它可以構建出一個新的 DStream,其內部數據為(鍵,狀態) 對。?
updateStateByKey() 的結果會是一個新的 DStream,其內部的 RDD 序列是由每個時間區間對應的(鍵,狀態)對組成的。?
updateStateByKey操作使得我們可以在用新信息進行更新時保持任意的狀態。為使用這個功能,你需要做下面兩步:?
1. 定義狀態,狀態可以是一個任意的數據類型。?
2. 定義狀態更新函數,用此函數闡明如何使用之前的狀態和來自輸入流的新值對狀態進行更新。?
使用updateStateByKey需要對檢查點目錄進行配置,會使用檢查點來保存狀態。
4.2.2 Window
Window Operations有點類似于Storm中的State,可以設置窗口的大小和滑動窗口的間隔來動態的獲取當前Steaming的允許狀態。?
基于窗口的操作會在一個比 StreamingContext 的批次間隔更長的時間范圍內,通過整合多個批次的結果,計算出整個窗口的結果。?
所有基于窗口的操作都需要兩個參數,分別為窗口時長以及滑動步長,兩者都必須是 StreamContext 的批次間隔的整數倍。窗口時長控制每次計算最近的多少個批次的數據,其實就是最近的 windowDuration/batchInterval 個批次。如果有一個以 10 秒為批次間隔的源 DStream,要創建一個最近 30 秒的時間窗口(即最近 3 個批次),就應當把 windowDuration 設為 30 秒。而滑動步長的默認值與批次間隔相等,用來控制對新的 DStream 進行計算的間隔。如果源 DStream 批次間隔為 10 秒,并且我們只希望每兩個批次計算一次窗口結果, 就應該把滑動步長設置為 20 秒。?
假設,你想拓展前例從而每隔十秒對持續30秒的數據生成word count。為做到這個,我們需要在持續30秒數據的(word,1)對DStream上應用reduceByKey。使用操作reduceByKeyAndWindow.