Kafka學習記錄

基礎環境搭建(參照官網)好了之後直接搞,kafka的配置文件server.properties詳細說明戳我。下面的Demo都是基於win10搭建出來的基礎環境。

1. 基本概念

 Kafka是一個分佈式流處理平臺,注意它是使用流來處理數據的(核心特徵之一),最終是爲了能夠進行實時的流處理。它是一個集羣,通過 topic 對其中存儲的數據進行分類,每條記錄包含一個 key、一個value、一個時間戳。官網上總結Kafka有4個核心API:

  1. The Producer API:允許一個應用程序發佈一串流式的數據到一個或者多個Kafka topic。
  2. The Consumer API:允許一個應用程序訂閱一個或多個 topic ,並且對發佈給他們的流式數據進行處理。
  3. The Streams API:允許一個應用程序作爲一個流處理器,消費一個或者多個topic產生的輸入流,然後生產一個輸出流到一個或多個topic中去,在輸入輸出流中進行有效的轉換。
  4. The Connector API 允許構建並運行可重用的生產者或者消費者,將Kafka topics連接到已存在的應用程序或者數據系統。比如,連接到一個關係型數據庫,捕捉表(table)的所有變更內容。

 kafka中topic是一個非常重要的概念,本身的目的也是提供遺傳流式的記錄(即topic)。一些重要的基本概念如下:

  • Broker:可以理解爲各個kafka的節點標識,通常用broker.id來標識;
  • Topic:數據主題,是數據記錄發佈的地方,可以用來區分業務系統,通常一個Topic可以擁有多個消費者來訂閱;
  • Producer:生產者,將數據發佈到指定的topic的某個 partition 中,這個行爲可以使用循環或某些語義函數實現負載均衡;
  • Consumer:消費者,由消費者組名稱進行標識,發佈到topic中的記錄將會分配給訂閱該topic的消費者;
  • Consumer group:消費者組,由多個消費者實例組成(可以分佈在多個進程或機器上),對應邏輯上的一個訂閱者;
  • Partition:kafka 對每個 topic 都會維護一個分區,每個分區都是有序的記錄集(順序不可變),且日誌會不斷追加到結構化的 commit log 文件中,分區中每個記錄都會分配一個id(即 offset)來標識上述的順序,topic的分區可以是若干個副本,當節點出現問題時,能自動進行故障轉移,保證數據的可用性;
  • Replication:這裏的副本即指partition的副本,創建副本的單位是topic的partition,通常每個分區都有一個leader和0或多個followers,副本數目=leader數+follower數,所有的follower節點都會同步leader節點的日誌(故障轉移的一種機制)。

 日誌中的partition有一下2個作用:

  • 當日志大小超過了單臺服務器的限制,允許日誌進行擴展。每個單獨的分區都必須受限於主機的文件限制,不過一個主題可能有多個分區,因此可以處理無限量的數據;
  • 可以作爲並行的單元集;

 kafka 有一些質量保證:生產者發送到某個topic的消息會按照發送的順序處理,即先發的消息記錄的偏移(offset)必定比後發的消息的偏移小,且在日誌中較早出現;消費者實例按照日誌中的順序查看;如果一個主題有N個副本,那kafka最多容忍N-1個服務故障,保證數據不會丟失。在kafka中,各個節點是否“存活”,主要取決於下面2個標準:

  1. 節點必須可以維護和 ZooKeeper 的連接,Zookeeper 通過心跳機制檢查每個節點的連接;
  2. 如果節點是個 follower ,它必須能及時的同步 leader 的寫操作,並且延時不能太久;

只有滿足上述2個標準的節點纔是kafka認可的in sync狀態。Leader會追蹤所有 “in sync” 的節點。如果有節點掛掉了, 或是寫超時, 或是心跳超時, leader 就會把它從同步副本列表中移除。 同步超時和寫超時的時間由replica.lag.time.max.ms配置確定

2. 基本命令

 Talk is cheap. Show me the code,實操纔是最好的熟悉新事物的方式,下面是一些簡單的操作:

# 1. --create 指定當前對於topic的操作行爲
# --topic 指定名爲 test 的 topic
# --zookeeper 指定 zookeeper 地址爲 localhost:2181
# --replication-factor 指定 topic 的副本數量爲3
# --partitions 指定分區 必選
.\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

# 2. 查看topic列表
.\kafka-topics.bat --list --zookeeper localhost:2181

# 3. 查看某個topic(這裏是my-replicated-topic)的詳細信息
.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic my-replicated-topic
# 輸出如下, 其中第一行標識所有分區的摘要,後續的第二行第三行..標識每個分區的信息,my-replicated-topic 只有一個分區,故只有一行
Topic:my-replicated-topic       PartitionCount:1        ReplicationFactor:3     Configs:
        Topic: my-replicated-topic      Partition: 0    Leader: 0       Replicas: 0,1,2 Isr: 0,1,2

