消费位移的提交方式
Kafka中消费位移的管理在工作中经常会使用,比如常见的流处理结合kafka.
对于Kafka中的分区而言,它的每条消息都有唯一的offset,用来表示消息在分区中对应的位置
对于消费者而言,它也有一个offset的概念,消费者使用offset来表示消费到分区中某个消息所在的位置。
对于消息在分区中的位置,将offset称为偏移量.
对于消费者消费到的位置,将offset称为位移,消费位移.
消费位移是我们经常需要使用的,并且需要进行持久化的操作,否则会出现重复消费等问题。
比如消费者进行了重启,如果不知道上一次消费的位移就会出现问题。
在新的消费者客户端,消费位移存储在Kafka内部主题_consumer_offsets中。消费者在消费完消息后需要执行消费位移的提交。
1-默认提交
Kafka中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数enable.auto.commit配置,不配置默认是true:
Properties properties = new Properties();
properties.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
properties.put("bootstrap.servers", broker);
properties.put("group.id", groupId);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(Collections.singletonList(topic));
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
}
}
自动提交位移的操作是在poll()方法里面完成的,在每次真正向服务端发起拉取请求之前会检查是否可以进行位移提交,如果可以就会提交上一次轮询的位移。
2-手动提交分为同步和异步
手动提交需要先进行配置enable.auto.commit:
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
}
consumer.commitSync();
}
也可以按照分区的粒度进行处理消息提交位移
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords =
records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
}
long lastConsumedOffset = partitionRecords
.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition,
new OffsetAndMetadata(lastConsumedOffset + 1)));
}
}
上面使用的都是同步提交,异步提交的方法是commitAsync()
这里回顾上一次分享中flume里面消费者的代码:
tpAndOffsetMetadata.put(new TopicPartition(message.topic(), message.partition()),
new OffsetAndMetadata(message.offset() + 1, batchUUID));
if (!tpAndOffsetMetadata.isEmpty()) {
long commitStartTime = System.nanoTime();
consumer.commitSync(tpAndOffsetMetadata);
long commitEndTime = System.nanoTime();
...
tpAndOffsetMetadata.clear();
}
我们自己写代码的时候也可以多参考一些开源框架。
多线程消费探讨
KafkaProducer是线程安全的,但是KafkaConsumer是非线程安全的。
如果生产者发送消息的速度大于消费者处理的速度,那么就会有越来越多的消息得不到及时的消费,造成了一定的延迟,消息就会堆积,这时候其实需要一个完善的监控系统来监控消费者时候出现了消费滞后的情况,后面会继续学习监控方面的知识进行分享。
虽然KafkaConsumer是非线程安全的,但是依然可以使用多线程进行消费。
一般来说我们可以每个线程实例化一个KafkaConsumer对象,一个线程对应一个KafkaConsumer实例。一个消费线程可以消费一个或者多个分区,前提是所有的消费线程都属于同一个消费组。
这样每个线程可以按顺序消费各个分区中的消息。但是每个消费线程都维护一个独立的TCP连接,如果分区数和consumerThreadNum的值都很大,那么会造成不小的系统开销。
一般来说poll拉取消息的速度是相当快的,而整体消费的瓶颈也正是在处理消息这一块。
我们可以将处理消息的模块改为多线程的实现方式。但是消息的顺序处理比较困难
比如可以定义一个线程池:
executorService = new ThreadPoolExecutor(10, 30,
0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
每次拉取到一批消息就提交到线程池中进行处理
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
executorService.submit(new ConsumerRecordsHandler(records));
}
}
对于消息顺序的处理,大家可以进行分享
扩展
Spark Streaming 经常会结合kafka使用,为了保证Spark Streaming应用挂掉之后仍然能够正确地读取数据,我们肯定需要进行offset的管理。
具体是实现方式,后续会通过学习进行分享。
后续
再均衡
Spark Streaming+kafka
打开App,阅读手记
热门评论
有源码吗?。。。。。。。。。1。。