其他更多java基礎文章:
java基礎學習(目錄)
概述
SparkStreaming是流式處理框架,是Spark API的擴充套件,支援可擴充套件、高吞吐量、容錯的實時資料流處理,實時資料的來源可以是:Kafka, Flume, Twitter, ZeroMQ或者TCP sockets,並且可以使用高階功能的複雜運算元來處理流資料。例如:map,reduce,join,window 。最終,處理後的資料可以存放在檔案系統,資料庫等,方便實時展現。
執行原理
Spark Streaming架構
Spark Streaming是將流式計算分解成一系列短小的批處理作業。這裡的批處理引擎是Spark Core,也就是把Spark Streaming的輸入資料按照batch interval(如5秒)分成一段一段的資料(Discretized Stream),每一段資料都轉換成Spark中的RDD(Resilient Distributed Dataset),然後將Spark Streaming中對DStream的Transformation操作變為針對Spark中對RDD的Transformation操作,將RDD經過操作變成中間結果儲存在記憶體中。整個流式計算根據業務的需求可以對中間的結果進行疊加或者儲存到外部裝置
DStream
DStream(Discretized Stream)作為Spark Streaming的基礎抽象,它代表持續性的資料流。這些資料流既可以通過外部輸入源賴獲取,也可以通過現有的Dstream的transformation操作來獲得。在內部實現上,DStream由一組時間序列上連續的RDD來表示。每個RDD都包含了自己特定時間間隔內的資料流。
下面是DStream的建立例子: SparkConf conf = new SparkConf().setMaster("local[4]").setAppName("NetworkWordCount")
.set("spark.testing.memory","2147480000");
JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(1));
JavaReceiverInputDStream<String> lines = jssc.socketTextStream("master", 9999);
複製程式碼
API
transform運算元
Transformation | 含義 |
---|---|
map(func) | 對DStream中的各個元素進行func函式操作,然後返回一個新的DStream |
flatMap(func) | 與map方法類似,只不過各個輸入項可以被輸出為零個或多個輸出項 |
filter(func) | 過濾出所有函式func返回值為true的DStream元素並返回一個新的DStream |
repartition(numPartitions) | 增加或減少DStream中的分割槽數,從而改變DStream的並行度 |
union(otherStream) | 將源DStream和輸入引數為otherDStream的元素合併,並返回一個新的DStream. |
count() | 通過對DStream中的各個RDD中的元素進行計數,然後返回只有一個元素的RDD構成的DStream |
reduce(func) | 對源DStream中的各個RDD中的元素利用func進行聚合操作,然後返回只有一個元素的RDD構成的新的DStream. |
countByValue() | 對於元素型別為K的DStream,返回一個元素為(K,Long)鍵值對形式的新的DStream,Long對應的值為源DStrea |
reduceByKey(func, [numTasks]) | 利用func函式對源DStream中的key進行聚合操作,然後返回新的(K,V)對構成的DStream |
join(otherStream, [numTasks]) | 輸入為(K,V)、(K,W)型別的DStream,返回一個新的(K,(V,W)型別的DStream |
cogroup(otherStream, [numTasks]) | 輸入為(K,V)、(K,W)型別的DStream,返回一個新的 (K, Seq[V], Seq[W]) 元組型別的DStream |
transform(func) | 通過RDD-to-RDD函式作用於DStream中的各個RDD,可以是任意的RDD操作,從而返回一個新的RDD |
updateStateByKey(func) | 根據於key的前置狀態和key的新值,對key進行更新,返回一個新狀態的DStream |
Windows Operation
總結:- batch interval:5s
每隔5秒切割一次batch,封裝成DStream - window length:15s
進行計算的DStream中包含15s的資料。即3個batch interval - sliding interval:10s
每隔10s取最近3個batch(window length)封裝的DStream,封裝成一個更大的DStream進行計算
/**
* batch interval:5s
* sliding interval:10s
* window length: 60s
* 所以每隔 10s 會取 12 個 rdd,在計算的時候會將這 12 個 rdd 聚合起來
* 然後一起執行 reduceByKeyAndWindow 操作
* reduceByKeyAndWindow 是針對視窗操作的而不是針對 DStream 操作的
*/
JavaPairDStream<String, Integer> searchWordCountsDStream =
searchWordPairDStream.reduceByKeyAndWindow(new Function2<Integer,
Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
}, Durations.seconds(60), Durations.seconds(10));
複製程式碼
優化Windows Operation
假設 batch=1s, window length=5s, sliding interval=1s, 那麼每個 DStream 重複計算了 5 次,優化後, (t+4)時刻的 Window 由(t+3)時刻的 Window 和(t+4)時刻的 DStream 組成, 由於(t+3)時刻的 Window 包含(t-1)時刻的 DStream,而(t+4)時刻的 Window 中不需要包含(t-1) 時刻的 DStream,所以還需要減去(t-1)時刻的 DStream,所以: Window(t+4) = Window(t+3) + DStream(t+4) - DStream(t-1)。注意,使用此方法必須設定checkpoint目錄,用來儲存Window(t+3)的資料//必須設定 checkpoint 目錄
jssc.checkpoint("hdfs://node01:8020/spark/checkpoint");
JavaPairDStream<String, Integer> searchWordCountsDStream =
searchWordPairDStream.reduceByKeyAndWindow(new Function2<Integer,
Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
},new Function2<Integer, Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 - v2;
}
}, Durations.seconds(60), Durations.seconds(10));
複製程式碼
Driver HA
提交任務時設定
spark-submit –supervise
複製程式碼
以叢集方式提交到 yarn 上時, Driver 掛掉會自動重啟,不需要任何設定
提交任務,在客戶端啟動 Driver,那麼不管是提交到 standalone 還是 yarn, Driver 掛掉後
都無法重啟
程式碼中配置
上面的方式重新啟動的 Driver 需要重新讀取 application 的資訊然後進行任務排程,實 際需求是,新啟動的 Driver 可以直接恢復到上一個 Driver 的狀態(可以直接讀取上一個 StreamingContext 的 DSstream 操作邏輯和 job 執行進度,所以需要把上一個 StreamingContext 的後設資料儲存到 HDFS 上) ,直接進行任務排程,這就需要在程式碼層面進 行配置。
public class SparkStreamingOnHDFS {
public static void main(String[] args) {
final SparkConf conf = new SparkConf()
.setMaster("local[1]")
.setAppName("SparkStreamingOnHDFS");
//這裡可以設定一個執行緒,因為不需要一個專門接收資料的執行緒,而是監控一個目錄
final String checkpointDirectory = "hdfs://node01:9000/spark/checkpoint";
JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
@Override
public JavaStreamingContext create() {
return createContext(checkpointDirectory,conf);
}
};
JavaStreamingContext jsc = JavaStreamingContext.getOrCreate(checkpointDirectory, factory);
jsc.start();
jsc.awaitTermination();
// jsc.close();
}
@SuppressWarnings("deprecation")
private static JavaStreamingContext createContext(String checkpointDirectory,SparkConf conf) {
System.out.println("Creating new context");
SparkConf sparkConf = conf;
//每隔 15s 檢視一下監控的目錄中是否新增了檔案
JavaStreamingContext ssc = new JavaStreamingContext(sparkConf, Durations.seconds(15));
ssc.checkpoint(checkpointDirectory);
/**
* 只是監控資料夾下新增的檔案,減少的檔案是監控不到的,
檔案內容有改動也是監控不到
*/
JavaDStream<String> lines = ssc.textFileStream("hdfs://node01:8020/spark");
/**
* 接下來可以寫業務邏輯,比如 wordcount
*/
return ssc;
}
}
複製程式碼
執行一次程式後, JavaStreamingContext 會在 checkpointDirectory 中儲存,當修 改了業務邏輯後,再次執行程式, JavaStreamingContext.getOrCreate(checkpointDirectory, factory); 因為 checkpointDirectory 中有這個 application 的 JavaStreamingContext,所以不會 呼叫 JavaStreamingContextFactory 來建立 JavaStreamingContext,而是直接 checkpointDirectory 中的 JavaStreamingContext,所以即使業務邏輯改變了,執行的效 果也是之前的業務邏輯, 如果需要執行修改過的業務邏輯,可以修改或刪除 checkpointDirectory
與Kafka的兩種連線方式
Receiver模式
獲取 kafka 傳遞的資料來計算:SparkConf conf = new SparkConf()
.setAppName("SparkStreamingOnKafkaReceiver")
.setMaster("local[2]")
.set("spark.streaming.receiver.writeAheadLog.enable","true");
JavaStreamingContext jsc = new JavaStreamingContext(conf,Durations.seconds(5));
//設定持久化資料的目錄
jsc.checkpoint("hdfs://node01:8020/spark/checkpoint");
Map<String, Integer> topicConsumerConcurrency = new HashMap<String,Integer>();
//topic 名 receiver task 數量
topicConsumerConcurrency.put("test_create_topic", 1);
JavaPairReceiverInputDStream<String,String> lines =
KafkaUtils.createStream(
jsc,
"node02:2181,node03:2181,node04:2181",
"MyFirstConsumerGroup",
topicConsumerConcurrency,
StorageLevel.MEMORY_AND_DISK_SER());
/*
* 第一個引數是 StreamingContext
* 第二個引數是 ZooKeeper 叢集資訊(接受 Kafka 資料的時候會從 Zookeeper 中獲得
Offset 等後設資料資訊)
* 第三個引數是 Consumer Group
* 第四個引數是消費的 Topic 以及併發讀取 Topic 中 Partition 的執行緒數
* 第五個引數是持久化資料的級別,可以自定義
*/
//對 lines 進行其他操作……
複製程式碼
kafka 客戶端生產資料的程式碼:
public class SparkStreamingDataManuallyProducerForKafka extends Thread {
private String topic; //傳送給 Kafka 的資料的類別
private Producer<Integer, String> producerForKafka;
public SparkStreamingDataManuallyProducerForKafka(String topic){
this.topic = topic;
Properties conf = new Properties();
conf.put("metadata.broker.list","node01:9092,node02:9092,node03:9092");
conf.put("serializer.class", StringEncoder.class.getName());
producerForKafka = new Producer<Integer, String>(new ProducerConfig(conf)) ;
}
@Override
public void run() {
while(true){
counter ++;
String userLog = createUserLog();
//生產資料這個方法可以根據實際需求自己編寫
producerForKafka.send(new KeyedMessage<Integer, String>(topic, userLog));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new SparkStreamingDataManuallyProducerForKafka("test_create_topic").start();
//test_create_topic 是 topic 名
}
}
複製程式碼
Direct 方式
把 kafka 當作一個儲存系統,直接從 kafka 中讀資料, SparkStreaming 自己維護消費者 的消費偏移量
SparkConf conf = new SparkConf()
.setAppName("SparkStreamingOnKafkaDirected")
.setMaster("local[1]");
JavaStreamingContext jsc = new JavaStreamingContext(conf,Durations.seconds(10));
Map<String, String> kafkaParameters = new HashMap<String, String>();
kafkaParameters.put("metadata.broker.list","node01:9092,node02:9092,node03:9092");
HashSet<String> topics = new HashSet<String>();
topics.add("test_create_topic");
JavaPairInputDStream<String,String> lines =KafkaUtils.createDirectStream(jsc,
String.class,
String.class,
StringDecoder.class,
StringDecoder.class,
kafkaParameters,
topics);
//對 lines 進行其他操作……
複製程式碼
Direct方式優劣
在實際的應用中,Direct Approach方式很好地滿足了我們的需要,與Receiver-based方式相比,有以下幾方面的優勢:
- 降低資源。Direct不需要Receivers,其申請的Executors全部參與到計算任務中;而Receiver-based則需要專門的Receivers來讀取Kafka資料且不參與計算。因此相同的資源申請,Direct 能夠支援更大的業務。
- 降低記憶體。Receiver-based的Receiver與其他Exectuor是非同步的,並持續不斷接收資料,對於小業務量的場景還好,如果遇到大業務量時,需要提高Receiver的記憶體,但是參與計算的Executor並無需那麼多的記憶體。而Direct 因為沒有Receiver,而是在計算時讀取資料,然後直接計算,所以對記憶體的要求很低。實際應用中我們可以把原先的10G降至現在的2-4G左右。
- 魯棒性更好。Receiver-based方法需要Receivers來非同步持續不斷的讀取資料,因此遇到網路、儲存負載等因素,導致實時任務出現堆積,但Receivers卻還在持續讀取資料,此種情況很容易導致計算崩潰。Direct 則沒有這種顧慮,其Driver在觸發batch 計算任務時,才會讀取資料並計算。佇列出現堆積並不會引起程式的失敗。
但是也存在一些不足,具體如下:
- 提高成本。Direct需要使用者採用checkpoint或者第三方儲存來維護offsets,而不像Receiver-based那樣,通過ZooKeeper來維護Offsets,此提高了使用者的開發成本。
- 監控視覺化。Receiver-based方式指定topic指定consumer的消費情況均能通過ZooKeeper來監控,而Direct則沒有這種便利,如果做到監控並視覺化,則需要投入人力開發。 接!
兩種方式下提高 SparkStreaming 並行度的方法
Receiver 方式調整 SparkStreaming 的並行度的方法:
- 假設 batch interval 為 5s, Receiver Task 會每隔 200ms(spark.streaming.blockInterval 默 認)將接收來的資料封裝到一個 block 中,那麼每個 batch 中包括 25 個 block, batch 會被封 裝到 RDD 中,所以 RDD 中會包含 25 個 partition,所以提高接收資料時的並行度的方法 是:調低 spark.streaming.blockInterval 的值,建議不低於 50ms
其他配置:
- spark.streaming.backpressure.enable 預設 false, 設定為 true 後, sparkstreaming 會根 據上一個 batch 的接收資料的情況來動態的調整本次接收資料的速度,但是最大速度不能 超過 spark.streaming.receiver.maxRate 設定的值(設定為 n,那麼速率不能超過 n/s)
- spark.streaming.receiver.writeAheadLog.enable 預設 false 是否開啟 WAL 機制 Direct 方式並行度的設定:
- 第一個 DStream 的分割槽數是由讀取的 topic 的分割槽數決定的,可以通過增加 topic 的 partition 數來提高 SparkStreaming 的並行度
優化
1. 並行化資料接收:處理多個topic的資料時比較有效
int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<JavaPairDStream<String, String>>(numStreams);
for (int i = 0; i < numStreams; i++) {
kafkaStreams.add(KafkaUtils.createStream(...));
}
JavaPairDStream<String, String> unifiedStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
unifiedStream.print();
複製程式碼
2. spark.streaming.blockInterval:增加block數量,增加每個batch rdd的partition數量,增加處理並行度
receiver從資料來源源源不斷地獲取到資料;首先是會按照block interval,將指定時間間隔的資料,收集為一個block;預設時間是200ms,官方推薦不要小於50ms;接著呢,會將指定batch interval時間間隔內的block,合併為一個batch;建立為一個rdd,然後啟動一個job,去處理這個batch rdd中的資料
batch rdd,它的partition數量是多少呢?一個batch有多少個block,就有多少個partition;就意味著並行度是多少;就意味著每個batch rdd有多少個task會平行計算和處理。
當然是希望可以比預設的task數量和並行度再多一些了;可以手動調節block interval;減少block interval;每個batch可以包含更多的block;有更多的partition;也就有更多的task並行處理每個batch rdd。
3. inputStream.repartition():重分割槽,增加每個batch rdd的partition數量
有些時候,希望對某些dstream中的rdd進行定製化的分割槽 對dstream中的rdd進行重分割槽,去重分割槽成指定數量的分割槽,這樣也可以提高指定dstream的rdd的計算並行度
4. 調節並行度
spark.default.parallelism
reduceByKey(numPartitions)
複製程式碼
5. 使用Kryo序列化機制:
spark streaming,也是有不少序列化的場景的 提高序列化task傳送到executor上執行的效能,如果task很多的時候,task序列化和反序列化的效能開銷也比較可觀 預設輸入資料的儲存級別是StorageLevel.MEMORY_AND_DISK_SER_2,receiver接收到資料,預設就會進行持久化操作;首先序列化資料,儲存到記憶體中;如果記憶體資源不夠大,那麼就寫入磁碟;而且,還會寫一份冗餘副本到其他executor的block manager中,進行資料冗餘。
6. batch interval:每個的處理時間必須小於batch interval
實際上你的spark streaming跑起來以後,其實都是可以在spark ui上觀察它的執行情況的;可以看到batch的處理時間; 如果發現batch的處理時間大於batch interval,就必須調節batch interval 儘量不要讓batch處理時間大於batch interval 比如你的batch每隔5秒生成一次;你的batch處理時間要達到6秒;就會出現,batch在你的記憶體中日積月累,一直囤積著,沒法及時計算掉,釋放記憶體空間;而且對記憶體空間的佔用越來越大,那麼此時會導致記憶體空間快速消耗
如果發現batch處理時間比batch interval要大,就儘量將batch interval調節大一些