一文詳解Kafka API

華為雲開發者社群發表於2022-02-11
摘要:Kafka的API有Producer API,Consumer API還有自定義Interceptor (自定義攔截器),以及處理的流使用的Streams API和構建聯結器的Kafka Connect API。

本文分享自華為雲社群《【Kafka筆記】Kafka API詳細解析 Java版本(Producer API,Consumer API,攔截器等)》,作者: Copy工程師。

簡介

Kafka的API有Producer API,Consumer API還有自定義Interceptor (自定義攔截器),以及處理的流使用的Streams API和構建聯結器的Kafka Connect API。

Producer API

Kafka的Producer傳送訊息採用的是非同步傳送的方式。在訊息傳送過程中,涉及兩個執行緒:main執行緒和Sender執行緒,以及一個執行緒共享變數RecordAccumulator。main執行緒將訊息傳送給RecordAccmulator,Sender執行緒不斷地從RecordAccumulator中拉取訊息傳送給Kafka broker。

這裡的ACk機制,不是生產者得到ACK返回資訊才開始傳送,ACK保證的是生產者不丟失資料。例如:

一文詳解Kafka API

而是隻要有訊息資料,就向broker傳送。

訊息傳送流程

生產者使用send方法,經過攔截器之後在經過序列化器,然後在走分割槽器。然後通過分批次把資料傳送到PecordAccumulator,main執行緒到此過程就結束了,然後在回去執行send。

Sender執行緒不斷的獲取RecordAccumulator的資料傳送到topic。

訊息傳送流程是非同步傳送的,並且順序是一定的攔截器-》序列化器-》分割槽器

非同步傳送API

需要用到的類:

KafkaProducer: 需要建立一個生產者物件,用來傳送資料
ProducerConfig:獲取所需要的一系列配置引數
ProducerRecord:每條資料都要封裝成一個ProducerRecord物件

例項:

public class KafkaProducerDemo {

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "XXXXXXXXX:9093");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 建立KafkaProducer客戶端
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 10 ; i++) {
            producer.send(new ProducerRecord<>("my-topic","ImKey-"+i,"ImValue-"+i));
        }
        // 關閉資源
        producer.close();

    }
}

配置引數說明:

send():方法是非同步的,新增訊息到緩衝區等待傳送,並立即返回。生產者將單個的訊息批量在一起傳送來提高效率。

ack:是判斷請求是否完整的條件(就會判斷是不是成功傳送了,也就是上次說的ACK機制),指定all將會阻塞訊息,效能低但是最可靠。

retries:如果請求失敗,生產者會自動重試,我們指定是1次,但是啟動重試就有可能出現重複資料。

batch.size:指定快取的大小,生產者快取每個分割槽未傳送的訊息。值越大的話將會產生更大的批量,並需要更大的記憶體(因為每個活躍的分割槽都有一個快取區)。

linger.ms:指示生產者傳送請求之前等待一段時間,設定等待時間是希望更多地訊息填補到未滿的批中。預設緩衝可以立即傳送,即便緩衝空間還沒有滿,但是如果想減少請求的數量可以設定linger.ms大於0。需要注意的是在高負載下,相近的時間一般也會組成批,即使等於0。

buffer.memory:控制生產者可用的快取總量,如果訊息傳送速度比其傳輸到伺服器的快,將會耗盡這個快取空間。當快取空間耗盡,其他傳送呼叫將被阻塞,阻塞時間的閾值通過max.block.ms設定,之後將會丟擲一個TimeoutException

key.serializer和value.serializer將使用者提供的key和value物件ProducerRecord轉換成位元組,你可以使用附帶的ByteArraySerializaer或StringSerializer處理簡單的string或byte型別。

執行日誌:

