Spark Streaming 可以⽀持多種數據源,但目前最常用的還是 Kafka 作爲數據源。
Spark Streaming的kafka編程主要包括兩種模型
1.基於Receiver模式
2.Direct(無Receiver)模式
基於Receiver (spark-streaming-kafka-0-10以上已不支持)
這種方式利用接收器(Receiver)來接收kafka中的數據,其最基本是使用Kafka高階用戶API接口。對於所有的接收器,從kafka接收來的數據會存儲在spark的executor中,之後spark streaming提交的job會處理這些數據。
然而,在默認的配置下,這種方式可能會因爲底層的失敗而丟失數據。如果要啓用高可靠機制,讓數據零丟失,就必須啓用Spark Streaming的預寫日誌機制(Write Ahead Log,WAL)。該機制會同步地將接收到的Kafka數據寫入分佈式文件系統(比如HDFS)上的預寫日誌中。所以,即使底層節點出現了失敗,也可以使用預寫日誌中的數據進行恢復。
需要注意的要點:
1、Kafka中的topic的partition,與Spark中的RDD的partition是沒有關係的。所以,在KafkaUtils.createStream()中,提高partition的數量,只會增加一個Receiver中讀取partition的線程的數量。不會增加Spark處理數據的並行度。
2、可以創建多個Kafka輸入DStream,使用不同的consumer group和topic,來通過多個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的中轉站。它是currentbuffer和BlockManager的中間環節。它裏面的每一個元素其實就是一個currentbuffer。
3:維護兩個定時器,其實就是一個生產-消費模式。blockintervaltimer定時器,負責生產端,定時將currentbuffer放進blockforpushing隊列。blockforpushingthread負責消費端,定時將blockforpushing裏的數據轉移到BlockManager。
基於Direct的方式
這種新的不基於Receiver的直接方式,是在Spark 1.3中引入的,從而能夠確保更加健壯的機制。替代掉使用Receiver來接收數據後,這種方式會週期性地查詢Kafka,來獲得每個topic+partition的最新的offset,從而定義每個batch的offset的範圍。當處理數據的job啓動時,就會使用Kafka的簡單consumer api來獲取Kafka指定offset範圍的數據。它並不需要單獨線程實時接收數據,而是每隔batch size時間抓取數據。
這種方式有如下優點:
1、簡化並行讀取:如果要讀取多個partition,不需要創建多個輸入DStream然後對它們進行union操作。Spark會創建跟Kafka partition一樣多的RDD partition,並且會並行從Kafka中讀取數據。所以在Kafka partition和RDD partition之間,有一個一對一的映射關係。
2、高性能:如果要保證零數據丟失,在基於receiver的方式中,需要開啓WAL機制。這種方式其實效率低下,因爲數據實際上被複制了兩份,Kafka自己本身就有高可靠的機制,會對數據複製一份,而這裏又會複製一份到WAL中。而基於direct的方式,不依賴Receiver,不需要開啓WAL機制,只要Kafka中作了數據的複製,那麼就可以通過Kafka的副本進行恢復。
3、一次且僅一次的事務機制:基於receiver的方式,是使用Kafka的高階API來在ZooKeeper中保存消費過的offset的。這是消費Kafka數據的傳統方式。這種方式配合着WAL機制可以保證數據零丟失的高可靠性,但是卻無法保證數據被處理一次且僅一次,可能會處理兩次。因爲Spark和ZooKeeper之間可能是不同步的。
4、降低資源。
Direct不需要Receivers,其申請的Executors全部參與到計算任務中;而Receiver-based則需要專門的Receivers來讀取Kafka數據且不參與計算。因此相同的資源申請,Direct 能夠支持更大的業務。
5、降低內存。
Receiver-based的Receiver與其他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();