《Apache kafka實戰》學習總結及代碼實操(未完--持續更新)

Kafka高性能高吞吐的原因:

kafka是作爲消息中間件、流處理平臺、消息引擎性能吞吐量均是是目前市面第一,得益於它的消息讀寫的方式

一、延時批量發送消息

Kafka不會一有消息就會進行發送,而延遲到固定的時間,生產者可以將多個發送到同一個分區的消息放到同一個批次內積累起來,當批次大小填滿(通過batch.size設置批次大小)或者是到達了超時時間(通過linger.ms設置超時時間)就會進行發送,通過批量的發送減小了發送的次數,有效的減少網絡消耗,這也是kafka高吞吐的主要原因。

二、頁緩存

kafka的寫入和讀取都是先通過操作頁緩存,頁緩存操作的是內存,寫時kafka只會將消息寫入頁緩存,然後由操作系統決定什麼時候將數據寫入磁盤,讀消息時kafka也是先到頁緩存中撈數據,命中則直接返回,大部分情況kafka都能從也緩存中讀取到數據,所以kafka的讀取和寫入都是操作內存速度非常快,得益於JVM對象的朝生夕死,大部分的情況下,kafka只需要6G的JVM內存就可以完成正常的工作,其他的內存可以分配給kafka的頁緩存

三、磁盤順序讀寫

機械磁盤順序讀寫比內存的隨機讀寫和固態硬盤的順序讀寫還要快,所以kafka寫入消息的操作是採用的追加寫入(append)的方式進行,即只能在日誌文件的末尾追加寫入新的消息,並且不能修改已經寫入的消息,這就是kafka的對磁盤的順序訪問,可以很輕鬆的實現每秒幾十萬消息的寫入量,這是kafka高性能的重要原因
四、sendfile零拷貝

kafka讀取消息先是從緩存頁中讀取消息,如果命中就直接將消息經緩存頁交由網絡的socket緩存中進行發送,不必經過用戶態和內核態的切換,節省了內核緩衝區與用戶態應用程序緩衝區之間的數據拷貝的消耗,減少了CPU開銷,這是kafka高吞吐高性能的主要原因
五、分區機制

kafka通過將Topic進行拆分,分成一個個partition,partition的leader通常會均勻的分配到不同的broker上,這樣將消息的讀寫壓力分配給了集羣的各個broker上,充分的利用了集羣的資源

Kafka負載均衡和故障轉移:

kafka通過將Topic進行拆分,分成一個個partition,通常還會對partition創建replica(replica數量不能超過broker的數量-1),每個partition leader和他們的replica會組成一個ISR,ISR中有leader和follower的角色,follower作爲leader的備份負責將partition leader複製備份,leader負責處理客戶端請求,ISR裏的每個成員會均勻的落到broker中,ISR中的每個成員的id是所在當前broker的編號,也就是說同一個ISR的成員不能在同一個broker中,這樣就避免了一個broker掛了,其他broker上的follower會頂替掛掉的broker上的leader,所以ISR的成員要分佈在不同的broker中。

Kafka還會將同一個Tocpic下的partition中作爲leader的partition均勻的分配到不同的broker中,這樣可以使一個broker不至於負載過熱,使kafka充分的利用集羣的資源,實現了負載均衡

而kafka是不需要保存集羣狀態的,kafka依賴Zookeeper協調服務來獲取集羣的狀態,每個broker會通過臨時會話機制將自己的元數據保存在Zookeeper的臨時節點中,通過通過心跳來檢查broker的狀態,通過zookeeper的通知集羣可以相互通信共享狀態,當集羣中的某個broker掛了,zookeeper可以通知broker controller進行新的partition leader的選舉,這樣就達到了故障轉移的效果

生產者 Producer:

kafka的消息是由key,value,timestamp組成,其中value是消息體,保存實際的消息數據。timestamp是消息發送的時間戳。
key是消息鍵,它主要起到了路由的作用,在Apache kafka producer中提供了默認的分區器,如果指定了固定的key值就會取key的哈希值來決定路由到哪個partition中,如果沒有指定key則會默認使用輪詢的方式均勻的將消息發送到partition中,當然我們可以自定義分區器

這是生產者的流程

kafka的生產者客戶端提供了異步發送和同步發送
代碼演示

導入依賴

 <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-streams</artifactId>
            <version>2.4.0</version>
        </dependency>

先定義好參數