[Godway] INFO  2019-11-14 14:46 - org.apache.kafka.clients.producer.ProducerConfig[main] - ProducerConfig values: 
    acks = all
    batch.size = 16384
    bootstrap.servers = [XXXXXX:9093]
    buffer.memory = 33554432
    client.id = 
    compression.type = none
    connections.max.idle.ms = 540000
    enable.idempotence = false
    interceptor.classes = null
    key.serializer = class org.apache.kafka.common.serialization.StringSerializer
    linger.ms = 1
    max.block.ms = 60000
    max.in.flight.requests.per.connection = 5
    max.request.size = 1048576
    metadata.max.age.ms = 300000
    metric.reporters = []
    metrics.num.samples = 2
    metrics.recording.level = INFO
    metrics.sample.window.ms = 30000
    partitioner.class = class org.apache.kafka.clients.producer.internals.DefaultPartitioner
    receive.buffer.bytes = 32768
    reconnect.backoff.max.ms = 1000
    reconnect.backoff.ms = 50
    request.timeout.ms = 30000
    retries = 1
    retry.backoff.ms = 100
    sasl.jaas.config = null
    sasl.kerberos.kinit.cmd = /usr/bin/kinit
    sasl.kerberos.min.time.before.relogin = 60000
    sasl.kerberos.service.name = null
    sasl.kerberos.ticket.renew.jitter = 0.05
    sasl.kerberos.ticket.renew.window.factor = 0.8
    sasl.mechanism = GSSAPI
    security.protocol = PLAINTEXT
    send.buffer.bytes = 131072
    ssl.cipher.suites = null
    ssl.enabled.protocols = [TLSv1.2, TLSv1.1, TLSv1]
    ssl.endpoint.identification.algorithm = null
    ssl.key.password = null
    ssl.keymanager.algorithm = SunX509
    ssl.keystore.location = null
    ssl.keystore.password = null
    ssl.keystore.type = JKS
    ssl.protocol = TLS
    ssl.provider = null
    ssl.secure.random.implementation = null
    ssl.trustmanager.algorithm = PKIX
    ssl.truststore.location = null
    ssl.truststore.password = null
    ssl.truststore.type = JKS
    transaction.timeout.ms = 60000
    transactional.id = null
    value.serializer = class org.apache.kafka.common.serialization.StringSerializer

[Godway] INFO  2019-11-14 14:46 - org.apache.kafka.common.utils.AppInfoParser[main] - Kafka version : 0.11.0.3
[Godway] INFO  2019-11-14 14:46 - org.apache.kafka.common.utils.AppInfoParser[main] - Kafka commitId : 26ddb9e3197be39a
[Godway] WARN  2019-11-14 14:46 - org.apache.kafka.clients.NetworkClient[kafka-producer-network-thread | producer-1] - Error while fetching metadata with correlation id 1 : {my-topic=LEADER_NOT_AVAILABLE}
[Godway] INFO  2019-11-14 14:46 - org.apache.kafka.clients.producer.KafkaProducer[main] - Closing the Kafka producer with timeoutMillis = 9223372036854775807 ms.

Process finished with exit code 0

有一條警告{my-topic=LEADER_NOT_AVAILABLE} 提示該topic不存在,但是沒有關係kafka會自動給你建立一個topic,不過建立的topic是有一個分割槽和一個副本:

一文詳解Kafka API

檢視一下該topic的訊息:

一文詳解Kafka API

訊息已經在topic裡了

上面的例項是沒有回撥函式的,send方法是有回撥函式的:

public class KafkaProducerCallbackDemo {

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "XXXXX:9093");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 建立KafkaProducer客戶端
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 10; i < 20 ; i++) {
            producer.send(new ProducerRecord<String, String>("my-topic", "ImKey-" + i, "ImValue-" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null){
                        System.out.println("訊息傳送成功!"+recordMetadata.offset());
                    }else {
                        System.err.println("訊息傳送失敗!");
                    }
                }
            });
        }
        producer.close();
    }
}

回撥函式會在producer收到ack時呼叫,為非同步呼叫,該方法有兩個引數,分別是RecordMetadata和Exception,如果Exception為null,說明訊息傳送成功,如果Exception不為null說明訊息傳送失敗。

