從0開始學Kafka(下)

引言

文章相關代碼已收錄至我的github,歡迎star:lsylulu/myarticle
從之前的描述中,可以得知,Producer通過主動Push的方式將消息發佈到Broker,Consumer通過Pull從Broker消費數據。還有這樣設計的動機,本文重點以Consumer爲切入口,瞭解一下其中的API與Rebalance算法。

文章導讀

  • Producer使用簡介
  • High Level Consumer、Consumer Group、Rebalance機制
  • High Level API的使用
  • Low Level Consumer
  • Consumer Offset的管理(Log Compaction,Log Deletion)
  • Kafka高性能實現原理

一、Producer使用簡介

依賴:

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

屬性:

        Properties props=new Properties();
        props.put("bootstrap.servers","kafka0:9042");
        props.put("acks","all");
        props.put("retries",3);
        props.put("batch.size",16384);
        props.put("linger.ms",1);
        props.put("buffer.memory",33554432);
        props.put("key.serializier", StringSerializer.class.getName());
        props.put("value.serializier", StringSerializer.class.getName());
        props.put("partition.class", HashPartitioner.class.getName());

Kafka支持自定義負載均衡。從屬性可以看到我指定的Partitioner,也就是實現了一個Producer對Broker的負載均衡策略。

public class HashPartitioner implements Partitioner {

  public HashPartitioner(VerifiableProperties verifiableProperties) {}

  @Override
  public int partition(Object key, int numPartitions) {
    if ((key instanceof Integer)) {
      return Math.abs(Integer.parseInt(key.toString())) % numPartitions;
    }
    return Math.abs(key.hashCode() % numPartitions);
  }
}

新版的Producer支持send成功後的回調操作,這裏用lambda對其方法進行實現。

        Producer<String, String> producer = new KafkaProducer<String, String>(props);
        for(int i=0;i<10;i++){
            ProducerRecord record=new ProducerRecord<String,String>("topic1",Integer.toString(i));
            producer.send(record,(metadata, exception)-> {
                if(metadata!=null){
                    //輸出成功發送消息的元信息
                    System.out.println(metadata);
                }
                if(exception!=null){
                    exception.printStackTrace();
                }
            });
        }
        producer.close();

二、High Level Consumer、Consumer Group、Rebalance機制

根據Kafka提供的API不同,可以講Consumer劃分爲:High Level Consumer和Low Level Consumer(也叫Simple Consumer)。雖然說0.9版本開始講兩種Consumer合二爲一了,但在API上還是有assign和subscribe的區分的。下面先來看看High Level Consumer。

2.1 High Level API的應用場景

1.很多應用場景下,客戶程序只是希望從Kafka順序讀取並處理數據,而不太關心具體的Offset。High Level API圍繞着Consumer Group這個邏輯概念展開,它屏蔽了每個Topic的每個Partition的Offset管理細節。

2.同時也希望提供一些語義,例如同一條消息只被某一個Consumer消費(單播)或被所有Consumer消費(廣播)。

因此,Kafka High Level API提供了一個從Kafka消費數據的高層抽象,從而屏蔽掉其中的細節,並提供豐富的語義。

High Level Consumer是基於ConsumerGroup來實現的,首先了解一下什麼是Consumer Group。

2.2 Consumer Group

High Level Consumer將從某個Partition讀取的最後一條消息的offset存於Zookeeper中(從0.8.2開始同時支持將Offset存於Zookeeper中和專用的Kafka Topic中)。

思考
爲什麼要支持存儲於專用的Kafka Topic中?
因爲Zookeeper中只有Leader才能處理寫請求,過分依賴Zookeeper會讓其成爲kafka性能上的短板。

這個Offset基於客戶程序提供給Kafka的名字來保存,這個名字被稱爲Consumer Group。Consumer Group是整個Kafka集羣全局唯一的,而不是針對於某個Topic。也就是說,一組Consumer Group共享一個Offset,以此實現消息在同一個Consumer Group中只能被消費一次。每個High Level Consumer實例都屬於一個Consumer Group,若不指定則屬於默認的Group。

說的有些抽象,配合下圖理解能更到位~
img

圖中屏蔽了Follower,Kafaka是消息訂閱系統,所有的值都是順序存儲,消息都是append only到Partition中的,一旦刪除,其他的Consumer Group就無法消費。