private final static Properties properties = new Properties();
static{
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.220.128:9092");
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,"3");
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
properties.put(ProducerConfig.LINGER_MS_CONFIG,"1");
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
}

異步發送,發送後就不管發送的結果如何,不推薦

/*
        Producer異步發送演示
     */
    public static void producerSend(){
        // Producer的主對象
        Producer<String,String> producer = new KafkaProducer<>(properties);

        // 消息對象 - ProducerRecoder
        for(int i=0;i<10;i++){
            ProducerRecord<String,String> record =
                    new ProducerRecord<>(TOPIC_NAME,"key-"+i,"value-"+i);

            producer.send(record);
        }

        // 所有的通道打開都需要關閉
        producer.close();
    }

實際上kafka的send方法是個異步的過程,好在它提供了結果回調這種非阻塞式的方法,通過onCompletion回調可以知道發送的結果

在onCompletion方法中的參數RecordMetadata 和Exception不會同時爲null,如果Exception爲null或者RecordMetadata 不爲null代表消息發送成功,如果RecordMetadata爲null或者Exception不爲null代表消息發送失敗

 /*
        Producer異步發送帶回調函數
     */
    public static void producerSendWithCallback(){
        // Producer的主對象
        Producer<String,String> producer = new KafkaProducer<>(properties);
        // 消息對象 - ProducerRecoder
        for(int i=0;i<10;i++){
            ProducerRecord<String,String> record =
                    new ProducerRecord<>(TOPIC_NAME,"key-"+i,"value-"+i);
            @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e==null){
                        System.out.println("消息發送成功");
                        System.out.println("partition : "+recordMetadata.partition()+" , offset : "+recordMetadata.offset());
                    }else {
                        System.out.println("消息發送失敗,執行失敗處理,一般存庫或者重發");
                    }
                   
                }
        }

        // 所有的通道打開都需要關閉
        producer.close();
    }

kafka的send方法通過future可以實現異步阻塞的方式

 /*
        Producer異步阻塞發送
     */
    public static void producerSyncSend() throws ExecutionException, InterruptedException {
        // Producer的主對象
        Producer<String,String> producer = new KafkaProducer<>(properties);
        // 消息對象 - ProducerRecoder
        for(int i=0;i<10;i++){
            String key = "key-"+i;
            ProducerRecord<String,String> record =
                    new ProducerRecord<>(TOPIC_NAME,key,"value-"+i);
            Future<RecordMetadata> send = producer.send(record);
            RecordMetadata recordMetadata = send.get();
            System.out.println(key + "partition : "+recordMetadata.partition()+" , offset : "+recordMetadata.offset());
        }

        // 所有的通道打開都需要關閉
        producer.close();
    }

失敗重試

(前段時間我遇到一個弱智面試官,我說kafka可以支持錯誤重試,他就把我pass掉了,說kafka是不支持重試的,我反駁了他還不接受,我想一錘子敲死他,不懂還當什麼面試官,真是出來禍害社會)
無論發送方式是異步還是同步的,都會發送錯誤,錯誤包括了可重試錯誤和不可重試錯誤,對於可重試的錯誤,如果在kafka可以在重新發送,如果重新發送的次數在配置的次數還沒有成功就會在回調函數nCompletion的Exception打印錯誤信息,這時需要我們手動處理,一般可以進行存庫記錄發送失敗的信息,重試配置是

下面是處理重試錯誤的代碼

//先把重試次數設置爲3
properties.put(ProducerConfig.RETRIES_CONFIG,"3")

/*
        Producer異步發送帶回調函數
     */
    public static void producerSendWithCallback(){
        // Producer的主對象
        Producer<String,String> producer = new KafkaProducer<>(properties);
        // 消息對象 - ProducerRecoder
        for(int i=0;i<10;i++){
            ProducerRecord<String,String> record =
                    new ProducerRecord<>(TOPIC_NAME,"key-"+i,"value-"+i);
            @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e==null){
                        System.out.println("消息發送成功");
                        System.out.println("partition : "+recordMetadata.partition()+" , offset : "+recordMetadata.offset());
                    }else {
                        if (e instanceof RetriableException){
                            System.out.println("可重試錯誤");
                        }else {
                            System.out.println("不可重試錯誤");
                        }
                        System.out.println("消息發送失敗,執行失敗處理,一般存庫或者重發");
                    }
                   
                }
        }

        // 所有的通道打開都需要關閉
        producer.close();
    }

自定義分區器