注意: 訊息傳送失敗會自動重試,不需要我們在回撥函式中手動重試,使用回撥也是無阻塞的。而且callback一般在生產者的IO執行緒中執行,所以是非常快的,否則將延遲其他的執行緒訊息傳送。如果需要執行阻塞或者計算的回撥(耗時比較長),建議在callbanck主體中使用自己的Executor來並行處理!

一文詳解Kafka API

同步傳送API

同步傳送的意思就是,一條訊息傳送之後,會阻塞當前的執行緒,直到返回ack(此ack和非同步的ack機制不是一個ack)。

此ack是Future阻塞main執行緒,當傳送完成就返回一個ack去通知main執行緒已經傳送完畢,繼續往下走了

public Future<RecordMetadata> send(ProducerRecord<K,V> record,Callback callback)

send是非同步的,並且一旦訊息被儲存在等待傳送的訊息快取中,此方法就立即返回。這樣並行傳送多條訊息而不阻塞去等待每一條訊息的響應。

傳送的結果是一個RecordMetadata,它指定了訊息傳送的分割槽,分配的offset和訊息的時間戳。如果topic使用的是CreateTime,則使用使用者提供的時間戳或傳送的時間(如果使用者沒有指定指定訊息的時間戳)如果topic使用的是LogAppendTime,則追加訊息時,時間戳是broker的本地時間。

由於send呼叫是非同步的,它將為分配訊息的此訊息的RecordMetadata返回一個Future。如果future呼叫get(),則將阻塞,直到相關請求完成並返回該訊息的metadata,或丟擲傳送異常。

Throws:

InterruptException - 如果執行緒在阻塞中斷。
SerializationException - 如果key或value不是給定有效配置的serializers。
TimeoutException - 如果獲取後設資料或訊息分配記憶體話費的時間超過max.block.ms。
KafkaException - Kafka有關的錯誤(不屬於公共API的異常)。

public class KafkaProducerDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "XXXXX:9093");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 建立KafkaProducer客戶端
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 20; i < 30 ; i++) {
            RecordMetadata metadata = producer.send(new ProducerRecord<>("my-topic", "ImKey-" + i, "ImValue-" + i)).get();
            System.out.println(metadata.offset());
        }
        producer.close();

    }
}

API生產者自定義分割槽策略

生產者在向topic傳送訊息的時候的分割槽規則:

public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value)
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, K key, V value)
public ProducerRecord(String topic, K key, V value)
public ProducerRecord(String topic, V value)

根據send方法的引數的構造方法就可以看出來,

  1. 指定分割槽就傳送到指定分割槽
  2. 沒有指定分割槽,有key值,就按照key值的Hash值分配分割槽
  3. 沒有指定分割槽,也沒有指定key值,輪詢分割槽分配(只分配一次,以後都按照第一次的分割槽順序)

自定義分割槽器

自定義分割槽器需要實現org.apache.kafka.clients.producer.Partitioner介面。並且實現三個方法

public class KafkaMyPartitions 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) {

    }
}

自定義分割槽例項:

KafkaMyPartitions:

public class KafkaMyPartitions implements Partitioner {

    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        // 這裡寫自己的分割槽策略
        // 我這裡指定為1
        return 1;
    }
    @Override
    public void close() {
    }

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

KafkaProducerCallbackDemo:

public class KafkaProducerCallbackDemo {

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "XXXXX:9093");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 指定自定義分割槽
        props.put("partitioner.class","com.firehome.newkafka.KafkaMyPartitions");

        // 建立KafkaProducer客戶端
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 20; i < 25 ; i++) {
            producer.send(new ProducerRecord<String, String>("th-topic", "ImKey-" + i, "ImValue-" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null){
                        System.out.printf("訊息傳送成功!topic=%s,partition=%s,offset=%d \n",recordMetadata.topic(),recordMetadata.partition(),recordMetadata.offset());
                    }else {
                        System.err.println("訊息傳送失敗!");
                    }
                }
            });
        }
        producer.close();
    }
}

返回日誌:

訊息傳送成功!topic=th-topic,partition=1,offset=27 
訊息傳送成功!topic=th-topic,partition=1,offset=28 
訊息傳送成功!topic=th-topic,partition=1,offset=29 
訊息傳送成功!topic=th-topic,partition=1,offset=30 
訊息傳送成功!topic=th-topic,partition=1,offset=31 

可以看到直接傳送到了分割槽1上了。

多執行緒傳送訊息

Producer API是執行緒安全的,直接就可以使用多執行緒傳送訊息,例項:

public class KafkaProducerThread implements Runnable {

    private KafkaProducer<String,String> kafkaProducer;

    public KafkaProducerThread(){

    }
    public KafkaProducerThread(KafkaProducer kafkaProducer){
        this.kafkaProducer = kafkaProducer;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i++) {
            String key = "ImKey-" + i+"-"+Thread.currentThread().getName();
            String value = "ImValue-" + i+"-"+Thread.currentThread().getName();
            kafkaProducer.send(new ProducerRecord<>("th-topic", key, value));
            System.out.printf("Thread-name = %s, key = %s, value = %s",Thread.currentThread().getName(),key,value);
        }
    }

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "XXXXXXXX:9093");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 建立KafkaProducer客戶端
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        KafkaProducerThread producerThread1 = new KafkaProducerThread(producer);
        //KafkaProducerThread producerThread2 = new KafkaProducerThread(producer);
        Thread one = new Thread(producerThread1, "one");
        Thread two = new Thread(producerThread1, "two");
        System.out.println("執行緒開始");
        one.start();
        two.start();
    }
}

這裡只是一個簡單的例項。

Consumer API

kafka客戶端通過TCP長連線從叢集中消費訊息,並透明地處理kafka叢集中出現故障伺服器,透明地調節適應叢集中變化的資料分割槽。也和伺服器互動,平衡均衡消費者。

偏移量和消費者的位置

kafka為分割槽中的每條訊息儲存一個偏移量(offset),這個偏移量是該分割槽中一條訊息的唯一標示符。也表示消費者在分割槽的位置。例如,一個位置是5的消費者(說明已經消費了0到4的訊息),下一個接收訊息的偏移量為5的訊息。實際上有兩個與消費者相關的“位置”概念:

消費者的位置給出了下一條記錄的偏移量。它比消費者在該分割槽中看到的最大偏移量要大一個。 它在每次消費者在呼叫poll(long)中接收訊息時自動增長。

“已提交”的位置是已安全儲存的最後偏移量,如果程式失敗或重新啟動時,消費者將恢復到這個偏移量。消費者可以選擇定期自動提交偏移量,也可以選擇通過呼叫commit API來手動的控制(如:commitSync 和 commitAsync)。

這個區別是消費者來控制一條訊息什麼時候才被認為是已被消費的,控制權在消費者。

消費者組和主題訂閱

Kafka的消費者組概念,通過程式池瓜分訊息並處理訊息。這些程式可以在同一臺機器執行,也可分佈到多臺機器上,以增加可擴充套件性和容錯性,相同group.id的消費者將視為同一個消費者組。

分組中的每個消費者都通過subscribe API動態的訂閱一個topic列表。kafka將已訂閱topic的訊息傳送到每個消費者組中。並通過平衡分割槽在消費者分組中所有成員之間來達到平均。因此每個分割槽恰好地分配1個消費者(一個消費者組中)。所有如果一個topic有4個分割槽,並且一個消費者分組有隻有2個消費者。那麼每個消費者將消費2個分割槽。

消費者組的成員是動態維護的:如果一個消費者故障。分配給它的分割槽將重新分配給同一個分組中其他的消費者。同樣的,如果一個新的消費者加入到分組,將從現有消費者中移一個給它。這被稱為重新平衡分組。當新分割槽新增到訂閱的topic時,或者當建立與訂閱的正規表示式匹配的新topic時,也將重新平衡。將通過定時重新整理自動發現新的分割槽,並將其分配給分組的成員。

