Kafka從入門到放棄(三)—— 詳說消費者

Max_Lyu發表於2021-12-21

之前介紹了Kafka以及生產者,包括它的一些特性和引數,這回寫一下消費者。

之前沒看得可以點選連結閱讀。

Kafka從入門到放棄(一) —— 初識Kafka

Kafka從入門到放棄(二) —— 詳說生產者

消費者與消費者組

在Kafka中消費者是消費訊息的物件。假設目前有一個消費者正在消費訊息,但生產資料的速度突然上升,這時候消費者會有點力不從心,跟不上訊息生產的速度,這時候咋辦呢?

我們對消費者進行橫向擴充套件,加幾個消費者,達到負載均衡的作用。但是要做點限制吧,不然幾個消費者消費同一個分割槽的訊息,不僅沒辦法提高消費能力,還會造成重複消費。因此讓他們分別消費不同的分割槽。

在Kafka中的消費者組就是如此,一個消費者組內的消費者訂閱同一個Topic的資料,但消費不同分割槽的資料,提高了消費能力。

但是消費者組裡的消費者數量建議不要超過分割槽數量,不然就浪費資源。

LEO & HW

Kafka中的分割槽是可以有多個副本的,我們把每個副本中待寫入的那個offset稱為LEO(Log End Offset),把最少訊息的那個副本的LEO稱為HW(High Watermark)

對於消費者而言,消費者所能消費的區間就是小於HW那部分,即圖中 0-3 部分。這樣消費者不管是哪個副本,訂閱到的訊息都是一致的,即使換了leader也能接著消費。

提交偏移量

假如一個消費者退出,另一個消費者接替它的任務,這時候就需要知道上一個消費者消費到了哪條資料,因此消費者需要追蹤偏移量。

在Kafka中,有一個名為_consumer_offset的主題,消費者會往裡面傳送訊息,提交偏移量,這個時候消費者也是生產者。

當消費者掛了或者有新的消費者假如消費者組,就會觸發在均衡操作,即為消費者重新分配分割槽。

為了能夠繼續之前的操作,消費者需要獲取每個分割槽最後一次提交的偏移量。

如果提交的偏移量小於處理的最後一個訊息的偏移量,會造成重複消費。比如消費者提交了 6 的offset,此時又拉取了2條資料,還沒等提交,消費者就掛掉了,然後就發生了再均衡。新的消費者獲取到 6 的偏移量,接著處理,這就造成了重複消費。

如果提交的偏移量大於處理的最後一個訊息的偏移量,會造成資料丟失。比如消費者一次性拉取了 88 條資料,並且提交了偏移量,還沒處理完就當機了,新的消費者獲取 88 的偏移量,繼續消費,就造成了資料丟失。

因此,如何提交偏移量對客戶端影響很大,稍有不慎就會造成不好的影響。

在Kafka中,有幾種提交偏移量的方式。

自動提交

這種提交方式有兩個很重要的引數:

enable.auto.commit=true(是否開啟自動提交,true or false)

auto.commit.interval.ms=5000(提交偏移量的時間間隔,預設5000ms)

這種方式最容易造成資料丟失以及重複消費。

通過CommitSync()方法手動提交當前偏移量

在處理完所有訊息後提交,前提要把enable.auto.commit設定為false。

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for(ConsumerRecords<String, String> record: records){
        System.out.println("topic=%s, offset=%s,partition=%s",
                          record.topic(), record.offset(),record.partition());
    }
    try{
        consumer.commitSync();
    } catch(Exception e){
        log.error(e);
    }
}

消費者通過poll方法輪詢獲取訊息,poll裡的引數是一個超時時間,用於控制阻塞的時間,如果沒有資料則會阻塞這麼久,如果設定為0則會立即放回。

使用這種方法一定要在處理完所有記錄後呼叫CommitSync()方法,避免資料丟失。如果發生錯誤,會進行重試。

非同步提交

