Kafka詳細教程加面試題

女友在高考發表於2021-09-06

一、部署kafka叢集

啟動zookeeper服務:

zkServer.sh start

修改配置檔案config/server.properties

#broker 的全域性唯一編號,不能重複
broker.id=0
#刪除 topic 功能使能
delete.topic.enable=true
#處理網路請求的執行緒數量
num.network.threads=3
#用來處理磁碟 IO 的現成數量
num.io.threads=8
#傳送套接字的緩衝區大小
socket.send.buffer.bytes=102400
#接收套接字的緩衝區大小
socket.receive.buffer.bytes=102400
#請求套接字的緩衝區大小
socket.request.max.bytes=104857600
#kafka 執行日誌存放的路徑
log.dirs=/opt/module/kafka/logs
#topic 在當前 broker 上的分割槽個數
num.partitions=1
#用來恢復和清理 data 下資料的執行緒數量
num.recovery.threads.per.data.dir=1
#segment 檔案保留的最長時間,超時將被刪除
log.retention.hours=168
#配置連線 Zookeeper 叢集地址
zookeeper.connect=localhost:2181

配置環境變數

vi /etc/profile

#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin

source /etc/profile

啟動kafka服務:
cd /usr/local/kafka/

nohup bin/kafka-server-start.sh config/server.properties &

建立topic

bin/kafka-topics.sh --create --zookeeper 192.168.1.12:2181,192.168.1.12:2181,192.168.1.14:2181 --replication-factor 1 --partitions 1 --topic mmc

檢視topic

bin/kafka-topics.sh --zookeeper localhost:2181 --list

檢視topic詳情

bin/kafka-topics.sh --describe --topic mmc --zookeeper localhost:2181

產生訊息:

bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test

接收訊息:

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning

二、Kafka架構

kafka沒有實現JMS協議,但其消費組可以像點對點模型一樣讓訊息被一組程式處理,同時也可以像釋出/訂閱模式一樣,讓你傳送廣播訊息到多個消費組。

簡單來說:一個消費組就是點對點,多個消費組就能實現釋出、訂閱。

一個Topic可以有多個分割槽,每個分割槽是一個有序的,不可變的訊息序列。新的訊息不斷追加,同時分割槽會給每個訊息記錄分配一個順序ID號 – 偏移量。儘管記錄被消費了,也不會馬上刪除,只是移動偏移量,Kafka會有可配置的保留策略刪除(預設7天)。

Kafka只保證一個分割槽內的訊息有序,不能保證一個主題的不同分割槽之間的訊息有序。但是,如果你想要保證所有的訊息都絕對有序可以只為一個主題分配一個分割槽,雖然這將意味著每個消費群同時只能有一個消費程式在消費。

分割槽策略

生產者傳送訊息後會進入哪個分割槽?

  • 使用者可以指定訊息的分割槽
  • 也可以指定key,系統根據key的hash值取模得到分割槽
  • 如果使用者不指定分割槽,也不宣告key,那麼系統會自動生成key,並根據自動生成的key進行hash之後取模然後算出分割槽

2.2 儲存架構

  • 將一個topic的多個parition大檔案分為多個小檔案段(segment)儲存。segment檔案由兩部分組成,分別為“.index”檔案和“.log”檔案,分別表示索引檔案和資料檔案
  • 通過索引檔案可以快速定位到message和確定response的最大大小。

00000000000000170410.log這個檔案記錄了第170411到~(下一個log檔案編號)的訊息。圖中第三條訊息對應的是348,也就是說在log檔案中,第三條訊息的偏移量是348.

三、保證資料可靠性

3.1 副本同步策略

一般有兩種方案,Kafka選擇了第二種。

方案 優點 缺點
半數以上完成同步,傳送ack 延遲低 選取新的leader時,容忍n個節點故障時,必須要有2n+1個副本
全數以上完成同步,傳送ack 選取新的leader時,容忍n個節點故障時,需要n+1個副本 延遲高

3.2 ISR

ISR(In-sync replica set)意為與leader保持同步的follower集合。當ISR中的follower完成與leader的資料同步時,向生產者傳送ask。如果在規定的時間內(replica.lag.time.max.ms 此引數設定)follower未同步資料,則踢出ISR。leader出現故障後,就在ISR佇列裡選舉。

3.3 ack應答機制