從概念上講,你可以將消費者分組看作是由多個程式組成的單一邏輯訂閱者。作為一個多訂閱系統,Kafka支援對於給定topic任何數量的消費者組,而不重複。

這是在訊息系統中常見的功能的略微概括。所有程式都將是單個消費者分組的一部分(類似傳統訊息傳遞系統中的佇列的語義),因此訊息傳遞就像佇列一樣,在組中平衡。與傳統的訊息系統不同的是,雖然,你可以有多個這樣的組。但每個程式都有自己的消費者組(類似於傳統訊息系統中pub-sub的語義),因此每個程式都會訂閱到該主題的所有訊息。

此外,當分組重新分配自動發生時,可以通過ConsumerRebalanceListener通知消費者,這允許他們完成必要的應用程式級邏輯,例如狀態清除,手動偏移提交等

它也允許消費者通過使用assign(Collection)手動分配指定分割槽,如果使用手動指定分配分割槽,那麼動態分割槽分配和協調消費者組將失效。

發現消費者故障

訂閱一組topic,當呼叫poll(long)時,消費者將自動加入到消費者組中。只要持續呼叫poll,消費者將一直保持可用,並繼續從分配的分割槽中接收資料。此外,消費者向伺服器定時傳送心跳。如果消費者崩潰或無法再session.timeout.ms配置的時間內傳送心跳,則消費者就被視為死亡,並且其分割槽將被重新分配。

還有一種可能,消費者可能遇到活鎖的情況,它持續的傳送心跳,但是沒有處理。為了預防消費者在這總情況下一直擁有分割槽,我們使用max.poll.interval.ms活躍監測機制。在此基礎上,如果你呼叫的poll的頻率大於最大間隔,則客戶端將主動地離開組,以便其他消費者接管該分割槽。發生這種情況時,你會看到offset提交失敗( 呼叫commitSync()引發的CommitFailedException )。這是一種安全機制,保障只有活動成員能夠提交offset。所以要留在組中,你必須持續呼叫poll。

消費者提供兩種配置設定來控制poll迴圈:

  1. max.poll.interval.ms: 增大poll的間隔,可以為消費者提供更多的時間去處理返回的訊息(呼叫poll(long)返回的訊息,通常返回的訊息都是一批),缺點是此值越大將會延遲組重新平衡。
  2. max.poll.records:此設定限制每次呼叫poll返回的訊息數,這樣可以更容易的預測每次poll間隔要處理的最大值。通過調整此值,可以減少poll間隔,減少重新平衡分組的

對於訊息處理時間不可預測地情況,這些選項是不夠的。 處理這種情況的推薦方法是將訊息處理移到另一個執行緒中,讓消費者繼續呼叫poll。 但是必須注意確保已提交的offset不超過實際位置。另外,你必須禁用自動提交,並只有線上程完成處理後才為記錄手動提交偏移量。 還要注意, 你需要pause暫停分割槽,不會從poll接收到新訊息,讓執行緒處理完之前返回的訊息(如果你的處理能力比拉取訊息的慢,那建立新執行緒將導致機器記憶體溢位)。

例項:

自動提交偏移量

public static void main(String[] args) {

        Properties props = new Properties();
        props.put("bootstrap.servers","xxxxxxxxxx:9093");
        props.put("group.id","test-6");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit","true");//自動提交offset
        props.put("auto.commit.interval.ms","1000"); // 自動提交時間間隔
        props.put("max.poll.records","5"); // 拉取的資料條數
        props.put("session.timeout.ms","10000"); // 維持session的時間。超過這個時間沒有心跳 就會剔出消費者組
        props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest"); 
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        // 可以寫多個topic
        consumer.subscribe(Arrays.asList("my-topic"));
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(5000);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            System.out.println("處理了一批資料!");
        }
    }

配置說明:

bootstrap.servers: 叢集是通過配置bootstrap.servers指定一個或多個broker。不用指定全部的broker,它將自動發現叢集中的其餘的borker(最好指定多個,萬一有伺服器故障)

