Kafka Stream概念及初識高層架構圖
Kafka Stream是Apache Kafka從0.10版本引入的一個新Feature,它提供了對存儲於Kafka內的數據進行流式處理和分析的功能。簡而言之,Kafka Stream就是一個用來做流計算的類庫,與Storm、Spark Streaming、Flink的作用類似,但要輕量得多。
Kafka Stream的基本概念:
- Kafka Stream是處理分析存儲在Kafka數據的客戶端程序庫(lib)
- 由於Kafka Streams是Kafka的一個lib,所以實現的程序不依賴單獨的環境
- Kafka Stream通過state store可以實現高效的狀態操作
- 支持原語Processor和高層抽象DSL
Kafka Stream的高層架構圖:
- Partition的數據會分發到不同的Task上,Task主要是用來做流式的並行處理
- 每個Task都會有自己的state store去記錄狀態
- 每個Thread裏會有多個Task
Kafka Stream 核心概念
Kafka Stream關鍵詞:
- 流和流處理器:流指的是數據流,流處理器指的是數據流到某個節點時對其進行處理的單元
- 流處理拓撲:一個拓撲圖,該拓撲圖展示了數據流的走向,以及流處理器的節點位置
- 源處理器及Sink處理器:源處理器指的是數據的源頭,即第一個處理器,Sink處理器則反之,是最終產出結果的一個處理器
如下圖所示:
Kafka Stream使用演示
下圖是Kafka Stream完整的高層架構圖:
從上圖中可以看到,Consumer
對一組Partition
進行消費,這組Partition
可以在一個Topic中或多個Topic中。然後形成數據流,經過各個流處理器後最終通過Producer
輸出到一組Partition
中,同樣這組Partition
也可以在一個Topic中或多個Topic中。這個過程就是數據流的輸入和輸出。
因此,我們在使用Stream API前需要先創建兩個Topic,一個作爲輸入,一個作爲輸出。到服務器上使用命令行創建兩個Topic:
[root@txy-server2 ~]# kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic input-topic
[root@txy-server2 ~]# kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic output-topic
由於之前依賴的kafka-clients
包中沒有Stream API,所以需要另外引入Stream的依賴包。在項目中添加如下依賴:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>2.5.0</version>
</dependency>
接下來以一個經典的詞頻統計爲例,演示一下Stream API的使用。代碼示例:
package com.zj.study.kafka.stream;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Produced;
import java.util.List;
import java.util.Properties;
public class StreamSample {
private static final String INPUT_TOPIC = "input-topic";
private static final String OUTPUT_TOPIC = "output-topic";
/**
* 構建配置屬性
*/
public static Properties getProperties() {
Properties properties = new Properties();
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "49.232.153.84:9092");
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-app");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
return properties;
}
public static KafkaStreams createKafkaStreams() {
Properties properties = getProperties();
// 構建流結構拓撲
StreamsBuilder builder = new StreamsBuilder();
// 構建wordCount這個Processor
wordCountStream(builder);
Topology topology = builder.build();
// 構建KafkaStreams
return new KafkaStreams(topology, properties);
}
/**
* 定義流計算過程
* 例子爲詞頻統計
*/
public static void wordCountStream(StreamsBuilder builder) {
// 不斷的從INPUT_TOPIC上獲取新的數據,並追加到流上的一個抽象對象
KStream<String, String> source = builder.stream(INPUT_TOPIC);
// KTable是數據集的抽象對象
KTable<String, Long> count = source.flatMapValues(
// 以空格爲分隔符將字符串進行拆分
v -> List.of(v.toLowerCase().split(" "))
// 按value進行分組統計
).groupBy((k, v) -> v).count();
KStream<String, Long> sink = count.toStream();
// 將統計結果輸出到OUTPUT_TOPIC
sink.to(OUTPUT_TOPIC, Produced.with(Serdes.String(), Serdes.Long()));
}
public static void main(String[] args) {
KafkaStreams streams = createKafkaStreams();
// 啓動該Stream
streams.start();
}
}
KTable
與KStream
的關係與區別,如下圖:
KTable
類似於一個時間片段,在一個時間片段內輸入的數據就會update進去,以這樣的形式來維護這張表KStream
則沒有update這個概念,而是不斷的追加
運行以上代碼,然後到服務器中使用kafka-console-producer.sh
腳本命令向input-topic
生產一些數據,如下:
[root@txy-server2 ~]# kafka-console-producer.sh --broker-list 127.0.0.1:9092 --topic input-topic
>Hello World Java
>Hello World Kafka
>Hello Java Kafka
>Hello Java
然後再運行kafka-console-consumer.sh
腳本命令從output-topic
中消費數據,並進行打印。具體如下:
[root@txy-server2 ~]# kafka-console-consumer.sh --bootstrap-server 172.21.0.10:9092 --topic output-topic --property print.key=true --property print.value=true --property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer --property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer --from-beginning
控制檯輸出的結果:
world 2
hello 3
java 2
kafka 2
hello 4
java 3
從輸出結果中可以看到,Kafka Stream首先是對前三行語句進行了一次詞頻統計,所以前半段是:
world 2
hello 3
java 2
kafka 2
當最後一行輸入之後,又再做了一次詞頻統計,並針對新的統計結果進行輸出,其他沒有變化的則不作輸出,所以最後打印了:
hello 4
java 3
這也是KTable
和KStream
的一個體現,從測試的結果可以看出Kafka Stream是實時進行流計算的,並且每次只會針對有變化的內容進行輸出。
foreach方法
在之前的例子中,我們是從某個Topic讀取數據進行流處理後再輸出到另一個Topic裏。但在一些場景下,我們可能不希望將結果數據輸出到Topic,而是寫入到一些存儲服務中,例如ElasticSearch、MongoDB、MySQL等。
在這種場景下,就可以利用到foreach
方法,該方法用於迭代流中的元素。我們可以在foreach
中將數據存入例如Map、List等容器,然後再批量寫入到數據庫或其他存儲中間件即可。
foreach
方法使用示例:
public static void foreachStream(StreamsBuilder builder) {
KStream<String, String> source = builder.stream(INPUT_TOPIC);
source.flatMapValues(
v -> List.of(v.toLowerCase().split(" "))
).foreach((k, v) -> System.out.println(k + " : " + v));
}