通過設定request.required.acks應答來保證。有如下三種設定方式

  • 1(預設):代表producer在ISR中的leader成功接收到資料並確認時,繼續傳送下一條資料。如果leader當機,則丟失資料
  • 0:無需確認則直接傳送下一條,可靠性最低
  • -1:等待producer在ISR中的所有follower確認再傳送下一條。此時訊息副本數越多則可靠性越高。

3.4 故障處理細節

LEO:每個副本最大的offset

HW:消費者能見到的最大的offet,ISR中最小的LEO

(1)follower故障時

follower發生故障後會被臨時踢出ISR,待該follower重啟後,follower會讀取本地磁碟記錄的上次的HW,然後將他log檔案中高於HW的部分截掉,然後從leader開始同步,直到該follower的LEO大於或等於該Partition的HW,就可以重新加入ISR。

(2)leader故障時

leader發生故障後,會重新選取一個leader,為了保證多個副本的資料一致性,其餘的follower會將高於HW的部分截掉,然後從新leader那裡同步

Exactly Once 語義

當ack設定為-1時,可以保證producer到Server之間不丟資料,即至少一次

而ack設定為0,則以保證訊息至多一次。

而對於某些非常重要的訊息,要保證既不丟失又不重複,即Exactly Once語義。在 0.11 版本以前的 Kafka是做不到的。0.11 版本的 Kafka,引入了一項重大特性:冪等性。所謂的冪等性就是指Producer不論向 Server傳送多少次重複資料,Server 端都只會持久化一條。冪等性結合 At Least Once 語
義,就構成了 Kafka 的 Exactly Once 語義。即:

At Least Once + 冪等性 = Exactly Once

要啟用冪等性,將enable.idompotence 設定為 true 即可

實現方式;開啟冪等性的Producer在初始化的時候會分配一個PID,發往同一個Partition的訊息會附帶Sequence Number。而Broker端會對<PID,Partition,SeqNumber>做快取,當相同主鍵的訊息提交時,只會持久化一條。但是PID重啟就會發生變化,不同的Partition也具有不同的主鍵,所以他的冪等性無法保證跨分割槽跨會話。

四、消費者

4.1 消費方式

訊息是採用的pull的方式,pull方式的不足之處是如果沒有資料,會造成空輪詢,針對這一點,Kafka的消費者在消費資料時會傳入一個時長引數 timeout,如果當前沒有資料可供消費,consumer會等待一段時間之後再返回,這段時長即為 timeout。

4.2 分割槽分配策略

Kafka訊息消費的時候,有兩種策略,RoundRobin和Range

4.3 消費者Offset位置儲存

Offset是以消費者組+Topic+Partatition為key來儲存的。

0.9版本前儲存在Zookeeper裡

0.9版本之後儲存在Kafka內建的一個Topic中,該topic為__consumer_offsets

5.1 Kafka高效讀寫資料

  1. 順序寫磁碟

順序寫可達到600M/s,而隨機寫只有100K/s
2. 零拷貝技術

5.2 Kafka事務

Kafka在0.11版本後引入了事務支援。事務可以保證訊息正好一次語義的基礎上,生產和消費可以跨分割槽和會話。

為了實現跨分割槽跨會話的事務,需要引入一個全域性唯一的 Transaction ID,並將 Producer獲得的PID和Transaction ID 繫結。這樣當Producer 重啟後就可以通過正在進行的Transaction ID 獲得原來的 PID。

六、Kafka API

6.1 Producer API

訊息傳送過程

程式碼示例

package com.mmc.springbootstudy.kafka;

import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;

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

/**
 * @description:
 * @author: mmc
 * @create: 2021-04-22 21:22
 **/

public class ProductDemo {

    public final static String TOPIC = "mmc";


    /**
     * 不帶回撥的傳送API
     */
    public void send() {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("acks", "all");
        props.put("retries", 3);
        props.put("batch.size", 16384);
        props.put("key.serializer", StringSerializer.class.getName());
        props.put("value.serializer", StringSerializer.class.getName());
        String key="test";
        String value="我是一個小紅花222";
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
        producer.send(new ProducerRecord<String, String>(TOPIC,key,value));
        producer.close();
    }