# 4. 修改 topic 的 partitions 爲2
.\kafka-topics.bat --zookeeper localhost:2181 --alter --topic test --partitions 2
# 再次查看 test 的詳情:.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic test 輸出如下
Topic:test      PartitionCount:2        ReplicationFactor:1     Configs:
        Topic: test     Partition: 0    Leader: 0       Replicas: 0     Isr: 0
        Topic: test     Partition: 1    Leader: 1       Replicas: 1     Isr: 1
# 4.1 增加配置項:對 topic test 添加某個配置項 x=y (key-value的形式)
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --add-config x=y
# 4.2 刪除配置項
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --delete-config x=y

# 5. 向集羣發送消息,運行生產者,選擇某個主題,這裏選擇 test 的topic
.\kafka-console-producer.bat --broker-list localhost:9092 --topic test

# 6. 從集羣消費消息,運行消費者,選擇和生產者一致的topic進行連接,當然可以是集羣中的任意kafka節點,比如我這裏是 9092~9094 3個節點
.\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning

# 7. 刪除topic,如果配置文件中沒有配置 delete.topic.enable=true 刪除操作時並不會真正刪除,只是打上一個標記
.\kafka-topics.bat --delete --zookeeper localhost:2181 --topic my-replicated-topic

上述 --describe 行爲輸出結果詳細參數說明如下:

  • “leader”是負責給定分區所有讀寫操作的節點。每個節點都是隨機選擇的部分分區的領導者。
  • “replicas”是複製分區日誌的節點列表,不管這些節點是leader還是僅僅活着。
  • “isr”是一組“同步”replicas,是replicas列表的子集,它活着並被指到leader。

通過在生產者balabala一些,他就會將你說的這些廢話(默認一行廢話爲一個message)發送到kafka的集羣,集羣內的所有消費者都會接受你的廢話,如下:

生產者發送消息

 上述的基本命令都是topic級別的,很多參數都可以通過 --config 的方式指定,比如上述的--create--zookeeper--replication-factor,當然還有很多參數可以配置,比如:--config max.message.bytes=64000--config flush.messages=1指定最大消息的大小和刷新頻率。topic的創建行爲可以像上述那樣手動創建,也可以在數據首次發佈到某個不存在的topic時自動創建,對於一些參數詳細說明如下:

  • --replication-factor:控制有多少服務器將複製每個寫入的消息。如果設置了3個複製因子,那麼只能最多2個相關的服務器能出問題,否則將無法訪問數據(N-1)。建議您使用2或3個複製因子,以便在不中斷數據消費的情況下透明的調整集羣;
  • --partitions:控制 topic 將被分片到多少個日誌裏。partitions 會產生幾個影響-首先,每個分區只屬於一個臺服務器,所以如果有20個分區,那麼全部數據(包含讀寫負載)將由不超過20個服務器(不包含副本)處理,最後 partitions 還會影響 consumer 的最大並行度。

每個分區日誌都放在自己的Kafka日誌目錄下的文件夾中。這些文件夾的名稱的形式爲topic名-分區ID,注意topic的名稱不能超過249個字符(爲了留下了足夠的空間以顯示短劃線和可能的5位長的分區ID)。

3. SpringBoot集成kafka

 基於上述進行簡單的集成,基本環境如下:

  • SpringBoot:2.2.1.RELEASE;
  • spring-kafka:2.3.4.RELEASE;
  • kafka:本地 win10 搭建的 kafka-2.3.1 3節點集羣;
  • zookeeper:本地 win 10 搭建的 zookeeper-3.5.6;

pom 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.glodon</groupId>
    <artifactId>spring_kafka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_kafka</name>
    <packaging>jar</packaging>
    <description>Spring project for Kafka</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-kafka.version>2.3.4.RELEASE</spring-kafka.version>
    </properties>

    <dependencies>
        <!--spring-boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--fastJson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

        <!-- kafka-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>${spring-kafka.version}</version>
        </dependency>
    </dependencies>
</project>

相關參數配置:

# kafka
spring.kafka.bootstrap-servers=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094
spring.kafka.listener.concurrency=3
# 生產者相關配置
spring.kafka.producer.retries=1
spring.kafka.producer.batch-size=16384
spring.kafka.producer.buffer-memory=33554432
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 消費者相關配置
spring.kafka.consumer.group-id=consumer-default
spring.kafka.consumer.auto-commit-interval=100
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

生產者和消費者實現類:

/**
 * @author liuwg-a
 * @date 2019/1/4 15:49
 * @description 實例化配置,主要用於創建2個測試的topic
 */
