Spark Streaming Kafka

Spark Streaming 可以⽀持多種數據源,但目前最常用的還是 Kafka 作爲數據源。

Spark Streamingkafka編程主要包括兩種模型

1.基於Receiver模式

2.Direct(無Receiver)模式

 

基於Receiver spark-streaming-kafka-0-10以上已不支持)

這種方式利用接收器(Receiver)來接收kafka中的數據,其最基本是使用Kafka高階用戶API接口。對於所有的接收器,從kafka接收來的數據會存儲在sparkexecutor中,之後spark streaming提交的job會處理這些數據。

然而,在默認的配置下,這種方式可能會因爲底層的失敗而丟失數據。如果要啓用高可靠機制,讓數據零丟失,就必須啓用Spark Streaming的預寫日誌機制(Write Ahead LogWAL)。該機制會同步地將接收到的Kafka數據寫入分佈式文件系統(比如HDFS)上的預寫日誌中。所以,即使底層節點出現了失敗,也可以使用預寫日誌中的數據進行恢復。

需要注意的要點:

1Kafka中的topicpartition,與Spark中的RDDpartition是沒有關係的。所以,在KafkaUtils.createStream()中,提高partition的數量,只會增加一個Receiver中讀取partition的線程的數量。不會增加Spark處理數據的並行度。

2、可以創建多個Kafka輸入DStream,使用不同的consumer grouptopic,來通過多個receiver並行接收數據。

3、如果基於容錯的文件系統,比如HDFS,啓用了預寫日誌機制,接收到的數據都會被複制一份到預寫日誌中。因此,在KafkaUtils.createStream()中,設置的持久化級別是StorageLevel.MEMORY_AND_DISK_SER

spark streaming啓動過後,會選擇一臺excetor作爲ReceiverSupervior

1:Reciver的父級ReciverTracker分發多個job(task)到不同的executor,並啓動ReciverSupervisor.

2:ReceiverSupervior會啓動對應的實例reciver(kafkareciver,TwitterReceiver),並調用onstart()

3:kafkareciver在通過onstart()啓動後就開啓線程源源不斷的接收數據,並交給ReceiverSupervior,通過ReceiverSupervior.store函數一條一條接收

4:ReceiverSupervior會調用BlockGenertor.adddata填充數據。

所有的中間數據都緩存在BlockGenertor

1:首先BlockGenertor維護了一個緩衝區,currentbuffer,一個無限長度的arraybuffer。爲了防止內存撐爆,這個currentbuffer的大小可以被限制,通過設置參數spark.streaming.reciver.maxRate,以秒爲單位。currentbuffer所使用的內存不是storage(負責spark計算過程中的所有存儲,包括磁盤和內存),而是珍貴的計算內存。所以currentbuffer應該被限制,防止佔用過多計算內存,拖慢任務計算效率,甚至有可能拖垮Executor甚至集羣。

2:維護blockforpushing隊列,它是等待被拉到到BlockManager的中轉站。它是currentbufferBlockManager的中間環節。它裏面的每一個元素其實就是一個currentbuffer

3:維護兩個定時器,其實就是一個生產-消費模式。blockintervaltimer定時器,負責生產端,定時將currentbuffer放進blockforpushing隊列。blockforpushingthread負責消費端,定時將blockforpushing裏的數據轉移到BlockManager

基於Direct的方式

這種新的不基於Receiver的直接方式,是在Spark 1.3中引入的,從而能夠確保更加健壯的機制。替代掉使用Receiver來接收數據後,這種方式會週期性地查詢Kafka,來獲得每個topic+partition的最新的offset,從而定義每個batchoffset的範圍。當處理數據的job啓動時,就會使用Kafka的簡單consumer api來獲取Kafka指定offset範圍的數據。它並不需要單獨線程實時接收數據,而是每隔batch size時間抓取數據。

這種方式有如下優點:

1、簡化並行讀取:如果要讀取多個partition,不需要創建多個輸入DStream然後對它們進行union操作。Spark會創建跟Kafka partition一樣多的RDD partition,並且會並行從Kafka中讀取數據。所以在Kafka partitionRDD partition之間,有一個一對一的映射關係。

2、高性能:如果要保證零數據丟失,在基於receiver的方式中,需要開啓WAL機制。這種方式其實效率低下,因爲數據實際上被複制了兩份,Kafka自己本身就有高可靠的機制,會對數據複製一份,而這裏又會複製一份到WAL中。而基於direct的方式,不依賴Receiver,不需要開啓WAL機制,只要Kafka中作了數據的複製,那麼就可以通過Kafka的副本進行恢復。

