聊聊kafka

兩個月因爲忙於工作毫無輸出了,最近想給團隊小夥伴分享下kafka的相關知識,於是就想着利用博客來做個提前的準備工作了;接下來會對kafka做一個簡單的介紹,包括利用akf原則來解析單機下kafk的各個角色介紹、集羣下kafka的架構、生產者消費者實操、offset的維護粒度、ack取不同值時kafka集羣的表現等;

一、結合AKF來聊聊單機kafka的架構

簡單介紹下AKF

  • AKF是微服務的拆分原則,或者說可以跟任何分佈式系統掛鉤,AKF以座標系的概念把系統劃分爲X、Y、Z三個軸,不同軸解決不同的問題
    X軸:解決單點故障問題,對服務進行水平擴容,即集羣中常見的主從、主備複製,把主機複製一份到遠端來解決單點故障的問題
    Y軸:解決系統壓力問題,可將服務按業務拆分,不同的業務打到不同的集羣節點;
    Z軸:針對Y軸的補充,當Y軸扛不住壓力時可對Y軸的服務做拓展

整合KAFKA來理解,如圖:

kafka常用角色介紹

  • broker
    可以理解爲一個JVM進程,kafka的服務節點
  • topic
    一個邏輯概念,可以跨broker,可以擁有多個分區,用來對業務進行區分
  • partition
    真正存儲數據的物理分區,存在於broker進程所在的物理機上

二、集羣下kafka的結構

kafka的集羣由zookeeper調度,現在的版本中zk更單純的只做分佈式的協調者,即爲kafka集羣選主,集羣中各節點的信息由每個節點中存儲的元數據(metadata)持有,所以生產者消費者可以直連broker集羣從而降低zk的壓力,詳見下圖:

三、入門實操

kafka和zk的安裝與啓動

爲了方便測試的環境我是直接在windows上搭建的,下面簡單介紹下

  • 首先去官網分別下載zk和kafka的安裝包然後解壓出來
  • 啓動zk:進入zk的bin目錄雙擊zkServer.cmd啓動zk服務端
  • 啓動kafka:進入kafka安裝目錄,使用:.\bin\windows\kafka-server-start.bat .\config\server.properties啓動kafka服務;需要注意的是爲了zk根目錄的簡結也爲了後續其他服務接入zk時能夠區分,建議在kafka的config/server.properties文件中將zookeeper.connect的值再加一個目錄後綴,如:localhost:2181/kafka01;這樣也方便去zk查詢當前kafka的節點信息

整合springboot搭建單元測試

  • 首先可以通過命令行創建一個topic
    進入kafka的bin/windows目錄,調用:kafka-topics.bat --create --zookeeper localhost:2181/kafka01 --replication-factor 1 --partitions 2 --topic dll_test_01
  • 生產者用例
package com.darling.controller.kafka;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.Test;

import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * @description: 生產者用例
 * windows創建topic命令:kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
 * @author: dll
 * @date: Created in 2022/10/10 10:06
 * @version:
 * @modified By:
 */
public class ProducerTest {

    @Test
    public void producer() throws ExecutionException, InterruptedException {
        String topic = "dll_test_01";
        Properties p = new Properties();
        // broker的地址
        p.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
        // k-v的序列化設置
        p.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        p.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // ack設置,0
        p.setProperty(ProducerConfig.ACKS_CONFIG, "-1");

        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(p);

        while(true){
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j <3; j++) {
                    ProducerRecord<String, String> record = new ProducerRecord<>(topic, "item"+j,"val" + i);
                    Future<RecordMetadata> send = producer.send(record);
                    RecordMetadata rm = send.get();
                    int partition = rm.partition();
                    long offset = rm.offset();
                    System.out.println("==============key: "+ record.key()+" val: "+record.value()+" partition: "+partition + " offset: "+offset);
                }
            }
        }



    }
}

  • 消費者用例
package com.darling.controller.kafka;

import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.junit.Test;

import java.time.Duration;
import java.util.*;