enable.auto.commit: 自動提交偏移量,如果設定了自動提交偏移量,下面這個設定就必須要用到了。

auto.commit.interval.ms:自動提交時間間隔,和自動提交偏移量配合使用

max.poll.records:控制從 broker拉取的訊息條數

poll(long time): 當消費者獲取不到訊息時,就會使用這個引數,為了減輕無效的迴圈請求訊息,消費者會每隔long time的時間請求一次訊息,單位是毫秒。

session.timeout.ms: broker通過心跳機器自動檢測消費者組中失敗的程式,消費者會自動ping叢集,告訴進群它還活著。只要消費者能夠做到這一點,它就被認為是活著的,並保留分配給它分割槽的權利,如果它停止心跳的時間超過session.timeout.ms,那麼就會認為是故障的,它的分割槽將被分配到別的程式。

auto.offset.reset:這個屬性很重要,一會詳細講解

這裡說明一下auto.commit.interval.ms以及何時提交消費者偏移量,經過測試:

  • 設定props.put("auto.commit.interval.ms","60000");

自動提交時間為一分鐘,也就是你在這一分鐘內拉取任何數量的訊息都不會被提交消費的當前偏移量,如果你此時關閉消費者(一分鐘內),下次消費還是從和第一次的消費資料一樣,即使你在一分鐘內消費完所有的訊息,只要你在一分鐘內關閉程式,導致提交不了offset,就可以一直重複消費資料。

  • 設定props.put("auto.commit.interval.ms","3000");

但是在消費過程中設定sleep。

public static void main(String[] args) {

        Properties props = new Properties();
        props.put("bootstrap.servers","xxxxxxxxxxxx:9093");
        props.put("group.id","test-6");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit","true");//自動提交offset
        props.put("auto.commit.interval.ms","100000"); // 自動提交時間間隔
        props.put("max.poll.records","5"); // 拉取的資料條數
        props.put("session.timeout.ms","10000"); // 維持session的時間。超過這個時間沒有心跳 就會剔出消費者組
        props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest"); //
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        // 可以寫多個topic
        consumer.subscribe(Arrays.asList("my-topic"));

        while (true){
            ConsumerRecords<String, String> records = consumer.poll(5000);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            try {
                Thread.sleep(5000L);
                System.out.println("等待了5秒了!!!!!!!!!!!!開始等待15秒了");
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("處理了一批資料!");
        }
    }

這裡如果你消費了第一批資料,在執行第二次poll的時候,關閉程式也不會提交偏移量,只有在執行第二次poll的時候才會把上一次的最後一個offset提交上去。

auto.offset.reset講解:

auto.offset.reset的值有三種:earliest,latest,none,代表者不同的意思

earliest:
    當各分割槽下有已經提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費,最常用的值
latest:
    當各分割槽下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分割槽下的資料
none:
    topic各分割槽都存在已提交的offset時,從offset後開始消費,只要有一個分割槽不存在已提交的offset,則丟擲異常

!!注意:當使用了latest,並且分割槽沒有已提交的offset時,消費新產生的該分割槽下的資料,其實是把offset的值直接設定到最後一個訊息的位置。例如,有個30條資料的demo的topic,各分割槽無提交offset,使用了latest,再看offset就會發現已經在30的位置了,所以才只能消費新產生的資料!!!!

手動提交偏移量

不需要定時提交偏移量,可以自己控制offset,當訊息已經被我們消費過後,再去手動提交他們的偏移量。這個很適合我們的一些處理邏輯。

手動提交offset的方法有兩種:分別是commitSync(同步提交) 和commitAsync(非同步提交)。兩者的相同點,都會將本次poll的一批資料最高的偏移量提交;不同點是commitSync會失敗重試,一直到提交成功(如果有不可恢復的原因導致,也會提交失敗),才去拉取新資料。而commitAsync則沒有重試機制(提交了就去拉取新資料,不管這次的提交有沒有成功),故有可能提交失敗。

例項:

 public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers","XXXXXC:9093");
        props.put("group.id","test-11");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit","false");//自動提交offset
        props.put("auto.commit.interval.ms","1000"); // 自動提交時間間隔
        props.put("max.poll.records","20"); // 拉取的資料條數
        props.put("session.timeout.ms","10000"); // 維持session的時間。超過這個時間沒有心跳 就會剔出消費者組
        props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("my-topic"));
        int i= 0;
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(5000);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
                i++;
            }
            if (i == 20){
                System.out.println("i_num:"+i);
                // 同步提交
                consumer.commitSync();
                // 非同步提交
                // consumer.commitAsync();
            }else {
                System.out.println("不足二十個,不提交"+i);
            }
            i=0;
        }
    }

