之前介紹了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個消費者,第一個分割槽給第一個消費者,第二個給第二個消費者,第三個分割槽給第三個消費者,第四個給第一個消費者... 以此類推
到這,消費者的點就講得差不多了,可能有些細節沒寫或者沒講明白。後面如果發現了,我另寫一篇補上。如果覺得寫得還行得的話,麻煩點個小贊,謝謝!
轉載請註明出處:工眾號“大資料的奇妙冒險”