/**
 * @description: 消費者用例
 * @author: dll
 * @date: Created in 2022/10/10 12:33
 * @version:
 * @modified By:
 */
@Slf4j
public class ConsumerTest {


    @Test
    public void consumer() {
        //基礎配置
        Properties p = new Properties();
        // 設置brokers
        p.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
        // k-v序列化
        p.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        p.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        //設置消費組
        p.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"GROUP_TEST");
        /**
         * 設置消費時消費消息的位置
         * earliest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費
         * latest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分區下的數據
         * none:topic各分區都存在已提交的offset時,從offset後開始消費;只要有一個分區不存在已提交的offset,則拋出異常
         */
        p.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");

        //是否自動提交offset,默認是true
        p.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(p);
        //kafka 的consumer會動態負載均衡
        consumer.subscribe(Arrays.asList("dll_test_01"), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                System.out.println("---onPartitionsRevoked:");
                Iterator<TopicPartition> iter = partitions.iterator();
                while(iter.hasNext()){
                    System.out.println(iter.next().partition());
                }

            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                System.out.println("---onPartitionsAssigned:");
                Iterator<TopicPartition> iter = partitions.iterator();

                while(iter.hasNext()){
                    System.out.println(iter.next().partition());
                }


            }
        });

        while(true){
            // 拉取數據
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(0));// 0~n

            if(!records.isEmpty()){
                log.info("-----當前拉取的消息數量------{}-------------",records.count());
                // 獲取當前批次拉取的消息分區
                Set<TopicPartition> partitions = records.partitions(); //每次poll的時候是取多個分區的數據
                // 遍歷分區獲取消息數據
                for (TopicPartition partition : partitions) {
                    List<ConsumerRecord<String, String>> pRecords = records.records(partition);
                    Iterator<ConsumerRecord<String, String>> piter = pRecords.iterator();
                    while(piter.hasNext()){
                        ConsumerRecord<String, String> next = piter.next();
                        int par = next.partition();
                        long offset = next.offset();
                        String key = next.key();
                        String value = next.value();
                        long timestamp = next.timestamp();
                        log.info("key: "+ key+" val: "+ value+ " partition: "+par + " offset: "+ offset+"time:: "+ timestamp);
                    }
                }
            }
        }
    }

}

四、offset的維護粒度

  • offset即偏移量,表示消費者的消費進度,對kafka的服務端來說是基於partition來維護的,即分區的數據被某組的某個consumer消費的進度;kafka只關注與數據的存儲和傳輸,不負責加工,不維護狀態,每次從哪開始消費需要consumer提供offset來確定;通過上面的用例我們可以看到消費者消費的時候可選擇是否對ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG屬性進行設置,默認值是true,表示系統自動提交offset,下面分析下自動提交、以消息維度、分區維度和批次維度來維護offset的優缺點
  • 自動提交offset
    即消息者不顯示維護offset,由程序自動在指定時間內提交;這樣做的優點就是降低開發複雜度,缺點就是自動提交不可控,有丟失和重複消費的風險出現
  • 按消息維度 每次消息消費完向服務端發送offset,是最安全可靠的方式,但是明顯會增加與服務端的交互成本,需要結合業務進行取捨
  • 按分區粒度 offset本身在服務端就是按分區維度來維護的,按分區維度提交的話只需保證分區內數據一致性即可;需要注意的是在拉取的批次裏有多個分區的話,如果按分區維度更新offset需要等該批次所有的數據跑完才能進行下批次數據的拉取,否則可能會因爲某個分區消費進度慢沒有來得及更新offset導致重複消費;另外,批次內的每個分區還可以使用多線程並行消費,但是需要確保分區的內的數據都消費完才能更新offset,可以用CountDownLatch來控制使線程全部結束後再更新offset
  • 按每次拉取的批次粒度 按批次的話需要保證整個批次內數據的一致性,會有一定風險,但是如果批次的大小較小的話應該也能控制得當
    也可參照下方的圖示:

五、ACK