CommitSync() 提交偏移量的方式會造成阻塞,即需要等客戶端處理完所有訊息後才提交偏移量,限制了吞吐量。因此可以使用非同步提交的方式,通過呼叫commitAsync()方法實現。

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for(ConsumerRecords<String, String> record: records){
        System.out.println("topic=%s, offset=%s,partition=%s",
                          record.topic(), record.offset(),record.partition());
    }
    consumer.commitAsync();
}

提交偏移量後就可以去做其他事了。CommitSync()方式發生錯誤會重試,但CommitAsync()不會。

之所以不重試,是因為有可能在收到broker響應前有其它偏移量提交了。

試想一下,如果會重試的話,當提交 66 的偏移量時發生網路問題,與此同時提交了 88 的偏移量,這時候剛好網路又通了,然後 88 的偏移量就提交成功了,然後 66 就重試,成功後又變成 66 了,就有可能造成重複消費。

之所以說這個問題,是因為非同步提交支援在broker響應時回撥,常被用於記錄錯誤或生成度量指標。如果用他重試的話一定要注意提交的順序。

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for(ConsumerRecords<String, String> record: records){
        System.out.println("topic=%s, offset=%s,partition=%s",
                          record.topic(), record.offset(),record.partition());
    }
    consumer.commitAsync(new OffsetCommitCallback() {
    	public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, 			Exception e){
    		if(e != null){
    			log.error("Error");
    		}
    	}
    });
}

非同步與同步組合提交

如果發生在關閉消費者或者再均衡前的最後一次提交,就需要確保其成功。

因此在消費者關閉前一般會通過組合使用的方式確保其提交成功。

try{
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for(ConsumerRecords<String, String> record: records){
            System.out.println("topic=%s, offset=%s,partition=%s",
            record.topic(),record.offset(),record.partition());
        }
        consumer.commitAsync();
    }
}catch(Exception e){
    log.error(e);
}finally {
    try {
        consumer.commitSync();
    }
    finally{
        consumer.close();
    }
}

提交特定偏移量

commitSync() 和 commitAsync() 方法一般是在處理完一個批次後提交偏移量。如果需要更頻繁的提交偏移量,需要在處理的過程中間提交的話,消費者 API 允許在呼叫 commitSync()和 commitAsync () 方法時傳進去希望提交的分割槽和偏移量的 map

Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<TopicPartition, OffsetAndMetadata>();
int count = 0; 		
try {
    while(true){
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
        if (records.isEmpty()){
           continue;
        }
        for (ConsumerRecord<String, String> record : records){
			System.out.println("topic=%s, offset=%s,partition=%s",
            	record.topic(),record.offset(),record.partition());
			currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset(), "no metadata"));
			// 每處理完1000條訊息後就提交偏移量
            if (count%1000==0) {
				consumer.commitAsync(currentOffsets, null);
			}
			count++;
        }
    }
} finally {
    try{
        consumer.commitSync();
    } finally{
        consumer.close();
    }
}

消費者分割槽分配策略

分割槽會被分配給消費者組裡的消費者進行消費,在Kafka種可以通過配置引數partition.assignment.strategy選擇分割槽分配策略。

  • Range 範圍分割槽

    假設現在有10個分割槽,消費者組裡有3個消費者。

    分割槽數量 10 除以消費者數量 3 取整(10/3)得 3,設為 x;分割槽數量 10 模 消費者數量 3(10%3)得 1,設為 y

    則前 y 個消費者分得 x+1 個分割槽;其餘消費者分得 x 個分割槽。

  • RoundRobin 輪詢分割槽

    假設有10個分割槽,3個消費者,第一個分割槽給第一個消費者,第二個給第二個消費者,第三個分割槽給第三個消費者,第四個給第一個消費者... 以此類推

到這,消費者的點就講得差不多了,可能有些細節沒寫或者沒講明白。後面如果發現了,我另寫一篇補上。如果覺得寫得還行得的話,麻煩點個小贊,謝謝!

轉載請註明出處:工眾號“大資料的奇妙冒險

相關文章