到這裏爲止,可以簡單歸納一下ConsumerGroup的一些性質:

  • 消息被消費後,並不會被刪除,只是相應的offset加一
  • 對於每條消息,在同一個Consumer Group裏只會被一個Consumer消費。
  • 不同Consumer Group可消費同一條消息。

擴展閱讀:
Kafka consumer如何加入consumer group

2.3 Consumer的Rebalance算法

我們都知道,在一個ConsumerGroup中,一個Partition中的數據只能由一個Consumer消費。那麼Kafka是如何規定Consumer應該消費哪條Partition的數據呢?合理的分配才能顯得相對均勻。Rebalance算法就是爲這個而生的。

Rebalance的時機

  • Consumer增加或減少;
  • Broker增加或減少。

基於以下控制策略來實現Rebalance的觸發(在Zookeeper中):

  1. 在/consumers/[consumer-group]/下注冊id。
  2. 設置對/consumers/[consumer-group] 的watcher。
  3. 設置對/brokers/ids的watcher。
  4. zk下設置watcher的路徑節點更改,觸發consumer rebalance。

Kafka的Rebalance算法是也是基於Zookeeper來實現的。大概過程是:

  1. 將目標Topic下的所有Partirtion排序,存於PTP_T
  2. 對某 Consumer Group下所有Consumer 排序,存CGC_G,第 i 個Consumer 記爲CiC_i
  3. N=size(PTP_T)/size(CGC_G),向上取整。
  4. 解除CiC_i對原來分配的Partition的消費權(i從0開始)。
  5. 將第i*N到(i+1)*N-1個 partition 分配給CiC_i

潛在問題

羊羣效應(Herd Effect):一個被watch的zk節點變化,導致大量的watcher通知需要被髮送給客戶端,導致在通知期間其他操作延遲。

腦裂(Split Brain):每個Consumer都是通過zk保存的元數據來判斷group中其他各成員的狀態,以及Broker的狀態。由於Zookeeper只保證最終的一致性,所以不同的Consumer在同一時刻可能連接在不同的zk服務器上,看到的元數據就可能不一樣,基於不一樣的元數據,執行Rebalance就會產生不一致的結果。

後續版本Kafka對Rebalance的優化

(1)通過延遲進入Preparing Rebalance狀態減少Reblance次數
一個Group通常包含很多Consumer,當系統啓動時,Consumer陸續加入,採用延遲的方式,讓先加入的Consumer進入Initial Rebalance的狀態,等延遲時間過後,才從Initial Rebalance轉換爲Preparing Rebalance狀態。從而降低Rebalance的頻率。

(2)引入靜態成員ID,Consumer重新加入時,保持舊的標識

運行過程中,Consumer超時或重啓引起的Reblance無法避免,其中一個原因就是,Consumer重啓後,身份標識會變。簡單說就是Kafka不確認新加入的consumer是否是之前掛掉的那個。

在Kafka2.0中引入了靜態成員ID,使得consumer重新加入時,可以保持舊的標識,這樣Kafka就知道之前掛掉的Consumer又恢復了,從而不需要Rebalance。就算髮生了Rebalance,也儘量讓其他Consumer保持原有的Partition,提高重分配的性能。

擴展閱讀:
Kafka Consumer Rebalance
WeCoding:Kafka對reblance的優化,你瞭解嘛

三、 High Level API的使用

High Level API的使用分爲低版本和高版本,是有些區別的。

3.1 低版本High Level Consumer(0.8)

爲了避免配置的冗餘,先講將環境和配置貼出來。

依賴:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.8.2.2</version>
        </dependency>

注:先用0.8版本依賴,高版本的新API後面會提到。

配置:
我覺得寫代碼前還是很有必要會顧一下架構圖的,由於Consumer是通過Zookeeper獲取集羣信息的,因此需要配置ZK的IP+Port。後續還會涉及到與Kafka交互所以會有對應參數的配置。