    /**
     * 帶回撥的傳送
     */
    public void callSend() {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("acks", "all");
        props.put("retries", 3);
        props.put("batch.size", 16384);
        props.put("key.serializer", StringSerializer.class.getName());
        props.put("value.serializer", StringSerializer.class.getName());
        String key="test";
        String value="我是一個小紅花222";
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
        producer.send(new ProducerRecord<String, String>(TOPIC, key, value), new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if(e == null){
                    System.out.println("傳送成功");
                }else {
                    e.printStackTrace();
                }
            }
        });
        producer.close();
    }

    /**
     * 同步傳送API
     * 一條訊息傳送之後,會阻塞當前執行緒,直至返回 ack。
     */
    public void syncSend() throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("acks", "all");
        props.put("retries", 3);
        props.put("batch.size", 16384);
        props.put("key.serializer", StringSerializer.class.getName());
        props.put("value.serializer", StringSerializer.class.getName());
        String key="test";
        String value="我是一個小紅花222";
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
        RecordMetadata recordMetadata = producer.send(new ProducerRecord<String, String>(TOPIC, key, value)).get();
        System.out.println("----------recordMetadata:"+recordMetadata);
        producer.close();
    }

    public static void main( String[] args ) throws ExecutionException, InterruptedException {
//        new ProductDemo().send();
        new ProductDemo().syncSend();
    }
}

KafkaProducer 物件是比較重的,並且他是執行緒安全的,所以可以全域性都用同一個物件去發訊息。

6.2 Consumer API

消費者消費的時候有區分自動提交、手動同步提交和手動非同步提交。手動同步提交會阻塞當前執行緒直到成功提交,並有失敗重試。而非同步手動提交沒有失敗重試。

不管是同步提交還是非同步提交,都會可能造成資料漏消費和重複消費。如果先提交offset後消費,有可能導致資料漏消費,如果先消費後提交offset就有可能導致資料重複消費。

package com.mmc.springbootstudy.kafka;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.util.Arrays;
import java.util.Map;
import java.util.Properties;

/**
 * @description:
 * @author: mmc
 * @create: 2021-04-22 21:32
 **/

public class ConsumerDemo {

    public final static String TOPIC = "mmc";

    /**
     * 自動提交offset
     * @throws InterruptedException
     */
    void receive() throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("group.id", "group_id");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("session.timeout.ms", "30000");
        props.put("max.poll.records", 1000);
        props.put("auto.offset.reset", "earliest");
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(TOPIC));

        while (true){
            ConsumerRecords<String, String> msgList=consumer.poll(1000);
            for (ConsumerRecord<String,String> record:msgList){
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
        }
    }


    /**
     * 手動同步提交
     * @throws InterruptedException
     */
    void commitSyncReceive() throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("group.id", "group_id");
        props.put("enable.auto.commit", "false");
        props.put("auto.commit.interval.ms", "1000");
        props.put("session.timeout.ms", "30000");
        props.put("max.poll.records", 1000);
        props.put("auto.offset.reset", "earliest");
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(TOPIC));

        while (true){
            ConsumerRecords<String, String> msgList=consumer.poll(1000);
            for (ConsumerRecord<String,String> record:msgList){
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            //同步提交,當前執行緒會阻塞直到 offset 提交成功
            consumer.commitSync();
        }

    }

    /**
     * 手動非同步提交
     * @throws InterruptedException
     */
    void commitAsyncReceive() throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "49.234.77.60:9092");
        props.put("group.id", "group_id");
        props.put("enable.auto.commit", "false");
        props.put("auto.commit.interval.ms", "1000");
        props.put("session.timeout.ms", "30000");
        props.put("max.poll.records", 1000);
        props.put("auto.offset.reset", "earliest");
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(TOPIC));

        while (true){
            ConsumerRecords<String, String> msgList=consumer.poll(1000);
            for (ConsumerRecord<String,String> record:msgList){
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
        
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                    if(e!=null){
                        System.err.println("commit failed for "+map);
                    }
                }
            });
        }

    }




    public static void main(String[] args) throws InterruptedException {
//        new ConsumerDemo().receive();
        new ConsumerDemo().commitSyncReceive();
    }
}

6.3 自定義分割槽

實現Partitioner介面,並在配置中加入

 props.put("partitioner.class", "com.mmc.springbootstudy.kafka.MyPartition");

自定義分割槽實現類:

package com.mmc.springbootstudy.kafka;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

/**
 * @description:
 * @author: mmc
 * @create: 2021-04-27 20:41
 **/

public class MyPartition implements Partitioner {
    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

自定義儲存offset

consumer.subscribe(Arrays.asList(TOPIC), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                
            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {

            }
        });

6.4 自定義攔截器

package com.mmc.springbootstudy.kafka;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

/**
 * @description:
 * @author: mmc
 * @create: 2021-04-30 20:47
 **/

public class CountIntercepter implements ProducerInterceptor<String,String> {

    private int successCount=0;

