這篇博客是基於Spark Streaming整合Kafka-0.8.2.1官方文檔。
本文主要講解了Spark Streaming如何從Kafka接收數據。Spark Streaming從Kafka接收數據主要有兩種辦法,一種是基於Kafka high-level API實現的基於Receivers的接收方式,另一種是從Spark 1.3版本之後新增的無Receivers的方式。這兩種方式的代碼編寫,性能表現都不相同。本文後續部分對這兩種方式逐一進行分析。
一、基於Receiver的模式
這種模式主要會用到一個Receiver組件從Kafka接收數據,這個Receiver是基於Kafka的high-level消費者API實現的。Receivers從Kafka接收到的數據會保存在executors上,然後Spark Streaming啓動Job來處理這些數據。
然而,在默認配置情況下, 這種模式會有數據丟失的情況發生。爲了實現零數據丟失,需要在Spark Streaming中啓動Write Ahead Logs功能。WAL會同步的將所有從Kafka接收到的數據保存到一個分佈式文件系統,比如HDFS上。用這種辦法可以保證Spark Streaming從不可靠數據源獲取數據失敗時的恢復。
有關Write Ahead Logs的介紹,可以參考Streaming應用部署文檔。
接下來講解實際應用時如何實現這種模式。
1、依賴配置
這種模式依賴的jar包相關信息如下
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.11
version = 2.0.1
2、程序編寫
需要導入KafkaUtils
類來創建輸入DStream。
import org.apache.spark.streaming.kafka._
val kafkaStream = KafkaUtils.createStream(streamingContext,
[ZK quorum],
[consumer group id],
[per-topic number of Kafka partitions to consume])
還可以指定輸入數據key和value對於的解碼類。可以參考KafkaUtilsAPI文檔 ,或者Spark源碼中提供的KafkaWordCount
類。
需要注意的點是:
- Kafka中Topic的partitions和Spark Streaming中RDDs的partitions沒有對應關係。所以增大
KafkaUtils.createStream()
方法中的特定主題的partitions
數僅僅只會增加從單一Receiver接收並消費數據的線程數。並不會提供Spark併發處理數據的能力。 - 使用多個Receiver可以從Kafka的不同group和topic中讀取數據生成多個輸入DStream
如果啓動了Write Ahead Logs功能,接收到的數據在處理之前已經做過備份。因此需要把輸入數據流的存儲級別調整爲
StorageLevel.MEMORY_AND_DISK_SER
模式。即需要調用KafkaUtils.createStream
方法時傳入一個StorageLevel.MEMORY_AND_DISK_SER
參數。Receivers和Write Ahead Logs功能的結合時,Spark Streaming應用使用Kafka高階API將消費offsets保存在Zookeeper中。雖然這種使用方式可以保證避免數據丟失,但是不能保證在某些失敗情況下數據被多次處理,即這種情況下實現的是At Least Once。因爲Spark Streaming讀取數據的Offset都是由Zookeeper來維護的。這樣在Spak Streaming和Zookeeper維護offsets的過程中無法保證其同步。
3、應用運行
和其他Spark應用程序一樣,Spark Streaming應用也可以用spark-submit
來啓動。
需要將依賴的spark-streaming-kafka-0.8_2.11
以及該JAR包的依賴包都需要打入應用所在的JAR包中。並且要保證運行環境中提供了spark-core_2.11
以及spark-streaming_2.11
。
也可以使用spark-submit
的--jars
參數引入依賴的spark-streaming-kafka-0-8_2.11
引入。
二、直接模式(無Receiver)模式
這種模式下不需要使用Receivers從Kafka接收數據,這種模式下Streaming應用會定期的查詢每一個Kafka Topic的Partitions最新的消費Offsets,基於這些Offsets數據來定義每一個batch需要處理的數據範圍。有了這些Offset範圍後,Streaming應用匯使用Kafka的Simple Consumer API從Kafka讀取數據。
這種模式相比於基於Receivers的模式有以下優點:
- 併發更加簡單:不再需要定義多個Kafka輸入DStream然後將多個輸入合併。通過使用
directStream
,Spark Streaming會創建與Kafka partitions個數相同的RDD partitions來接收數據,這些partitions會併發的從Kafka讀取數據。所以在這種模式下,Kafka Partition和RDD Partitions有一一對應關係。這種對應更好理解與調試。 - 高效:由於沒有Receiver,所以也不需要啓用Write Ahead Logs功能。失敗重試時可以直接從Kafka中讀取數據。
保證了Exactly-Once:這種模式下,讀取數據不通過Zookeeper。Offsets由Spark Streaming應用程序維護並可以記錄在檢查點中。所以這保證了Spark Streaming數據讀取的exactly once。如果想要實現計算結果輸出的exactly once,應用程序中保存計算數據和offsets到外部數據系統的操作必須具有冪等性 (idempotent)或原子事物性(atomic transaction)。可以參考Spark Streaming輸出操作語義。
這種模式的一個缺點是它不會更新Zookeeper中的offsets狀態,所以那些基於Zookeeper的Kafka監控工具在這種情況下會失效,比如KafkaOffsetsMonitor等。然而如果在應用程序中可以手動的獲取每一batch的offset,並手動更新到Zookeeper中去。
接下來講解實際應用時如何實現這種模式。
1、依賴配置
這種模式依賴的jar包相關信息如下
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.11
version = 2.0.1
2、程序編寫
需要導入KafkaUtils
類來創建輸入DStream。
import org.apache.spark.streaming.kafka._
val directKafkaStream = KafkaUtils.createDirectStream[
[key class], [value class], [key decoder class], [value decoder class] ](
streamingContext, [map of Kafka parameters], [set of topics to consume])
可以爲createDirectStream
方法傳入一個messageHandler
對象來訪問MessageAndMetadata
,這個MessageAndMetadata
對當前message的metadata進行結構化。有關該方法的使用,可以仔細閱讀API文檔或者Spark源碼中提供的DirectKafkaWordCount
示例程序。
在Kafka參數[map of Kafka parameters]
中,必須指定的是metadata.broker
或者bootstrap.servers
。默認情況下,會從每一個Kafka partition的最新offset開始消費。如果在這裏將auto.offset.reset
設置成smallest
的話,Spark Streaming將從最小offset開始消費。
也可以往KafkaUtils.createDirectStream
方法中傳入offset參數從任意offset處開始消費。按照下面代碼中的方式,可以獲取每一個batch對應的offset狀況。
// Hold a reference to the current offset ranges, so it can be used downstream
var offsetRanges = Array[OffsetRange]()
directKafkaStream.transform { rdd =>
offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd
}.map {
...
}.foreachRDD { rdd =>
for (o <- offsetRanges) {
println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
}
...
}
獲取到Offset信息後,如果有需要,可以手動將這些數據寫入Zookeeper中。
需要注意的是:
- 上面代碼中,
HasOffsetRanges
的類型轉換隻有當directKafkaStream
上的第一個方法執行成功後才能成功。所以,如果想要獲取offsets,可以在輸入DStream的第一個方法調用處使用transform
,獲取到offset後,再調用其他方法處理該DStream,正如上面代碼所示。 - 前面提到在這種模式下Kafka和RDD的Partitions一一對應,但是如果在Spark Streaming應用程序中執行了
shuffle
或者repartition
操作,比如reduceByKey
或者window
操作後,這種對應關係就不存在了。 - 由於這種模式沒有Receivers,所以Spark配置參數中那些receiver相關的參數在這種模式下不會起作用,比如
spark.streaming.recerver.*
參數。此時應該配置的參數是spark.streaming.kafka.*
,在這些參數裏面很重要的一個是spark.streaming.kafka.maxRatePerPartition
,這個參數的作用是控制Streaming程序通過Kafka direct API每個partition每秒讀入的消息最大數。這個參數在程序初次運行時特別重要。如果不設置這個參數,在Streaming啓動時如果將offsets設置爲smallest,第一個batch將會讀入所有數據,導致後續batch長時間卡住。
3、應用運行
和第一種模式相同。