img

        String topic = "topic1";
        Properties props = new Properties();
        props.put("zookeeper.connect", "192.168.29.100:2181");
        props.put("zookeeper.session.timeout.ms","3600000");
        //調用High Level API必填
        props.put("group.id", "group1");
        props.put("client.id", "consumer1");
        props.put("consumer.id","consumer1");
        //開啓自動提交(默認開啓)
        props.put("auto.commit.enable", "true");
        //初始化從頭開始讀
        props.put("auto.offset.reset", "smallest");
        //自動提交offset偏移量間隔時間修改爲6s(默認60s)
        props.put("auto.commit.interval.ms", "6000");

重點代碼:
裏面重點部分都會有詳細解釋,如果沒有看懂,可以對照下面的解釋看。

        ConsumerConfig consumerConfig = new ConsumerConfig(props);
        ConsumerConnector consumerConnector = Consumer.createJavaConsumerConnector(consumerConfig);

        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
        //key是topic,value是該Topic數據消費的線程數,表示該Topic使用多少個線程進行數據消費操作
        //一般地,分區數==線程數,這樣通常能夠達到最大的吞吐量。
        //超過N的配置只是浪費系統資源,因爲多出的線程不會被分配到任何分區。
        //詳見解釋1
        topicCountMap.put(topic, 1);
        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =
                consumerConnector.createMessageStreams(topicCountMap);
        //拿到KafkaStream其實就相當於拿到對應Topic的數據了
        //解釋2
        KafkaStream<byte[], byte[]> kafkaStream = consumerMap.get(topic).get(0);
        ConsumerIterator<byte[], byte[]> iterator = kafkaStream.iterator();
        while (iterator.hasNext()) {
            //迭代消息,輸出元數據
            MessageAndMetadata<byte[], byte[]> messageAndMetadata = iterator.next();
            String message =
                    String.format("Consumer ID:%s, Topic:%s, GroupID:%s, PartitionID:%s, Offset:%s, Message Key:%s, Message Payload: %s",
                            consumerid,
                            messageAndMetadata.topic(), groupid, messageAndMetadata.partition(),
                            messageAndMetadata.offset(), new String(messageAndMetadata.key()),new String(messageAndMetadata.message()));
            System.out.println(message);
        }

解釋1:
這裏主要解釋”消費者的線程數“是什麼意思。
kafka底層會爲每個topic生成對應的消費線程。從一個blockingQueue中取數據。同時,在後臺爲Kafka的每個Broker生成一個fetch線程拉取消息數據,放入blockingQueue,等待消費,而對於blockingQueue的消費的線程數就是上述的count。

擴展閱讀:
【原創】如何確定Kafka的分區數、key和consumer線程數 - huxihx - 博客園

解釋2:
這裏解釋”consumerMap.get(topic).get(0)“的意思。
這個集合所承載的就是之前定義的消費線程。指定取某一個消費線程,拿出流數據,然後可以遍歷該數據,該方法會是阻塞的。

3.2 高版本High Level Consumer(0.10)

依賴:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.10.1.0</version>
        </dependency>
        <dependency>

配置:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group2");
        props.put("client.id","consumer2");
        props.put("enable.auto.commit","true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());

重點代碼:
這裏做一個解釋,subscribe方法在0.8版本就有,不過並不支持回調函數,也就是ConsumerRebalanceListener。

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
            @Override
            //parition原先被當前consumer消費,經過rebalance後不再被當前consumer消費了,就會調用
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                collection.forEach(topicPartition->{
                    System.out.printf("revoked partition for client %s : %s-%s %n",clientid,topicPartition.topic(),topicPartition.partition());
                });
            }
           
            @Override
            //parition原先不被當前consumer消費,經過rebalance後將分配給當前consumer消費的Partition時調用
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                collection.forEach(topicPartition->{
                    System.out.printf("assign partition for client %s : %s-%s %n",clientid,topicPartition.topic(),topicPartition.partition());
                });
            }
        });
        while(true){
            //從阻塞隊列中取消息,最高延遲爲100ms
            ConsumerRecords<String,String> records=consumer.poll(100);
            //停止對此Topic的partition0消費,同理可用resume方法讓該partition能夠被消費
            consumer.pause(Arrays.asList(new TopicPartition(topic,0)));
            records.forEach(record-> System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value()));
        }

這個代碼明顯比之前哪種方式簡潔,High Level Consumer自動Rebalance,所以不需要指定Partition

四、Low Level Consumer

