Kafka核心API——Stream API

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的高層架構圖:
Kafka核心API——Stream API

  • Partition的數據會分發到不同的Task上,Task主要是用來做流式的並行處理
  • 每個Task都會有自己的state store去記錄狀態
  • 每個Thread裏會有多個Task

Kafka Stream 核心概念

Kafka Stream關鍵詞:

  • 流和流處理器:流指的是數據流,流處理器指的是數據流到某個節點時對其進行處理的單元
  • 流處理拓撲:一個拓撲圖,該拓撲圖展示了數據流的走向,以及流處理器的節點位置
  • 源處理器及Sink處理器:源處理器指的是數據的源頭,即第一個處理器,Sink處理器則反之,是最終產出結果的一個處理器

如下圖所示:
Kafka核心API——Stream API


Kafka Stream使用演示

下圖是Kafka Stream完整的高層架構圖:
Kafka核心API——Stream API

從上圖中可以看到,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();
    }
}

KTableKStream的關係與區別,如下圖:
Kafka核心API——Stream API

  • 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

這也是KTableKStream的一個體現,從測試的結果可以看出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));
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章