@Configuration
public class Config {
    /**
     * 創建一個測試的 topic
     *
     * @return NewTopic
     */
    @Bean("noBlockingSpringKafkaTopic")
    public NewTopic createNoBlockingSpringKafkaTopic() {
        return TopicBuilder.name("spring-kafka-no-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
                                                                                                    "zstd").build();
    }

    @Bean("blockingSpringKafkaTopic")
    public NewTopic createBlockingSpringKafkaTopic() {
        return TopicBuilder.name("spring-kafka-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
                                                                                                 "zstd").build();
    }

}

/**
 * @author liuwg-a
 * @date 2019/12/10 19:01
 * @description kafka 消息類
 */
public class Message {

    private Long          id;
    private String        msg;
    private LocalDateTime time;

    public Message() {
    }

    public Message(Long id, String msg, LocalDateTime time) {
        this.id = id;
        this.msg = msg;
        this.time = time;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public LocalDateTime getTime() {
        return time;
    }

    public void setTime(LocalDateTime time) {
        this.time = time;
    }
}

/**
 * 生產者
 * @author liuwg-a
 * @date 2019/12/10 19:04
 * @description
 */
@Service("producer")
public class KafkaProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    @Autowired
    private NewTopic                      noBlockingSpringKafkaTopic;
    @Autowired
    private NewTopic                      blockingSpringKafkaTopic;

    private static final Logger           logger = LoggerFactory.getLogger(KafkaProducer.class);

    /**
     * 1. 異步非阻塞發送消息
     *
     * @param message message
     */
    public void noBlockingSend(Message message) {
        if (message == null) {
            return;
        }

        ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(noBlockingSpringKafkaTopic.name(),
                                                                                 JSON.toJSONString(message));
        /*
         * 消息發送成功進行回調
         */
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

            @Override
            public void onFailure(Throwable throwable) {
                // 處理失敗的邏輯
                logger.error("kafka producer:send message failed");
            }

            @Override
            public void onSuccess(SendResult<String, String> stringStringSendResult) {
                // 處理成功的邏輯
                logger.info("kafka producer: send message success");
            }
        });
    }

    /**
     * 2. 阻塞式發送消息
     *
     * @param message message
     */
    public void blockingSend(Message message) {
        try {
            kafkaTemplate.send(blockingSpringKafkaTopic.name(), JSON.toJSONString(message)).get(5, TimeUnit.SECONDS);
            // 發送成功立即處理
            logger.info("kafka producer: send message success");
        } catch (Exception e) {
            // 處理失敗的邏輯
            logger.error("kafka producer:send message failed");
        }
    }

}

/**
 * 消費者
 * @author liuwg-a
 * @date 2019/12/10 19:06
 * @description
 */
@Service("consumer")
public class KafkaConsumer {

    private static final String NO_BLOCKING_SPRING_KAFKA_TOPIC_STR = "spring-kafka-no-blocking-test";
    private static final String BLOCKING_SPRING_KAFKA_TOPIC_STR    = "spring-kafka-blocking-test";

    private static final Logger logger                             = LoggerFactory.getLogger(KafkaConsumer.class);

    /**
     * 註解 @KafkaListener 會促使 Spring Kafka 自動創建一個消息容器
     */
    @KafkaListener(topics = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR)
    public void receiveNoBlocking(ConsumerRecord<String, String> record) {
        Optional<String> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            logger.info("kafka consumer: receive message -> record = {}", record);
            logger.info("kafka consumer: receive message -> message = {}", message);
        }
    }

    @KafkaListener(topics = BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = BLOCKING_SPRING_KAFKA_TOPIC_STR)
    public void receiveBlocking(ConsumerRecord<String, String> record) {
        Optional<String> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            logger.info("kafka consumer: receive message -> record = {}", record);
            logger.info("kafka consumer: receive message -> message = {}", message);
        }
    }
}

測試類:

/**
 * @author liuwg-a
 * @date 2019/12/10 19:10
 * @description
 */
@SpringBootTest
@ExtendWith(SpringExtension.class)
class KafkaProducerTest {

    @Autowired
    private KafkaProducer kafkaProducer;

    @Test
    void noBlockingSend() {
        for (int i = 0; i < 3; i++) {
            kafkaProducer.noBlockingSend(produceMessage());
        }
    }

    @Test
    void blockingSend() {
        for (int i = 0; i < 3; i++) {
            kafkaProducer.blockingSend(produceMessage());
        }
    }

    /**
    * 模擬生產消息
    */
    private Message produceMessage() {
        return new Message(new Random().nextLong(), UUID.randomUUID().toString(), LocalDateTime.now());
    }
}

啓動測試即可,注意這裏如果不進行初始化創建的2個topic的java配置,則需要提前手動創建好測試的 topic,否則啓動會出現找不到該 topic 的異常。

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