使用Low Level Consumer (Simple Consumer)的主要原因是,用戶希望比Consumer Group更好的控制數據的消費,“粒度更細”。

4.1 Low Level API的應用場景

1.同一條消息讀多次,方便Replay。
2.只消費某個Topic的部分Partition。
3.管理事務,從而確保每條消息被處理一次(Exactly once)。當讀取某個消息的Consumer失敗了,Offset並沒有加1,下次可以接着讀,保證消息一定被處理一次。

與High Level Consumer相對,Low Level Consumer要求用戶做大量的額外工作。其中包括:

  • 在應用程序中跟蹤處理Offset,並決定下一條消費哪條消息。不會同步到Zookeeper。但是爲了kafka manager方便監控,一般也會手動的同步到Zookeeper上。
  • 獲知每個Partition的Leader。
  • 處理Leader的變化。
  • 處理多Consumer的協作。

4.2 Low Level API的使用

4.2.1 低版本Low Level Consumer(0.8)

依賴:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.8.2.2</version>
        </dependency>

重點代碼:

        final String topic="topic1";
        String clientID="consumer1";
        //設定leader broker的IP,port,timeout,bufferSize
        SimpleConsumer simpleConsumer=new SimpleConsumer("192.168.29.100",9092,100000,64*100000,clientID);
        //這裏設定了topic,partition,offset和fetchSize
        FetchRequest req=new FetchRequestBuilder().clientId(clientID)
                .addFetch(topic,0,0L,100000).addFetch(topic,1,0L,5000).addFetch(topic,2,0L,100000).build();
        //通過封裝的FetchRequest獲取響應結果
        FetchResponse fetchResponse=simpleConsumer.fetch(req);
        ByteBufferMessageSet messageSet=fetchResponse.messageSet(topic,1);
        //遍歷消息
        for(MessageAndOffset messageAndOffset:messageSet){
            ByteBuffer payload=messageAndOffset.message().payload();
            long offset=messageAndOffset.offset();
            byte[] bytes=new byte[payload.limit()];
            payload.get(bytes);
            System.out.println("AotoCommitDemo:"+offset+", payload:"+new String(bytes,"UTF-8"));
        }

擴展閱讀:
0.8.0 SimpleConsumer Example

4.2.2 高版本Low Level Consumer(0.10)

依賴:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.10.1.0</version>
        </dependency>
        <dependency>

配置:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group2");
        props.put("client.id","consumer2");
        props.put("enable.auto.commit","true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());
        props.put("auto.AotoCommitDemo.reset", "earliest");

重點代碼:
以assign的方式,Low Level Consumer,需要指定目標Partiton。

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);

        consumer.assign(Arrays.asList(new TopicPartition(topic,0),new TopicPartition(topic,1)));
        while(true){
            ConsumerRecords<String,String> records=consumer.poll(100);
            records.forEach(record-> System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value()));
        }

五、Consumer Offset的管理

Offset是message的唯一標識符。在前兩章的配置中,我們見過“props.put(“enable.auto.commit”,“true”);”,都是自動提交Offset。這種方式很簡單,但多數情況下,不會使用。因爲不論從Kafka集羣中拉取的數據是否被處理成功,Offset都會被更新,如果執行錯誤可能會出現數據丟失的情況。所以多數情況下我們會選擇手動提交方式。

如果是Low Level Consumer,手工管理offset時需要:

  • 每次從特定Partition的特定offset開始fetch特定大小的消息;
  • 完全由Consumer應用程序決定下一次fetch的起始offset

5.1 Commit的手動提交方式

下面就展示了手動同步和異步提交的方式,以High Level Consumer爲例:

屬性:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group1");
        props.put("client.id","consumer1");
        props.put("enable.auto.commit","false");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());
        props.put("max.poll.interval.ms", "300000");
        props.put("max.poll.records", "500");
        props.put("auto.offset.reset", "earliest");
        //將offset存儲到Kafka的Topic
        props.put("offset.storage","kafka");
        //自動存儲到對應的介質中
        props.put("dual.commit.enabled","true");