kafka的ACK指的是生產者發送消息的確認機制,有三個候選值,分別爲-1,0,1;

  • ack=-1表示消息發送到集羣的leader後,然後同步到各個follower成功後才表示消息發送成功,需要注意的是這裏的各個follower的個數等於ISR的個數,並不需要真的等完全同步完集羣內所有的follower
  • ack=0表示生產者發送完消息無論kafka接收成功與否都視爲發送成功
  • ack=1表示消息發出到leader且磁盤持久化成功表示發送成功,至於是否同步到集羣中其他follower並不關注

0和1的取值一般針對單機纔有意義

數據一致性策略

kafka默認的ack的值爲1,需要注意的是kafka並有做到完全的讀寫分離,寫操作只能發生在leader身上,所以就會涉及到數據同步的問題,數據同步必然會牽扯到數據一致性的問題,下面羅列一下數據一致性的不同策略以及kafka選擇的同步策略;

  • 強一致性
    強一致性即數據發到leader,等所有的follower都同步完成纔是爲消息發送成功;很顯然,強一致性破壞了整個集羣的可用性;
  • 弱一致性
    弱一致性即數據發送到leader即認爲發送成功,不關注其follower是否同步成功,這樣的話數據一致性得不到任何保證
  • 最終一致性
    最終一致性有兩種策略,一是數據發到leader後成功同步到過半的follower即認爲同步成功,二是在leader和follower之間加一個可靠的中間件,主機將需要同步的數據扔到中間件,由follower消費數據直至完全消費從而實現最終一致性

kafka如何實現一致性的

kafka的一致性策略

kafka使用的是最終一致性的策略,不過並不是過半通過或是添加中間件來實現而是利用了ISR來實現的,下面就來介紹下kafka特有的一些術語名詞,看看kafka是如何通過自己的方式保證集羣間消息的一致性的

  • ISR(In-Sync Replicas):能夠和leader保持一致性的所有follower的副本集合,並且包括leader本身
  • AR(Assigned Repllicas):一個分區裏面所有的副本
  • OSR(Out-Sync Replicas):不能在指定時間(默認10秒)內和leader保持一致性的follower的副本集合
  • 公式:AR=ISR+OSR

針對上面的術語解釋做以下補充以便理解:kafka在創建topic的時候就指定了分區的個數和副本數,所以leader在此時就已經維護了broker和分區的關係,並且也知道了每個分區對應分配到哪些broker上了,所以根據ISR、OSR和AR集合就能知悉對應的broker列表

ack等於-1

當ack=-1時,kafka通過ISR集合的數量來取代了過半通過的半加1的數量,ISR可以根據集羣內部同步數據的實際情況動態改變,並且kafka還支持通過設置ISR集合的個數來確定ACK是否確認成功;kafka還需要保證ISR集合中所有副本的消息進度是一致的;

ack等於1

當ack=1時,即消息發送到leader且持久化成功即表示發送成功且並不保證集羣其他follower同步數據成功,所以又衍生出了一堆名詞,下面一一介紹

  • LEO(LogEndOffset)
    我覺得可以理解成功leader中最新消息的offset,之所以拎出來,是因爲此時有可能集羣中其他follower的消息並沒同步到此處
  • HW(Hight Watermark)
    針對上面的LEO,假設存在其他follower同步消息延遲或者失敗的情況,此時集羣對外提供的消息肯定不能到LEO的位置,因爲其他follower可能還沒有該位置的數據,所以此時集羣能對外提供消息位置的offset肯定是整個集羣同步一致的位置,該位置被稱爲HW(即高水位)
  • LW(Low Watermark)
    低水位,kafka保存歷史消息的最早節點,即該節點的之前的數據kafka中已經不存在

六、kafka爲什麼快

kafka的讀取速度快是因爲它的零拷貝機制,即數據不用在磁盤、內核空間、用戶空間拷貝傳遞,而是通過sendFile在內核空間直接返回給消費者;而其寫入快是因爲其是以文件的形式保存到磁盤,且寫入是按順序的,爲了提高查詢的速度其在磁盤除了有數據文件還維護了一份索引文件,祥見下圖

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