這些都是全部提交偏移量,如果我們想更細緻的控制偏移量提交,可以自定義提交偏移量:

public static void main(String[] args) throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers","XXXXXXXXXX:9093");
        props.put("group.id","test-18");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit","false");//自動提交offset
        props.put("auto.commit.interval.ms","1000000"); // 自動提交時間間隔
        props.put("max.poll.records","5"); // 拉取的資料條數
        props.put("session.timeout.ms","10000"); // 維持session的時間。超過這個時間沒有心跳 就會剔出消費者組
        props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("my-topic"));
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(5000);
            for (TopicPartition partition : records.partitions()) {
                List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                for (ConsumerRecord<String, String> record : partitionRecords) {
                    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
                }
                long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                consumer.commitAsync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)), new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        for (Map.Entry<TopicPartition,OffsetAndMetadata> entry : map.entrySet()){
                            System.out.println("提交的分割槽:"+entry.getKey().partition()+",提交的偏移量:"+entry.getValue().offset());
                        }
                    }
                });
            }
        }
    }

訂閱指定的分割槽

通過消費者Kafka會通過分割槽分配分給消費者一個分割槽,但是我們也可以指定分割槽消費訊息,要使用指定分割槽,只需要呼叫assign(Collection)消費指定的分割槽即可:

public static void main(String[] args) throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers","XXXXXXXXX:9093");
        props.put("group.id","test-19");//消費者組,只要group.id相同,就屬於同一個消費者組
        props.put("enable.auto.commit","false");//自動提交offset
        props.put("auto.commit.interval.ms","1000000"); // 自動提交時間間隔
        props.put("max.poll.records","5"); // 拉取的資料條數
        props.put("session.timeout.ms","10000"); // 維持session的時間。超過這個時間沒有心跳 就會剔出消費者組
        props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "earliest");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);
        // 你可以指定多個不同topic的分割槽或者相同topic的分割槽 我這裡只指定一個分割槽
        TopicPartition topicPartition = new TopicPartition("my-topic", 0);
        // 呼叫指定分割槽用assign,消費topic使用subscribe
        consumer.assign(Arrays.asList(topicPartition));
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(5000);
            for (TopicPartition partition : records.partitions()) {
                List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                for (ConsumerRecord<String, String> record : partitionRecords) {
                    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
                }
                long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                consumer.commitAsync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)), new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        for (Map.Entry<TopicPartition,OffsetAndMetadata> entry : map.entrySet()){
                            System.out.println("提交的分割槽:"+entry.getKey().partition()+",提交的偏移量:"+entry.getValue().offset());
                        }
                    }
                });
            }
        }
    }

一旦手動分配分割槽,你可以在迴圈中呼叫poll。消費者分割槽任然需要提交offset,只是現在分割槽的設定只能通過呼叫assign 修改,因為手動分配不會進行分組協調,因此消費者故障或者消費者的數量變動都不會引起分割槽重新平衡。每一個消費者是獨立工作的(即使和其他的消費者共享GroupId)。為了避免offset提交衝突,通常你需要確認每一個consumer例項的groupId都是唯一的。

注意:

手動分配分割槽(assgin)和動態分割槽分配的訂閱topic模式(subcribe)不能混合使用。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章