重點代碼:

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(topic));
        AtomicLong atomicLong=new AtomicLong();
        while(true){
            ConsumerRecords<String,String> records=consumer.poll(100);
            records.forEach(record->{
                System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value());
//                //1.同步commit
//                if(atomicLong.get()%10==0){
//                    //組的commit上一次消費的offset
//                    consumer.commitSync();
//                }
                //2.異步commit
                if(atomicLong.get()%10==0){
                    //提供了異步的回調操作,如果commit成功,則執行該方法
                    //解釋1
                    consumer.commitAsync((Map<TopicPartition, OffsetAndMetadata> offsets,Exception exception)->{
                        offsets.forEach((TopicPartition partition,OffsetAndMetadata offset)->{
                            System.out.printf("commit %s-%d-%d %n",partition.topic(),partition.partition(),offset.offset());
                            offset.offset();
                        });
                        if(null!=exception){
                            exception.printStackTrace();
                        }
                    });
                }
            });
        }

解釋1:
採用異步提交可以通過匿名內部類的方式進行回調,實現了OffsetCommitCallback中的

void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception);

方法,這裏用lambda表達式實現,因此如果commit成功,又由於是批量提交的,就遍歷這些成功提交的Offset。

擴展閱讀:
Kafka Client API 基本使用

5.2 Kafka對Offset的Log管理

Kafka會對每次消費的Offset,key,Value做記錄。這些日誌可能用於大數據的數據聚合或者容災恢復,它的存儲還是很有必要的。但是log的體積隨着時間的推移會越來越大,如果不及時清理,很可能造成對內存的佔用。可以同過Topic的cleanup.policy設置清理策略。默認爲delete,也可設置爲compaction,也可同時設置。

5.2.1 Log Compaction

主要思想是:系統關心的是它原本的最新狀態而不是歷史時刻中的每一個狀態。下面看一下Log Compaction的過程:

img

圖片來源於網絡

Log Compaction對於有相同key的不同value值,只保留最後一個版本。如果應用只關心key對應的最新value值,可以開啓Kafka的日誌清理功能,Kafka會定期將相同key的消息進行合併,只保留最新的value值。

整體過程是:
前提
Kafka中的每個日誌清理線程使用名爲“SkimpyOffsetMap”的對象來構建key與offset的映射關係的哈希表。

(1)日誌清理需要遍歷兩次日誌文件,第一次遍歷把每個key的哈希值和最後出現的offset都保存在SkimpyOffsetMap中,映射模型如下圖所示。

(2)第二次遍歷檢查每個消息是否符合保留條件,如果符合就保留下來,否則就會被清理掉。假設一條消息的offset爲O1,這條消息的key在SkimpyOffsetMap中所對應的offset爲O2,如果O1>=O2,說明在O1在O2之後進行消費,即爲滿足保留條件。

擴展閱讀:
https://blog.csdn.net/u013256816/article/details/80487758

5.2.2 Log Deletion

之前的Log Compaction是以key爲維度來保留的,而Log Deletion是按照條件直接刪除不符合的所有日誌。
調度策略由broker端參數log.retention.check.interval.ms,默認爲5分鐘。也就是5分鐘掃描一次看看有沒有符合條件的log就刪除。這個條件包括,時間,日誌大小和日誌的起始偏移量。

基於時間

日誌刪除任務會掃描超過閾值時間(retentionMs)的過期日誌,閾值時間可通過可以通過broker端參數log.retention.hours、log.retention.minutes以及log.retention.ms來配置。系統會轉換成時間戳來計算,如果刪除的日誌分段總數是所有的日誌分段的數量時,必須要保證有一個活躍的日誌分段activeSegment。會先切分出一個新的日誌分段作爲activeSegment,然後再執行刪除操作。

基於日誌大小

日誌刪除任務會掃描超過設定的retentionSize的日誌,加入可刪除的日誌分段的文件集合deletableSegments。retentionSize可以通過broker端參數log.retention.bytes來配置,表示日誌文件的總大小,默認值爲-1,表示無窮大。刪除任務會先計算日誌的總大小和當前日誌大小的差值。然後從日誌文件中的第一個日誌分段開始進行查找可刪除的日誌分段的文件集合。

基於日誌起始偏移量

簡單來說,就是判斷日誌分段的下一個日誌分段的起始偏移量baseOffset是否小於等於logStartOffset,若是則刪除偏移量之前的日誌分段。

擴展閱讀:
https://blog.csdn.net/u013256816/article/details/80418297