在Apache kafka producer中提供了默認的分區器,如果指定了固定的key值就會取key的哈希值來決定路由到哪個partition中,如果沒有指定key則會默認使用輪詢的方式均勻的將消息發送到partition中,當然我們可以自定義分區器

代碼演示

需要繼承Partitioner

public class SamplePartition implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        String keyStr = key + "";
        String keyInt = keyStr.substring(4);
        int i = Integer.parseInt(keyInt);
        return i%2;
    }
    @Override
    public void close() {
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}

定義好我們的分區器後,需要給其添加到配置屬性中,這樣我們就可以按照我們的分區器進行消息路由了

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.example.kafka.producer.SamplePartition");

生產者的參數

batch.size:batch.size 是 producer 最重要的參數之一 ! producer 會將發往同一分區的多條消息封裝進一個 batch 中。當 batch 滿了的時候, producer 會發送 batch 中的所有消息 。不過, producer 並不總是等待 batch 滿了才發送消息,很有可能當 batch 還有很多空閒空間時 但是超過了linger.ms設置的時間producer 就發送該 batch 。
linger.ms: linger.ms 參數就是控制消息發送延時行爲的 。 該參數默認值是 0 ,表示消息需要被立即 發送,無須關心 b atch 是否己被填滿
ack:
0: 表示 producer 從來不等待來自 broker 的確認信息。這個選擇提供了最小的時延但同時風險最大
1:表示獲得 leader已經接收了數據的確認信息,延遲一般。kafka默認的配置,這個方案比較適中
-1:producer 會獲得所有同步 replicas 都收到數據的確認。同時時延最大
retries:重試次數,默認爲0,設置該參數爲一個大於 0 的值。 只不過在考慮 retries 的設置時,有兩點需要着重注意 。
1、重試可能造成消息的亂序:比如由於瞬時的網絡抖動使得 broker 端己成功寫入 消息但沒有成功發送響應給 producer ,因此 producer 會認爲消息發送失敗,從而開啓 重試機制
2、重試可能造成消息的重複發送:
buffer.memory:producer 端用於緩存消息的緩衝區大小,單位是字節,默認值是 33554432,即 32MB 。由於採用了異步發送消息的設計架構, Java 版本 producer 啓動時會首先 創建一塊 內存緩衝區用於保存待發送的消息,然後由另 一個專屬線程負責從緩衝區中讀取消息 執行真正的發送 。這部分 內存空間的大小即是由 buffer.memory 參數指定的。若 producer 向 緩 衝區寫消息的速度超過了專屬 I/O 線程發送消息的速度,那麼必然造成該緩衝區空間的不斷增 大。此時 producer 會停止手頭的工作等待 1/0 線程追上來,若一段時間之後 1/0 線程還是無法 追上 producer 的進度,那麼 producer 就會拋出異常並期望用戶介入進行處理。
compression.type:設置 producer 端是否壓縮消息,默認值是 none ,即不壓縮消息 。和
任何系統相同的是, Kafka 的 producer 端引入壓縮後可以顯著地降低網絡 νo 傳輸開銷從而提 升整體吞吐量,但也會增加 producer 端機器的 CPU 開銷 。另 外,如果 broker 端的壓縮參數設 置得與 producer 不同, broker 端在寫入消息時也會額外使用 CPU 資源對消息進行對應的解壓 縮-重壓縮操作。
max.request.size:該參數用於控制 producer 發送請求的大小 。 實際上該參數控制的是
producer 端能夠發送的最大消息大小。默認的 1048576 字節太小 了 , 通常無法滿足 企業級消息的大小要求 。
request.timeout.ms:當 producer 發送請求給 broker 後 , broker 需要在規定 的時 間範圍 內 將處理結果返還給producer 。默認是 30 秒 。 這就是說,如果 broker 在 30 秒內都 沒有給 produc er 發送響應,那麼 produc er 就會認爲該請求超時了,並在回調函數中顯式地拋出 TimeoutException 異常交由用戶處理。

消費者 consumer

消費者使用一個消費者組名(即 group.id )來標記自己, topic 的每條消息 都只會被髮送到每個訂閱它的消費者組的一個消費者實例上。
①一個 consumer group 可能有若干個 consumer 實例( 一個 group 只有一個實例也是允許的);
②對於同一個 group 而言, topic 的每條 消息只能被髮送到 group 下的一個 consumer 實例上;
③topic 消息可以被髮送到多個 group 中 ;

Offset