    private int failCount=0;

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        System.out.println("攔截到訊息的分割槽:"+producerRecord.topic());
        return producerRecord;
    }

    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        if(e==null){
            successCount++;
        }else {
            failCount++;
        }
    }

    @Override
    public void close() {
        System.out.println("success count:"+successCount);
    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

在生產者中需要加入

 props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,"com.mmc.springbootstudy.kafka.CountIntercepter");
      

七、第三方擴充套件

7.1 Kafka Eagle 監控

八、面試題

  1. Kafka 中的ISR(InSyncRepli)、OSR(OutSyncRepli)、AR(AllRepli)代表什麼?

答: kafka中與leader副本保持一定同步程度的副本(包括leader)組成ISR。與leader滯後太多的副本組成OSR。分割槽中所有的副本通稱為AR。

  1. Kafka 中的HW、LEO等分別代表什麼?

答:HW:高水位,指消費者只能拉取到這個offset之前的資料

LEO:標識當前日誌檔案中下一條待寫入的訊息的offset,大小等於當前日誌檔案最後一條訊息的offset+1.

  1. Kafka 中是怎麼體現訊息順序性的?

生產者:向leader副本負責訊息的順序寫入

消費者:同一個分割槽只能被同一個消費者組中的一個消費者消費。

kafka只保證同一個分割槽的順序性,所以如果是想保證全域性順序,可以自定義分割槽策略,將關聯的訊息發到同一個分割槽。如同一個訂單的各個狀態。
4. Kafka生產者客戶端的結構

答:整個生產者客戶端主要有兩個執行緒,主執行緒以及Sender執行緒。Producer在主執行緒中產生訊息,然後通過攔截器,序列化器,分割槽器之後快取到訊息累加器RecordAccumulator中。Sender執行緒從RecordAccumulator中獲取訊息併傳送到kafka中。RecordAccumulator主要用來快取訊息,這樣傳送的時候進行批量傳送以便減少相應的網路傳輸。RecordAccumulator快取的大小可以通過配置引數buffer.memory配置,預設是32M。如果建立訊息的速度過快,超過sender傳送給kafka伺服器的速度,會導致快取空間不足,這個時候sender執行緒可能會阻塞或者丟擲異常,max.block.ms配置決定阻塞的最大時間。

RecordAccumulator中為每個分割槽維護了一個雙端佇列,佇列中的內容是ProducerBatch,即Deque,建立訊息寫入到尾部,傳送訊息從頭部讀取。ProducerBatch是訊息傳送的一個批次,裡面包含了一個或多個ProducerRecord。

  1. 分割槽策略有哪些?

答:有兩種,一種是 RangeAssignor 分配策略(範圍分割槽),另一種是RoundRobinAssignor分配策略(輪詢分割槽)。預設採用 Range 範圍分割槽。

Range策略
如有10個分割槽,3個消費者,那麼通過10/3=3算出一個消費者消費3個分割槽。多出的分割槽由排在前面的消費者消費。那麼消費者1消費0,1,2,3分割槽。消費者2消費4,5,6分割槽。消費者3消費7,8,9分割槽。

缺點就是前面的消費者就會多消費到一個分割槽,如果是多個topic,那麼這個消費者就會多消費到多個分割槽。

RandRobin策略:同樣的例子,分割槽0被消費者1消費,分割槽1被消費者2消費,分割槽2被消費者3消費

注意:這種策略需要一個組內的消費者訂閱的主題相同。這樣輪詢的時候才是均勻的。

當出現以下幾種情況時,Kafka 會進行一次分割槽分配操作,即 Kafka 消費者端的 Rebalance 操作

  • 同一個 consumer 消費者組 group.id 中,新增了消費者進來,會執行 Rebalance 操作
  • 消費者離開當期所屬的 consumer group組。比如當機
  • 分割槽數量發生變化時(即 topic 的分割槽數量發生變化時)
  • 消費者主動取消訂閱
  1. kafka中哪些地方會選舉?

答:BrokerController
在broker啟動的時候,都會建立BrokerController,第一個在zookeeper中建立指定臨時節點成功的那個節點就是BrokerController。他負責管理叢集 broker的上下線,所有topic的分割槽副本分配和 leader 選舉等工作。

Partition Leader

  • 從Zookeeper中讀取當前分割槽的所有ISR(in-sync replicas)集合
  • 呼叫配置的分割槽選擇演算法選擇分割槽的leader
  1. 分割槽數能新增或減少嗎?

答:能新增,不能減少。因為減少的話,分割槽內已有的資料不好處理。

相關文章