前言
讀完本文,你將了解到如下知識點(diǎn):
- kafka 的消費(fèi)者 和 消費(fèi)者組
- 如何正確使用kafka consumer
- 常用的 kafka consumer 配置
消費(fèi)者 和 消費(fèi)者組
-
什么是消費(fèi)者?
顧名思義,消費(fèi)者就是從kafka集群消費(fèi)數(shù)據(jù)的客戶端,如下圖,展示了一個消費(fèi)者從一個topic中消費(fèi)數(shù)據(jù)的模型image 單個消費(fèi)者模型存在的問題?
如果這個時候 kafka 上游生產(chǎn)的數(shù)據(jù)很快,超過了這個消費(fèi)者1
的消費(fèi)速度,那么就會導(dǎo)致數(shù)據(jù)堆積,產(chǎn)生一些大家都知道的蛋疼事情了,那么我們只能加強(qiáng)消費(fèi)者
的消費(fèi)能力,所以也就有了我們下面來說的消費(fèi)者組
-
什么是消費(fèi)者組?
所謂消費(fèi)者組
,其實(shí)就是一組消費(fèi)者
的集合,當(dāng)我們看到下面這張圖是不是就特別舒服了,我們采用了一個消費(fèi)組
來消費(fèi)這個topic
,眾人拾柴火焰高,其消費(fèi)能力那是按倍數(shù)遞增的,所以這里我們一般來說都是采用消費(fèi)者組
來消費(fèi)數(shù)據(jù),而不會是單消費(fèi)者
來消費(fèi)數(shù)據(jù)的。這里值得我們注意的是:- 一個
topic
可以被 多個消費(fèi)者組
消費(fèi),但是每個消費(fèi)者組
消費(fèi)的數(shù)據(jù)是 互不干擾的,也就是說,每個消費(fèi)組
消費(fèi)的都是 完整的數(shù)據(jù) 。 - 一個分區(qū)只能被 同一個消費(fèi)組內(nèi) 的一個
消費(fèi)者
消費(fèi),而 不能拆給多個消費(fèi)者 消費(fèi)
- 一個
-
是不是一個 消費(fèi)組 的 消費(fèi)者 越多其消費(fèi)能力就越強(qiáng)呢?
從圖3
我們就很好的可以回答這個問題了,我們可以看到消費(fèi)者4
是完全沒有消費(fèi)任何的數(shù)據(jù)的,所以如果你想要加強(qiáng)消費(fèi)者組
的能力,除了添加消費(fèi)者,分區(qū)的數(shù)量也是需要跟著增加的,只有這樣他們的并行度才能上的去,消費(fèi)能力才會強(qiáng)。
為了提高 消費(fèi)組 的 消費(fèi)能力,我是不是可以隨便添加 分區(qū) 和 消費(fèi)者 呢?
答案當(dāng)然是否定的啦。。。嘿嘿
我們看到圖2
,一般來說我們建議消費(fèi)者
數(shù)量 和分區(qū)
數(shù)量是一致的,當(dāng)我們的消費(fèi)能力不夠時,就必須通過調(diào)整分區(qū)的數(shù)量來提高并行度,但是,我們應(yīng)該盡量來避免這種情況發(fā)生,比如:
現(xiàn)在我們需要在圖2
的基礎(chǔ)上增加一個分區(qū)4
,那么這個分區(qū)4
該由誰來消費(fèi)呢?這個時候kafka會進(jìn)行分區(qū)再均衡
,來為這個分區(qū)分配消費(fèi)者,分區(qū)再均衡
期間該消費(fèi)組
是不可用的,并且作為一個被消費(fèi)者
,分區(qū)數(shù)的改動將影響到每一個消費(fèi)者組
,所以在創(chuàng)建topic
的時候,我們就應(yīng)該考慮好分區(qū)數(shù),來盡量避免這種情況發(fā)生-
分區(qū)分配過程
上面我們提到了為 分區(qū)分配消費(fèi)者,那么我們現(xiàn)在就來看看分配過程是怎么樣的。- 確定 群組協(xié)調(diào)器
每當(dāng)我們創(chuàng)建一個消費(fèi)組,kafka 會為我們分配一個 broker 作為該消費(fèi)組的 coordinator(協(xié)調(diào)器) - 注冊消費(fèi)者 并選出 leader consumer
當(dāng)我們的有了 coordinator 之后,消費(fèi)者將會開始往該 coordinator上進(jìn)行注冊,第一個注冊的 消費(fèi)者將成為該消費(fèi)組的 leader,后續(xù)的 作為 follower, - 當(dāng) leader 選出來后,他會從coordinator那里實(shí)時獲取分區(qū) 和 consumer 信息,并根據(jù)分區(qū)策略給每個consumer 分配 分區(qū),并將分配結(jié)果告訴 coordinator。
- follower 消費(fèi)者將從 coordinator 那里獲取到自己相關(guān)的分區(qū)信息進(jìn)行消費(fèi),對于所有的 follower 消費(fèi)者而言,他們只知道自己消費(fèi)的分區(qū),并不知道其他消費(fèi)者的存在。
- 至此,消費(fèi)者都知道自己的消費(fèi)的分區(qū),分區(qū)過程結(jié)束,當(dāng)發(fā)送分區(qū)再均衡的時候,leader 將會重復(fù)分配過程
- 確定 群組協(xié)調(diào)器
消費(fèi)組與分區(qū)重平衡
可以看到,當(dāng)新的消費(fèi)者加入消費(fèi)組,它會消費(fèi)一個或多個分區(qū),而這些分區(qū)之前是由其他消費(fèi)者負(fù)責(zé)的;另外,當(dāng)消費(fèi)者離開消費(fèi)組(比如重啟、宕機(jī)等)時,它所消費(fèi)的分區(qū)會分配給其他分區(qū)。這種現(xiàn)象稱為重平衡(rebalance)。重平衡是Kafka一個很重要的性質(zhì),這個性質(zhì)保證了高可用和水平擴(kuò)展。不過也需要注意到,在重平衡期間,所有消費(fèi)者都不能消費(fèi)消息,因此會造成整個消費(fèi)組短暫的不可用。而且,將分區(qū)進(jìn)行重平衡也會導(dǎo)致原來的消費(fèi)者狀態(tài)過期,從而導(dǎo)致消費(fèi)者需要重新更新狀態(tài),這段期間也會降低消費(fèi)性能。后面我們會討論如何安全的進(jìn)行重平衡以及如何盡可能避免。
消費(fèi)者通過定期發(fā)送心跳(hearbeat)到一個作為組協(xié)調(diào)者(group coordinator)的broker來保持在消費(fèi)組內(nèi)存活。這個broker不是固定的,每個消費(fèi)組都可能不同。當(dāng)消費(fèi)者拉取消息或者提交時,便會發(fā)送心跳。
如果消費(fèi)者超過一定時間沒有發(fā)送心跳,那么它的會話(session)就會過期,組協(xié)調(diào)者會認(rèn)為該消費(fèi)者已經(jīng)宕機(jī),然后觸發(fā)重平衡。可以看到,從消費(fèi)者宕機(jī)到會話過期是有一定時間的,這段時間內(nèi)該消費(fèi)者的分區(qū)都不能進(jìn)行消息消費(fèi);通常情況下,我們可以進(jìn)行優(yōu)雅關(guān)閉,這樣消費(fèi)者會發(fā)送離開的消息到組協(xié)調(diào)者,這樣組協(xié)調(diào)者可以立即進(jìn)行重平衡而不需要等待會話過期。
在0.10.1版本,Kafka對心跳機(jī)制進(jìn)行了修改,將發(fā)送心跳與拉取消息進(jìn)行分離,這樣使得發(fā)送心跳的頻率不受拉取的頻率影響。另外更高版本的Kafka支持配置一個消費(fèi)者多長時間不拉取消息但仍然保持存活,這個配置可以避免活鎖(livelock)。活鎖,是指應(yīng)用沒有故障但是由于某些原因不能進(jìn)一步消費(fèi)。
創(chuàng)建Kafka消費(fèi)者
讀取Kafka消息只需要創(chuàng)建一個kafkaConsumer,創(chuàng)建過程與KafkaProducer非常相像。我們需要使用四個基本屬性,bootstrap.servers、key.deserializer、value.deserializer和group.id。其中,bootstrap.servers與創(chuàng)建KafkaProducer的含義一樣;key.deserializer和value.deserializer是用來做反序列化的,也就是將字節(jié)數(shù)組轉(zhuǎn)換成對象;group.id不是嚴(yán)格必須的,但通常都會指定,這個參數(shù)是消費(fèi)者的消費(fèi)組。
下面是一個代碼樣例:
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
props.put("group.id", "CountryCounter");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(props);
訂閱主題
創(chuàng)建完消費(fèi)者后我們便可以訂閱主題了,只需要通過調(diào)用subscribe()方法即可,這個方法接收一個主題列表,非常簡單:
consumer.subscribe(Collections.singletonList("customerCountries"));
這個例子中只訂閱了一個customerCountries主題。另外,我們也可以使用正則表達(dá)式來匹配多個主題,而且訂閱之后如果又有匹配的新主題,那么這個消費(fèi)組會立即對其進(jìn)行消費(fèi)。正則表達(dá)式在連接Kafka與其他系統(tǒng)時非常有用。比如訂閱所有的測試主題:
consumer.subscribe("test.*");
拉取循環(huán)
消費(fèi)數(shù)據(jù)的API和處理方式很簡單,我們只需要循環(huán)不斷拉取消息即可。Kafka對外暴露了一個非常簡潔的poll方法,其內(nèi)部實(shí)現(xiàn)了協(xié)作、分區(qū)重平衡、心跳、數(shù)據(jù)拉取等功能,但使用時這些細(xì)節(jié)都被隱藏了,我們也不需要關(guān)注這些。下面是一個代碼樣例:
try {
while (true) { //1)
ConsumerRecords<String, String> records = consumer.poll(100); //2)
for (ConsumerRecord<String, String> record : records) //3)
{
log.debug("topic = %s, partition = %s, offset = %d,
customer = %s, country = %s\n",
record.topic(), record.partition(), record.offset(),
record.key(), record.value());
int updatedCount = 1;
if (custCountryMap.countainsValue(record.value())) {
updatedCount = custCountryMap.get(record.value()) + 1;
}
custCountryMap.put(record.value(), updatedCount)
JSONObject json = new JSONObject(custCountryMap);
System.out.println(json.toString(4))
}
}
} finally {
consumer.close(); //4
}
其中,代碼中標(biāo)注了幾點(diǎn),說明如下:
1)這個例子使用無限循環(huán)消費(fèi)并處理數(shù)據(jù),這也是使用Kafka最多的一個場景,后面我們會討論如何更好的退出循環(huán)并關(guān)閉。
2)這是上面代碼中最核心的一行代碼。我們不斷調(diào)用poll拉取數(shù)據(jù),如果停止拉取,那么Kafka會認(rèn)為此消費(fèi)者已經(jīng)死亡并進(jìn)行重平衡。參數(shù)值是一個超時時間,指明線程如果沒有數(shù)據(jù)時等待多長時間,0表示不等待立即返回。
3)poll()方法返回記錄的列表,每條記錄包含key/value以及主題、分區(qū)、位移信息。
4)主動關(guān)閉可以使得Kafka立即進(jìn)行重平衡而不需要等待會話過期。
另外需要提醒的是,消費(fèi)者對象不是線程安全的,也就是不能夠多個線程同時使用一個消費(fèi)者對象;而且也不能夠一個線程有多個消費(fèi)者對象。簡而言之,一個線程一個消費(fèi)者,如果需要多個消費(fèi)者那么請使用多線程來進(jìn)行一一對應(yīng)。
單線程版
package com.neuedu;
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
public class Consumer {
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers",
"hadoop03:9092,hadoop05:9092,hadoop06:9092");// 該地址是集群的子集,用來探測集群。
props.put("group.id", "payment");// cousumer的分組id
props.put("enable.auto.commit", "true");// 自動提交offsets
props.put("auto.commit.interval.ms", "1000");// 每隔1s,自動提交offsets
props.put("session.timeout.ms", "30000");// Consumer向集群發(fā)送自己的心跳,超時則認(rèn)為Consumer已經(jīng)死了,kafka會把它的分區(qū)分配給其他進(jìn)程
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");// 反序列化器
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("payment"));// 訂閱的topic,可以多個
// String topic = "payment";
// TopicPartition partition0 = new TopicPartition(topic, 0);
// TopicPartition partition1 = new TopicPartition(topic, 1);
// consumer.assign(Arrays.asList(partition0));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("賭東道賭東道賭東道賭東道賭東道賭東道 offset = %d, key = %s, value = %s, partition = %s",
record.offset(), record.key(), record.value(),record.partition());
System.out.println();
}
}
}
}
多線程版
package com.neuedu;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class ConsumerRunnable implements Runnable {
// 每個線程維護(hù)私有的KafkaConsumer實(shí)例
private final KafkaConsumer<String, String> consumer;
public ConsumerRunnable(String brokerList, String groupId, String topic) {
Properties props = new Properties();
props.put("bootstrap.servers", brokerList);
props.put("group.id", groupId);
props.put("enable.auto.commit", "true"); //本例使用自動提交位移
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
//分區(qū)分配策略
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RangeAssignor");
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RoundRobinAssignor");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
this.consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic)); // 本例使用分區(qū)副本自動分配策略
}
@Override
public void run() {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(200); // 本例使用200ms作為獲取超時時間
for (ConsumerRecord<String, String> record : records) {
// 這里面寫處理消息的邏輯,本例中只是簡單地打印消息
System.out.println(Thread.currentThread().getName() + " consumed " + record.partition() +
"th message with offset: " + record.offset());
}
}
}
}
package com.neuedu;
import java.util.ArrayList;
import java.util.List;
public class ConsumerGroup {
private List<ConsumerRunnable> consumers;
public ConsumerGroup(int consumerNum, String groupId, String topic, String brokerList) {
consumers = new ArrayList<>(consumerNum);
for (int i = 0; i < consumerNum; ++i) {
ConsumerRunnable consumerThread = new ConsumerRunnable(brokerList, groupId, topic);
consumers.add(consumerThread);
}
}
public void execute() {
for (ConsumerRunnable task : consumers) {
new Thread(task).start();
}
}
}
package com.neuedu;
public class ConsumerMain {
public static void main(String[] args) {
String brokerList = "hadoop03:9092";
String groupId = "testGroup1";
String topic = "payment";
int consumerNum = 2;
ConsumerGroup consumerGroup = new ConsumerGroup(consumerNum, groupId, topic, brokerList);
consumerGroup.execute();
}
}
使用起來還是很簡單的,不過如果想要用好 consumer,可能你還需要了解以下這些東西:
- 分區(qū)控制策略
- consumer 的一些常用配置
- offset 的控制
ok,那么我們接下來一個個來看吧。。。
分區(qū)控制策略
- 手動控制分區(qū)
咱們先來說下最簡單的手動分區(qū)控制,代碼如下:
String topic = "foo";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0, partition1));
看起來只是把普通的訂閱方式修改成了訂閱知道 topic
的分區(qū),其余的還是照常使用,不過這里也需要注意一下的是:
- 一般只作為獨(dú)立消費(fèi)者,也就是不能加入消費(fèi)組,或者說他本身就是作為一個消費(fèi)組存在,要保證這一點(diǎn),我們只需要保證其
group id
是唯一的就可以了。 - 對于
topic
的分區(qū)變動不敏感,也就是說當(dāng)topic
新增了分區(qū),分區(qū)的數(shù)據(jù)將會發(fā)生改變,但該消費(fèi)組對此確是不感知的,依然照常運(yùn)行,所以很多時候需要你手動consumer.partitionsFor()
去查看topic
的分區(qū)情況 - 不要和
subscription
混合使用
- 使用
partition.assignment.strategy
進(jìn)行分區(qū)策略配置
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RangeAssignor");
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RoundRobinAssignor");
這里的話 kafka 是自帶兩種分區(qū)策略的,為了方便理解,我們以如下場景為例來進(jìn)行解釋:
已知:
TopicA 有 3 個 partition(分區(qū)):A-1,A-2,A-3;
TopicB 有 3 個 partition(分區(qū)):B-1,B-2,B-3;
ConsumerA 和 ConsumerB 作為一個消費(fèi)組 ConsumerGroup 同時消費(fèi) TopicA 和 TopicB
-
Range
該方式最大的特點(diǎn)就是會將連續(xù)的分區(qū)分配給一個消費(fèi)者,根據(jù)示例,我們可以得出如下結(jié)論:ConsumerGroup 消費(fèi) TopicA 的時候:
ConsumerA 會分配到 A-1,A-2
ConsumerB 會分配到 A-3ConsumerGroup 消費(fèi) TopicB 的時候:
ConsumerA 會分配到 B-1,B-2
ConsumerB 會分配到 B-3所以:
ConsumerA 分配到了4個分區(qū): A-1,A-2,B-1,B-2
ConsumerB 分配到了2個分區(qū):A-3,B-3 -
RoundRobin
該方式最大的特點(diǎn)就是會以輪詢的方式將分區(qū)分配給一個個消費(fèi)者,根據(jù)示例,我們可以得出如下結(jié)論:ConsumerGroup 消費(fèi) TopicA 的時候:
ConsumerA 分配到 A-1
ConsumerB 分配到 A-2
ConsumerA 分配到 A-3ConsumerGroup 消費(fèi) TopicB 的時候,因?yàn)樯洗畏峙涞搅?ConsumerA,那么這次輪到 ConsumerB了 所以:
ConsumerB 分配到 B-1
ConsumerA 分配到 B-2
ConsumerB 分配到 B-3所以:
ConsumerA 分配到了4個分區(qū): A-1,A-3,B-2
ConsumerB 分配到了2個分區(qū):A-2,B-1,B-3
從上面我們也是可以看出這兩種策略的異同,RoundRobin 相比較 Range 會使得分區(qū)分配的更加的均衡,但如果是消費(fèi)單個 topic ,那么其均衡是差不多的,而 Range 會比 RoundRobin 更具優(yōu)勢一點(diǎn),至于這個優(yōu)勢,還得看你的具體業(yè)務(wù)了。
-
自定義的分區(qū)策略
上面兩種分區(qū)策略是 kafka 默認(rèn)自帶的策略,雖然大多數(shù)情況下夠用了,但是可能針對一些特殊需求,我們也可以定義自己的分區(qū)策略- Range分區(qū)策略源碼
如何自定義呢?最好的方式莫過于看源碼是怎么實(shí)現(xiàn)的,然后自己依葫蘆畫瓢來一個,所以我們先來看看 Range分區(qū)策略源碼,如下,我只做了簡單的注釋,因?yàn)樗旧硪埠芎唵危攸c(diǎn)看下assign
的參數(shù)以及返回注釋就 ok了
public class RangeAssignor extends AbstractPartitionAssignor{ //省略部分代碼。。。。 /** * 根據(jù)訂閱者 和 分區(qū)數(shù)量來進(jìn)行分區(qū) * @param partitionsPerTopic: topic->分區(qū)數(shù)量 * @param subscriptions: memberId 消費(fèi)者id -> subscription 消費(fèi)者信息 * @return: memberId ->list<topic名稱 和 分區(qū)序號(id)> */ @Override public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic, Map<String, Subscription> subscriptions) { //topic -> list<消費(fèi)者> Map<String, List<String>> consumersPerTopic = consumersPerTopic(subscriptions); //初始化 返回結(jié)果 Map<String, List<TopicPartition>> assignment = new HashMap<>(); for (String memberId : subscriptions.keySet()) assignment.put(memberId, new ArrayList<TopicPartition>()); for (Map.Entry<String, List<String>> topicEntry : consumersPerTopic.entrySet()) { //topic String topic = topicEntry.getKey(); // 消費(fèi)該topic的 consumer-id List<String> consumersForTopic = topicEntry.getValue(); //topic 的分區(qū)數(shù)量 Integer numPartitionsForTopic = partitionsPerTopic.get(topic); if (numPartitionsForTopic == null) continue; Collections.sort(consumersForTopic); //平均每個消費(fèi)者分配的 分區(qū)數(shù)量 int numPartitionsPerConsumer = numPartitionsForTopic / consumersForTopic.size(); //平均之后剩下的 分區(qū)數(shù) int consumersWithExtraPartition = numPartitionsForTopic % consumersForTopic.size(); //這里就是將連續(xù)分區(qū)切開然后分配給每個消費(fèi)者 List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic); for (int i = 0, n = consumersForTopic.size(); i < n; i++) { int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition); int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1); assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length)); } } return assignment; } }
- 自定義一個 分區(qū)策略
這里先緩緩把,太簡單把,沒什么用,太復(fù)雜把,一時也想不出好的場景,如果你有需求,歡迎留言,我們一起來實(shí)現(xiàn)
- Range分區(qū)策略源碼
Consumer 常用配置
首先,我們都應(yīng)該知道,最全最全的文檔應(yīng)該是來自官網(wǎng)(雖然有時候可能官網(wǎng)找不到):
http://kafka.apachecn.org/documentation.html#newconsumerconfigs
嗯,以下內(nèi)容來自 kafka權(quán)威指南 ,請?jiān)徫业男卸琛!!:罄m(xù)有時間會把工作中的遇到的補(bǔ)充上
fetch.min.bytes
該屬性指定了消費(fèi)者從服務(wù)器獲取記錄的最小字節(jié)數(shù)。broker 在收到消費(fèi)者的數(shù)據(jù)請求時,
如果可用的數(shù)據(jù)量小于fetch.min.bytes 指定的大小,那么它會等到有足夠的可用數(shù)據(jù)時才把它返回給消費(fèi)者。這樣可以降低消費(fèi)者和 broker 的工作負(fù)載,因?yàn)樗鼈冊谥黝}不是很活躍的時候(或者一天里的低谷時段)就不需要來來回回地處理消息。如果沒有很多可用數(shù)據(jù),但消費(fèi)者的 CPU 使用率卻很高,那么就需要把該屬性的值設(shè)得比默認(rèn)值大。如果消費(fèi)者的數(shù)量比較多,把該屬性的值設(shè)置得大一點(diǎn)可以降低broker 的工作負(fù)載。fetch.max.wait.ms
我們通過 fetch.min.bytes 告訴 Kafka,等到有足夠的數(shù)據(jù)時才把它返回給消費(fèi)者。而feth.max.wait.ms 則用于指定 broker 的等待時間,默認(rèn)是 500ms。如果沒有足夠的數(shù)據(jù)流入 Kafka,消費(fèi)者獲取最小數(shù)據(jù)量的要求就得不到滿足,最終導(dǎo)致 500ms 的延遲。如果要降低潛在的延遲(為了滿足 SLA),可以把該參數(shù)值設(shè)置得小一些。
如果 fetch.max.wait.ms 被設(shè)為 100ms,并且fetch.min.bytes 被設(shè)為 1MB,那么 Kafka 在收到消費(fèi)者的請求后,要么返回 1MB 數(shù)據(jù),要么在100ms 后返回所有可用的數(shù)據(jù),就看哪個條件先得到滿足。max.partition.fetch.bytes
該屬性指定了服務(wù)器從每個分區(qū)里返回給消費(fèi)者的最大字節(jié)數(shù)。它的默認(rèn)值是 1MB,也就是說,KafkaConsumer.poll() 方法從每個分區(qū)里返回的記錄最多不超過 max.partition.fetch.bytes指定的字節(jié)。如果一個主題有 20 個分區(qū)和 5 個消費(fèi)者,那么每個消費(fèi)者需要至少 4MB 的可用內(nèi)存來接收記錄。在為消費(fèi)者分配內(nèi)存時,可以給它們多分配一些,因?yàn)槿绻航M里有消費(fèi)者發(fā)生崩潰,剩下的消費(fèi)者需要處理更多的分區(qū)。
max.partition.fetch.bytes 的值必須比 broker 能夠接收的最大消息的字節(jié)數(shù)(通過 max.message.size 屬性配置)大,否則消費(fèi)者可能無法讀取這些消息,導(dǎo)致消費(fèi)者一直掛起重試。在設(shè)置該屬性時,另一個需要考慮的因素是消費(fèi)者處理數(shù)據(jù)的時間。消費(fèi)者需要頻繁調(diào)用poll() 方法來避免會話過期和發(fā)生分區(qū)再均衡,如果單次調(diào)用 poll() 返回的數(shù)據(jù)太多,消費(fèi)者需要更多的時間來處理,可能無法及時進(jìn)行下一個輪詢來避免會話過期。
如果出現(xiàn)這種情況,可以把max.partition.fetch.bytes 值改小,或者延長會話過期時間。session.timeout.ms
該屬性指定了消費(fèi)者在被認(rèn)為死亡之前可以與服務(wù)器斷開連接的時間,默認(rèn)是 3s。如果消費(fèi)者沒有在session.timeout.ms 指定的時間內(nèi)發(fā)送心跳給群組協(xié)調(diào)器,就被認(rèn)為已經(jīng)死亡,協(xié)調(diào)器就會觸發(fā)再均衡,把它的分區(qū)分配給群組里的其他消費(fèi)者。
該屬性與 heartbeat.interval.ms 緊密相關(guān)。heartbeat.interval.ms 指定了 poll() 方法向協(xié)調(diào)器發(fā)送心跳的頻率,session.timeout.ms則指定了消費(fèi)者可以多久不發(fā)送心跳。
所以,一般需要同時修改這兩個屬性,heartbeat.interval.ms 必須比 session.timeout.ms 小,一般是session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那么 heartbeat.interval.ms 應(yīng)該是 1s。把session.timeout.ms 值設(shè)得比默認(rèn)值小,可以更快地檢測和恢復(fù)崩潰的節(jié)點(diǎn),不過長時間的輪詢或垃圾收集可能導(dǎo)致非預(yù)期的再均衡。把該屬性的值設(shè)置得大一些,可以減少意外的再均衡,不過檢測節(jié)點(diǎn)崩潰需要更長的時間。auto.offset.reset
該屬性指定了消費(fèi)者在讀取一個沒有偏移量的分區(qū)或者偏移量無效的情況下(因消費(fèi)者長時間失效,包含偏移量的記錄已經(jīng)過時并被刪除)該作何處理。
它的默認(rèn)值是 latest,意思是說,在偏移量無效的情況下,消費(fèi)者將從最新的記錄開始讀取數(shù)據(jù)(在消費(fèi)者啟動之后生成的記錄)。另一個值是earliest,意思是說,在偏移量無效的情況下,消費(fèi)者將從起始位置讀取分區(qū)的記錄。enable.auto.commit
我們稍后將介紹幾種不同的提交偏移量的方式。該屬性指定了消費(fèi)者是否自動提交偏移量,默認(rèn)值是true。為了盡量避免出現(xiàn)重復(fù)數(shù)據(jù)和數(shù)據(jù)丟失,可以把它設(shè)為 false,由自己控制何時提交偏移量。如果把它設(shè)為true,還可以通過配置 auto.commit.interval.ms 屬性來控制提交的頻率。-
partition.assignment.strategy
(這部分好像重復(fù)了 ~~~)
我們知道,分區(qū)會被分配給群組里的消費(fèi)者。PartitionAssignor 根據(jù)給定的消費(fèi)者和主題,決定哪些分區(qū)應(yīng)該被分配給哪個消費(fèi)者。Kafka 有兩個默認(rèn)的分配策略。
Range
該策略會把主題的若干個連續(xù)的分區(qū)分配給消費(fèi)者。假設(shè)消費(fèi)者 C1 和消費(fèi)者 C2 同時訂閱了主題T1 和 主題 T2,并且每個主題有 3 個分區(qū)。那么消費(fèi)者 C1 有可能分配到這兩個主題的分區(qū) 0 和分區(qū) 1,而消費(fèi)者 C2 分配到這兩個主題的分區(qū) 2。因?yàn)槊總€主題擁有奇數(shù)個分區(qū),而分配是在主題內(nèi)獨(dú)立完成的,第一個消費(fèi)者最后分配到比第二個消費(fèi)者更多的分區(qū)。只要使用了 Range 策略,而且分區(qū)數(shù)量無法被消費(fèi)者數(shù)量整除,就會出現(xiàn)這種情況。RoundRobin
該策略把主題的所有分區(qū)逐個分配給消費(fèi)者。如果使用 RoundRobin 策略來給消費(fèi)者 C1 和消費(fèi)者C2 分配分區(qū),那么消費(fèi)者 C1 將分到主題 T1 的分區(qū) 0 和分區(qū) 2 以及主題 T2 的分區(qū) 1,消費(fèi)者 C2 將分配到主題 T1 的分區(qū) 1 以及主題 T2 的分區(qū) 0 和分區(qū) 2。一般來說,如果所有消費(fèi)者都訂閱相同的主題(這種情況很常見),RoundRobin 策略會給所有消費(fèi)者分配相同數(shù)量的分區(qū)(或最多就差一個分區(qū))。可以通過設(shè)置 partition.assignment.strategy 來選擇分區(qū)策略。
默認(rèn)使用的是org.apache.kafka.clients.consumer.RangeAssignor,這個類實(shí)現(xiàn)了 Range 策略,不過也可以把它改成 org.apache.kafka.clients.consumer.RoundRobinAssignor。我們還可以使用自定義策略,在這種情況下,partition.assignment.strategy 屬性的值就是自定義類的名字。 client.id
該屬性可以是任意字符串,broker 用它來標(biāo)識從客戶端發(fā)送過來的消息,通常被用在日志、度量指標(biāo)和配額里。max.poll.records
該屬性用于控制單次調(diào)用 call() 方法能夠返回的記錄數(shù)量,可以幫你控制在輪詢里需要處理的數(shù)據(jù)量。receive.buffer.bytes 和 send.buffer.bytes
socket 在讀寫數(shù)據(jù)時用到的 TCP 緩沖區(qū)也可以設(shè)置大小。如果它們被設(shè)為 -1,就使用操作系統(tǒng)的默認(rèn)值。
如果生產(chǎn)者或消費(fèi)者與 broker 處于不同的數(shù)據(jù)中心內(nèi),可以適當(dāng)增大這些值,因?yàn)榭鐢?shù)據(jù)中心的網(wǎng)絡(luò)一般都有比較高的延遲和比較低的帶寬
提交(commit)與位移(offset)
當(dāng)我們調(diào)用poll()時,該方法會返回我們沒有消費(fèi)的消息。當(dāng)消息從broker返回消費(fèi)者時,broker并不跟蹤這些消息是否被消費(fèi)者接收到;Kafka讓消費(fèi)者自身來管理消費(fèi)的位移,并向消費(fèi)者提供更新位移的接口,這種更新位移方式稱為提交(commit)。
在正常情況下,消費(fèi)者會發(fā)送分區(qū)的提交信息到Kafka,Kafka進(jìn)行記錄。當(dāng)消費(fèi)者宕機(jī)或者新消費(fèi)者加入時,Kafka會進(jìn)行重平衡,這會導(dǎo)致消費(fèi)者負(fù)責(zé)之前并不屬于它的分區(qū)。重平衡完成后,消費(fèi)者會重新獲取分區(qū)的位移,下面來看下兩種有意思的情況。
假如一個消費(fèi)者在重平衡前后都負(fù)責(zé)某個分區(qū),如果提交位移比之前實(shí)際處理的消息位移要小,那么會導(dǎo)致消息重復(fù)消費(fèi),如下所示:
假如在重平衡前某個消費(fèi)者拉取分區(qū)消息,在進(jìn)行消息處理前提交了位移,但還沒完成處理宕機(jī)了,然后Kafka進(jìn)行重平衡,新的消費(fèi)者負(fù)責(zé)此分區(qū)并讀取提交位移,此時會“丟失”消息,如下所示:
因此,提交位移的方式會對應(yīng)用有比較大的影響,下面來看下不同的提交方式。
自動提交
這種方式讓消費(fèi)者來管理位移,應(yīng)用本身不需要顯式操作。當(dāng)我們將enable.auto.commit設(shè)置為true,那么消費(fèi)者會在poll方法調(diào)用后每隔5秒(由auto.commit.interval.ms指定)提交一次位移。和很多其他操作一樣,自動提交也是由poll()方法來驅(qū)動的;在調(diào)用poll()時,消費(fèi)者判斷是否到達(dá)提交時間,如果是則提交上一次poll返回的最大位移。
需要注意到,這種方式可能會導(dǎo)致消息重復(fù)消費(fèi)。假如,某個消費(fèi)者poll消息后,應(yīng)用正在處理消息,在3秒后Kafka進(jìn)行了重平衡,那么由于沒有更新位移導(dǎo)致重平衡后這部分消息重復(fù)消費(fèi)。
提交當(dāng)前位移
為了減少消息重復(fù)消費(fèi)或者避免消息丟失,很多應(yīng)用選擇自己主動提交位移。設(shè)置auto.commit.offset為false,那么應(yīng)用需要自己通過調(diào)用commitSync()來主動提交位移,該方法會提交poll返回的最后位移。
為了避免消息丟失,我們應(yīng)當(dāng)在完成業(yè)務(wù)邏輯后才提交位移。而如果在處理消息時發(fā)生了重平衡,那么只有當(dāng)前poll的消息會重復(fù)消費(fèi)。下面是一個自動提交的代碼樣例:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
try {
consumer.commitSync();
} catch (CommitFailedException e) {
log.error("commit failed", e)
}
}
上面代碼poll消息,并進(jìn)行簡單的打印(在實(shí)際中有更多的處理),最后完成處理后進(jìn)行了位移提交。
異步提交
手動提交有一個缺點(diǎn),那就是當(dāng)發(fā)起提交調(diào)用時應(yīng)用會阻塞。當(dāng)然我們可以減少手動提交的頻率,但這個會增加消息重復(fù)的概率(和自動提交一樣)。另外一個解決辦法是,使用異步提交的API。以下為使用異步提交的方式,應(yīng)用發(fā)了一個提交請求然后立即返回:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
System.out.printf("topic = %s, partition = %s,
offset = %d, customer = %s, country = %s\n",
record.topic(), record.partition(), record.offset(),
record.key(), record.value());
}
consumer.commitAsync();
}
但是異步提交也有個缺點(diǎn),那就是如果服務(wù)器返回提交失敗,異步提交不會進(jìn)行重試。相比較起來,同步提交會進(jìn)行重試直到成功或者最后拋出異常給應(yīng)用。異步提交沒有實(shí)現(xiàn)重試是因?yàn)椋绻瑫r存在多個異步提交,進(jìn)行重試可能會導(dǎo)致位移覆蓋。舉個例子,假如我們發(fā)起了一個異步提交commitA,此時的提交位移為2000,隨后又發(fā)起了一個異步提交commitB且位移為3000;commitA提交失敗但commitB提交成功,此時commitA進(jìn)行重試并成功的話,會將實(shí)際上將已經(jīng)提交的位移從3000回滾到2000,導(dǎo)致消息重復(fù)消費(fèi)。
因此,基于這種性質(zhì),一般情況下對于異步提交,我們可能會通過回調(diào)的方式記錄提交結(jié)果:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s,
offset = %d, customer = %s, country = %s\n",
record.topic(), record.partition(), record.offset(),
record.key(), record.value());
}
consumer.commitAsync(new OffsetCommitCallback() {
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (e != null)
log.error("Commit failed for offsets {}", offsets, e);
}
});
}
而如果想進(jìn)行重試同時又保證提交順序的話,一種簡單的辦法是使用單調(diào)遞增的序號。每次發(fā)起異步提交時增加此序號,并且將此時的序號作為參數(shù)傳給回調(diào)方法;當(dāng)消息提交失敗回調(diào)時,檢查參數(shù)中的序號值與全局的序號值,如果相等那么可以進(jìn)行重試提交,否則放棄(因?yàn)橐呀?jīng)有更新的位移提交了)。
混合同步提交與異步提交
正常情況下,偶然的提交失敗并不是什么大問題,因?yàn)楹罄m(xù)的提交成功就可以了。但是在某些情況下(例如程序退出、重平衡),我們希望最后的提交成功,因此一種非常普遍的方式是混合異步提交和同步提交,如下所示:
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d,
customer = %s, country = %s\n",
record.topic(), record.partition(),
record.offset(), record.key(), record.value());
}
consumer.commitAsync();
}
} catch (Exception e) {
log.error("Unexpected error", e);
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}
在正常處理流程中,我們使用異步提交來提高性能,但最后使用同步提交來保證位移提交成功。
提交特定位移
commitSync()和commitAsync()會提交上一次poll()的最大位移,但如果poll()返回了批量消息,而且消息數(shù)量非常多,我們可能會希望在處理這些批量消息過程中提交位移,以免重平衡導(dǎo)致從頭開始消費(fèi)和處理。幸運(yùn)的是,commitSync()和commitAsync()允許我們指定特定的位移參數(shù),參數(shù)為一個分區(qū)與位移的map。由于一個消費(fèi)者可能會消費(fèi)多個分區(qū),所以這種方式會增加一定的代碼復(fù)雜度,如下所示:
private Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
int count = 0;
....
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset()+1, "no metadata"));
if (count % 1000 == 0)
consumer.commitAsync(currentOffsets, null);
count++;
} }
代碼中在處理poll()消息的過程中,不斷保存分區(qū)與位移的關(guān)系,每處理1000條消息就會異步提交(也可以使用同步提交)。
重平衡監(jiān)聽器(Rebalance Listener)
變更分區(qū)命令
kafka-topics.sh --zookeeper hadoop03:2181 --alter --topic test --partitions 6
在分區(qū)重平衡前,如果消費(fèi)者知道它即將不再負(fù)責(zé)某個分區(qū),那么它可能需要將已經(jīng)處理過的消息位移進(jìn)行提交。Kafka的API允許我們在消費(fèi)者新增分區(qū)或者失去分區(qū)時進(jìn)行處理,我們只需要在調(diào)用subscribe()方法時傳入ConsumerRebalanceListener對象,該對象有兩個方法:
- public void onPartitionRevoked(Collection<topicpartition> partitions):此方法會在消費(fèi)者停止消費(fèi)消費(fèi)后,在重平衡開始前調(diào)用。</topicpartition>
- public void onPartitionAssigned(Collection<topicpartition> partitions):此方法在分區(qū)分配給消費(fèi)者后,在消費(fèi)者開始讀取消息前調(diào)用。</topicpartition>
下面來看一個onPartitionRevoked9)的例子,該例子在消費(fèi)者失去某個分區(qū)時提交位移(以便其他消費(fèi)者可以接著消費(fèi)消息并處理):
private Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
private class HandleRebalance implements ConsumerRebalanceListener {
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
}
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("Lost partitions in rebalance.
Committing current
offsets:" + currentOffsets);
consumer.commitSync(currentOffsets);
}
}
try {
consumer.subscribe(topics, new HandleRebalance());
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset()+1, "no metadata"));
}
consumer.commitAsync(currentOffsets, null);
}
} catch (WakeupException e) {
// ignore, we're closing
} catch (Exception e) {
log.error("Unexpected error", e);
} finally {
try {
consumer.commitSync(currentOffsets);
} finally {
consumer.close();
System.out.println("Closed consumer and we are done");
}
}
代碼中實(shí)現(xiàn)了onPartitionsRevoked()方法,當(dāng)消費(fèi)者失去某個分區(qū)時,會提交已經(jīng)處理的消息位移(而不是poll()的最大位移)。上面代碼會提交所有的分區(qū)位移,而不僅僅是失去分區(qū)的位移,但這種做法沒什么壞處。
從指定位移開始消費(fèi)
在此之前,我們使用poll()來從最后的提交位移開始消費(fèi),但我們也可以從一個指定的位移開始消費(fèi)。
如果想從分區(qū)開始端重新開始消費(fèi),那么可以使用seekToBeginning(TopicPartition tp);如果想從分區(qū)的最末端消費(fèi)最新的消息,那么可以使用seekToEnd(TopicPartition tp)。而且,Kafka還支持我們從指定位移開始消費(fèi)。從指定位移開始消費(fèi)的應(yīng)用場景有很多,其中最典型的一個是:位移存在其他系統(tǒng)(例如數(shù)據(jù)庫)中,并且以其他系統(tǒng)的位移為準(zhǔn)。
考慮這么個場景:我們從Kafka中讀取消費(fèi),然后進(jìn)行處理,最后把結(jié)果寫入數(shù)據(jù)庫;我們既不想丟失消息,也不想數(shù)據(jù)庫中存在重復(fù)的消息數(shù)據(jù)。對于這樣的場景,我們可能會按如下邏輯處理:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
currentOffsets.put(new TopicPartition(record.topic(), record.partition()), record.offset());
processRecord(record);
storeRecordInDB(record);
consumer.commitAsync(currentOffsets);
}
}
這個邏輯似乎沒什么問題,但是要注意到這么個事實(shí),在持久化到數(shù)據(jù)庫成功后,提交位移到Kafka可能會失敗,那么這可能會導(dǎo)致消息會重復(fù)處理。對于這種情況,我們可以優(yōu)化方案,將持久化到數(shù)據(jù)庫與提交位移實(shí)現(xiàn)為原子性操作,也就是要么同時成功,要么同時失敗。但這個是不可能的,因此我們可以在保存記錄到數(shù)據(jù)庫的同時,也保存位移,然后在消費(fèi)者開始消費(fèi)時使用數(shù)據(jù)庫的位移開始消費(fèi)。這個方案是可行的,我們只需要通過seek()來指定分區(qū)位移開始消費(fèi)即可。下面是一個改進(jìn)的樣例代碼:
public class SaveOffsetsOnRebalance implements ConsumerRebalanceListener {
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
//在消費(fèi)者負(fù)責(zé)的分區(qū)被回收前提交數(shù)據(jù)庫事務(wù),保存消費(fèi)的記錄和位移
commitDBTransaction();
}
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
//在開始消費(fèi)前,從數(shù)據(jù)庫中獲取分區(qū)的位移,并使用seek()來指定開始消費(fèi)的位移
for(TopicPartition partition: partitions)
consumer.seek(partition, getOffsetFromDB(partition));
}
}
consumer.subscribe(topics, new SaveOffsetOnRebalance(consumer));
//在subscribe()之后poll一次,并從數(shù)據(jù)庫中獲取分區(qū)的位移,使用seek()來指定開始消費(fèi)的位移
consumer.poll(0);
for (TopicPartition partition: consumer.assignment())
consumer.seek(partition, getOffsetFromDB(partition));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
{
processRecord(record);
//保存記錄結(jié)果
storeRecordInDB(record);
//保存位移
storeOffsetInDB(record.topic(), record.partition(), record.offset());
}
//提交數(shù)據(jù)庫事務(wù),保存消費(fèi)的記錄以及位移
commitDBTransaction();
}
具體邏輯見代碼注釋,此處不再贅述。另外注意的是,seek()只是指定了poll()拉取的開始位移,這并不影響在Kafka中保存的提交位移(當(dāng)然我們可以在seek和poll之后提交位移覆蓋)。
package com.neuedu;
import java.util.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
public class Consumer {
private static Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
static int count = 0;
static KafkaConsumer<String, String> consumer;
private class HandleRebalance implements ConsumerRebalanceListener {
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
}
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("Lost partitions in rebalance.Committing current offsets:" + currentOffsets);
consumer.commitSync(currentOffsets);
}
}
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers",
"hadoop03:9092,hadoop05:9092,hadoop06:9092");// 該地址是集群的子集,用來探測集群。
props.put("group.id", "payment");// cousumer的分組id
props.put("enable.auto.commit", "false");// 自動提交offsets
props.put("auto.commit.interval.ms", "1000");// 每隔1s,自動提交offsets
props.put("session.timeout.ms", "30000");// Consumer向集群發(fā)送自己的心跳,超時則認(rèn)為Consumer已經(jīng)死了,kafka會把它的分區(qū)分配給其他進(jìn)程
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");// 反序列化器
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
consumer = new KafkaConsumer<>(props);
// consumer.subscribe(Arrays.asList("payment"));// 訂閱的topic,可以多個
String topic = "payment";
TopicPartition partition0 = new TopicPartition(topic, 0);
// TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0));
Collection<TopicPartition> partitions = Arrays.asList(partition0);
consumer.seekToBeginning(partitions);
// consumer.seek(partition0,495);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("賭東道賭東道賭東道賭東道賭東道賭東道 offset = %d, key = %s, value = %s, partition = %s",
record.offset(), record.key(), record.value(),record.partition());
System.out.println();
currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset()+1, "no metadata"));
if (count % 1 == 0)
consumer.commitAsync(currentOffsets, null);
count++;
}
}
}
}