Spark Streaming + Kafka整合(Kafka broker版本0.8.2.1+)

  這篇博客是基於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類。

需要注意的點是:

  1. Kafka中Topic的partitions和Spark Streaming中RDDs的partitions沒有對應關係。所以增大KafkaUtils.createStream()方法中的特定主題的partitions數僅僅只會增加從單一Receiver接收並消費數據的線程數。並不會提供Spark併發處理數據的能力。
  2. 使用多個Receiver可以從Kafka的不同group和topic中讀取數據生成多個輸入DStream
  3. 如果啓動了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的模式有以下優點: 

  1. 併發更加簡單:不再需要定義多個Kafka輸入DStream然後將多個輸入合併。通過使用directStream,Spark Streaming會創建與Kafka partitions個數相同的RDD partitions來接收數據,這些partitions會併發的從Kafka讀取數據。所以在這種模式下,Kafka Partition和RDD Partitions有一一對應關係。這種對應更好理解與調試。
  2. 高效:由於沒有Receiver,所以也不需要啓用Write Ahead Logs功能。失敗重試時可以直接從Kafka中讀取數據。
  3. 保證了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中。

需要注意的是:

  1. 上面代碼中,HasOffsetRanges的類型轉換隻有當directKafkaStream上的第一個方法執行成功後才能成功。所以,如果想要獲取offsets,可以在輸入DStream的第一個方法調用處使用transform,獲取到offset後,再調用其他方法處理該DStream,正如上面代碼所示。
  2. 前面提到在這種模式下Kafka和RDD的Partitions一一對應,但是如果在Spark Streaming應用程序中執行了shuffle或者repartition操作,比如reduceByKey或者window操作後,這種對應關係就不存在了。
  3. 由於這種模式沒有Receivers,所以Spark配置參數中那些receiver相關的參數在這種模式下不會起作用,比如spark.streaming.recerver.*參數。此時應該配置的參數是spark.streaming.kafka.*,在這些參數裏面很重要的一個是spark.streaming.kafka.maxRatePerPartition,這個參數的作用是控制Streaming程序通過Kafka direct API每個partition每秒讀入的消息最大數。這個參數在程序初次運行時特別重要。如果不設置這個參數,在Streaming啓動時如果將offsets設置爲smallest,第一個batch將會讀入所有數據,導致後續batch長時間卡住。

3、應用運行

  和第一種模式相同。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章