3、一次且僅一次的事務機制:基於receiver的方式,是使用Kafka的高階API來在ZooKeeper中保存消費過的offset的。這是消費Kafka數據的傳統方式。這種方式配合着WAL機制可以保證數據零丟失的高可靠性,但是卻無法保證數據被處理一次且僅一次,可能會處理兩次。因爲SparkZooKeeper之間可能是不同步的。

4、降低資源。

Direct不需要Receivers,其申請的Executors全部參與到計算任務中;而Receiver-based則需要專門的Receivers來讀取Kafka數據且不參與計算。因此相同的資源申請,Direct 能夠支持更大的業務。

5、降低內存。

Receiver-basedReceiver與其他Exectuor是異步的,並持續不斷接收數據,對於小業務量的場景還好,如果遇到大業務量時,需要提高Receiver的內存,但是參與計算的Executor並無需那麼多的內存。而Direct 因爲沒有Receiver,而是在計算時讀取數據,然後直接計算,所以對內存的要求很低。實際應用中我們可以把原先的10G降至現在的2-4G左右。

6、魯棒性更好。

Receiver-based方法需要Receivers來異步持續不斷的讀取數據,因此遇到網絡、存儲負載等因素,導致實時任務出現堆積,但Receivers卻還在持續讀取數據,此種情況很容易導致計算崩潰。Direct 則沒有這種顧慮,其Driver在觸發batch 計算任務時,纔會讀取數據並計算。隊列出現堆積並不會引起程序的失敗。

KafkaUtils.createStream接收數據流程

 

實例

以 Kafka 作爲數據源的典型基於 Java 的 Spark Streaming 代碼框架如下:

public class JavaKafka2Spark { 
public static void main(String args[]) { 
    SparkConf conf = new SparkConf(); 
    JavaStreamingContext ssc = new JavaStreamingContext(conf, Durations.seconds(3)); 
    Map kafkaParams = new HashMap<>(); 
    kafkaParams.put("bootstrap.servers", "host1:9092,host2:9092");        
    kafkaParams.put("key.deserializer", StringDeserializer.class); 
    kafkaParams.put("value.deserializer", StringDeserializer.class);             
    kafkaParams.put("group.id", "test-id"); 
    kafkaParams.put("auto.offset.reset", "latest"); 
    kafkaParams.put("enable.auto.commit", false); 
    Collection topics = Arrays.asList("test"); 
    JavaInputDStream<ConsumerRecord> stream = KafkaUtils.createDirectStream( 
        ssc,     
        LocationStrategies.PreferConsistent(), 
        ConsumerStrategies.Subscribe(topics, kafkaParams) ); 
        final AtomicReference offsetRanges = new AtomicReference<>(); 
        stream.transform(rdd -> 
            {   OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges(); 
                offsetRanges.set(offsets); 
                return rdd; 
            }).map(/*業務邏輯*/).foreachRDD(rdd -> { 
                ((CanCommitOffsets)stream.inputDStream()).commitAsync(offsetRanges.get());
        }); 
    ssc.start(); 
    ssc.awaitTermination(); }
 }

其中,各主要段的含義如下:

1. Spark Streaming 環境初始化

SparkConf conf = new SparkConf();
JavaStreamingContext ssc = new JavaStreamingContext(conf, Durations.seconds(3));

2. 指定 Kafka 連接的參數

Map kafkaParams = new HashMap<>(); kafkaParams.put("bootstrap.servers", "host1:9092,host2:9092"); 
kafkaParams.put("key.deserializer", StringDeserializer.class); kafkaParams.put("value.deserializer", StringDeserializer.class); 
kafkaParams.put("group.id", "test-id"); 
kafkaParams.put("auto.offset.reset", "latest");  // 默認是earliest
kafkaParams.put("enable.auto.commit", false);

3. 指定用於數據讀取的 Topic

Collection topics = Arrays.asList("test");

4. 創建 Kafka 連接

JavaInputDStream<ConsumerRecord> stream = KafkaUtils.createDirectStream( 
    ssc, 
    LocationStrategies.PreferConsistent(),
    ConsumerStrategies.Subscribe(topics, kafkaParams) 
);

5. Kafka Offset 處理邏輯:先暫存,再持久化

final AtomicReference offsetRanges = new AtomicReference<>(); 
stream.transform(rdd -> { 
    offsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();           
    offsetRanges.set(offsets); 
    return rdd; }).map(/*業務邏輯*/).foreachRDD(rdd -> 
    { ((CanCommitOffsets) stream.inputDStream()).commitAsync(offsetRanges.get()); });

6. 啓動流計算處理進程

ssc.start();

7. 等待,直到用戶(或框架)下達終止指令

ssc.awaitTermination();

 

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