Kafka中有兩個Offset,一個是partition的Offset,一個是consumer的offset
partition的Offset:partition 上的每條消息都會被分配一個唯一 的序列號。該序列號是從 0 開始順序遞增的整數。位移信息可以唯一定位到某 partition 下的一條消息。

consumer的offset:每個 consumer 實例都會爲它消費的分區維護屬於自己的位置信息來記錄當前 消費了多少條消息 。在以前是把offset保存在了zookeeper中,而consumer 把offset提交到 Kafka 的一個內部 topic (__consumer_offset),所以現在consumer可以不用連接zookeeper

負載

kafka分配分區的策略有三種range,round-robin,sticky,默認使用range策略給consumer分配分區,通過分區對組的分配可以達到負載均衡的效果

消費者代碼演示

定義參數

private static final Properties props = new Properties();
static{
 //連接哪個kafka服務器
        props.setProperty("bootstrap.servers", "192.168.220.128:9092");
        props.setProperty("group.id", "test");
        props.setProperty("enable.auto.commit", "true");
        props.setProperty("auto.commit.interval.ms", "1000");
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
}
 public static void simpleConsumer(){
        KafkaConsumer<String,String> consumer = new KafkaConsumer(props);
        // 消費訂閱哪一個Topic或者幾個Topic
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true) {
            //每一萬毫秒拉取一次數據
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            for (ConsumerRecord<String, String> record : records)
                System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                        record.partition(),record.offset(), record.key(), record.value());
        }
    }
提交offset

默認情況下消費者會每隔5秒進行自動提交offset,如果想處理完業務再提交,可以設置手動提交,手動提交也是避免消息未消費是有效手動

設置手動提交

props.setProperty("enable.auto.commit", "false");
 private static void commitedOffset() {
   
        KafkaConsumer<String, String> consumer = new KafkaConsumer(props);
        // 消費訂閱哪一個Topic或者幾個Topic
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            for (ConsumerRecord<String, String> record : records) {
                // 想把數據保存到數據庫,成功就成功,不成功...
                // TODO record 2 db
                System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                        record.partition(), record.offset(), record.key(), record.value());
                // 如果失敗,則回滾, 不要提交offset
            }
           if (records.isEmpty()){
                System.out.println("消費失敗");
            }
            // 手動通知offset提交
            consumer.commitAsync();
        }
    }

ISR同步機制

每個partition 都有一個ISR列表,ISR中follower會向leader發送同步請求,leader副本會向follower副本推送數據,follower通過寫入推送的數據完成數據的同步,由於網絡等問題follower的數據可能會落後leader。
在舊版本的kafka中是通過判斷follower的數據落後leader到達一定的量就將其剔除,默認是4000條,這造成的問題是:假設落後4000條消息就剔除,當某個流量高峯期,每次生產者都發送5000條,那麼leader每次都會剔除follower,就算是follower本身沒有任何問題
新版本的kafka是通過落後時間去剔除,默認是10秒,follower無論落後多少隻要在10秒內趕上leader即可,如果持續落後10秒就會被剔除

HW(高水印)和LEO(日誌末端位移)

在ISR通過HW和LEO去比較同步情況,在ISR中每個成員都要去維護自己的HW和LEO

  • LEO:在leader和follower都代表了自身的所有消息的位移,在leader中生產者每發送一條消息LEO就會加1,對於follower來說LEO代表每成功寫入一條leader推送過來的數據就會將其LEO加1
  • HW:在leader中HW的值等於ISR成員裏最慢的那個follower的複製量,也就是等於最慢的那個follower的LEO,也是消費者可消費消息的最大範圍,通常可以將HW理解爲“已備份的”或者是“已提交的”,它可以起到限制消費者消息的消息最大位移的作用。而在follower中,最落後的follower的HW值和自身的LEO值相等並且等於leader的HW,如果follower不是最落後的,其HW值是小於自身的LEO值並且等於leader的HW

在ISR的同步中,leader會等最慢的那個follower的LEO加1,leader的HW纔會加1,所以生產者發送的消息不一定能全部能提供給消費者消息,必須ISR中其他成員都備份到了這個數據纔可以提供消費者消息,也就是說在leader中HW之間LEO是屬於有消息卻不能消費的數據,因爲他們未被所有的follower備份。如果最落後的follower實在太落後就會將其剔除,如果ISR中leader之間的備份非常的及時,那麼ISR中所有成員的HW和LEO都是相等的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章