六、Kafka高性能實現原理

6.1 高效使用磁盤

1.順序寫磁盤
順序寫磁盤性能高於隨機寫內存。

2.Append Only
數據不更新無記錄級的數據刪除(只會刪除整個segment)。

3.充分利用Page Cache

  • I/O Scheduler將連續的小塊寫組裝成大塊的物理寫從而提高性能。
  • I/O Scheduler會嘗試將一些寫操作重新按順序排好,從而減少磁盤頭的移動時間。
  • 充分利用所有空閒內存(非JVM內存)。
    應用層cache也會有對應的page cache與之對應,直接使用page cache可增大可用cache。如使用heap內的cache,會增加GC負擔。
  • 讀操作可直接在page cache內進行。如果進程重啓,JVM內的cache會失效,但page cache仍然可用。
  • 可通過如下參數強制flush,但並不建議這麼做
    log.flush.interval.messages=10000
    log.flush.interval.ms=1000

4.支持多Directory(可使用多Drive)
充分利用多磁盤的優勢。

6.2 利用Linux零拷貝

涉及方法:

  • File.read(fileDesc,buf,len)
  • Socket.send(socket,buf,len)

傳統模式

傳統模式下數據從文件傳輸到網絡需要4次數據拷貝4次上下文切換2次系統調用

img

4次數據拷貝:DMA將數據從磁盤中拷貝到內核的read緩衝區;CPU將讀緩存拷貝到應用緩存中;CPU將數據拷貝到內核的Socket緩衝區;DMA將緩衝區中的數據拷貝到網卡的緩衝區中(NIO buffer)。

4次上下文切換:剛開始由用戶態切換到內核態DMA讀取數據到緩衝區;由內核態切換到用戶態進行CPU拷貝到應用程序;由用戶態到內核態拷貝到內核的socket緩衝區;最終拷貝結束由內核態切換回用戶態。

2次系統調用:系統調用read;系統調用socket。

零拷貝

通過NIO的transferTo/transferFrom調用操作系統的sendfile實現零拷貝。

img

總共發生2次內核數據拷貝2次上下文切換1次系統調用,消除了CPU數據拷貝。

6.3 批處理和壓縮

Producer和Consumer均支持批量處理數據,消息按條數積累或者按時間的積累從而減少了網絡傳輸的開銷(比如異步Commit就是批處理)。
Producer可將數據壓縮後發送給Broker,從而減少網絡傳輸代價。目前支持Snappy, Gzip和LZ4壓縮。

6.4 Partition實現高性能

通過Partition實現了並行處理和水平擴展。

1.Partition是Kafka(包括Kafka Stream)並行處理的最小單位。high level api就是一個partition只能被一個consumer消費,partition越多,並行度就越高。

2.不同Partition可處於不同的Broker(節點),充分利用多機資源。

3.同一Broker(節點)上的不同Partition可置於不同的Directory,如果節點上
有多個Disk Drive,可將不同的Drive對應不同的Directory,從而使Kafka充分利用多Disk Drive的磁盤優勢。

6.5 ISR實現一致性,持久性之間的動態平衡

1.ISR實現了可用性和一致性的動態平衡

2.ISR可容忍更多的節點失敗

  • Majority Quorum如果要容忍f個節點失敗,則至少需要2f+1個節點。
  • ISR如果要容忍f個節點失敗,至少需要f+1個節點。

3.如何處理Replica Crash

  • Leader crash後,ISR中的任何Replica皆可競選成爲Leader。
  • 如果所有Replica都Crash,可選擇讓第一個Recover的Replica或者第一個在ISR中的Replica成爲Leader。
  • 配置:unclean.leader.election.enable=true

總結

總體上來說,內容還是挺豐富的,如果有錯誤,歡迎指出。
本文適合加入收藏點贊系列~

參考文章:
Kafka Consumer Rebalance
https://blog.csdn.net/silviakafka/article/details/77162075
Kafka如何實現每秒上百萬的高併發寫入
WeCoding:Kafka對reblance的優化,你瞭解嘛
Kafka Client API 基本使用
https://blog.csdn.net/u013256816/article/details/80487758
https://blog.csdn.net/u013256816/article/details/80418297
白天不懂夜的黑:Kafka史上最詳細原理總結

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