老版本的kafka Offset是Consumer 的消费位移,记录了 Consumer 要消费的下一条消息的位移。
Consumer 需要向 Kafka 汇报自己的位移数据,这个过程被称为提交位移。 Consumer 能够同时消费多个分区的数据,所以位移的提交实际上是在分区粒度上进行的,即Consumer 需要为分配给它的每个分区提交各自的位移数据。
提交位移主要是为了表征 Consumer 的消费进度,这样当 Consumer 发生故障重启之后,就能够从 Kafka 中读取之前提交的位移值,然后从相应的位移处继续消费,从而避免整个消费过程重来一遍。
provider角度来说位移提交分为自动提交和手动提交; Consumer 端的角度来说,位移提交分为同步提交和异步提交。
Consumer 端 KafkaConsumer.commitSync()会提交 KafkaConsumer.poll() 返回的最新位移。它是一个同步操作,即该方法会一直等待,直到位移被成功提交才会返回。如果提交过程中出现异常,该方法会将异常信息抛出。
调用 consumer.commitSync() 方法的时机是在处理完了 poll() 方法返回的所有消息之后。如果过早提交了位移,就可能会出现消费数据丢失的情况。
一旦设置了 enable.auto.commit 为 true,Kafka 会保证在开始调用 poll 方法时,提交上次 poll 返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消费丢失的情况。但自动提交位移的一个问题在于,它可能会出现重复消费。
在默认情况下,Consumer 每 5 秒自动提交一次位移。假设提交位移之后的 3 秒发生了 Rebalance 操作。在 Rebalance 之后,所有 Consumer 从上一次提交的位移处继续消费,但该位移已经是 3 秒前的位移数据了,故在 Rebalance 发生前 3 秒消费的所有数据都要重新再消费一次。虽然能够通过减少 auto.commit.interval.ms 的值来提高提交频率,但这么做只能缩小重复消费的时间窗口,不可能完全消除它。这是自动提交机制的一个缺陷。
手动提交位移好处就在于更加灵活,完全能够把控位移提交的时机和频率。但是在调用 commitSync() 时,Consumer 程序会处于阻塞状态,直到远端的 Broker 返回提交结果,这个状态才会结束。如果因为网络问题broker端一直迟迟不返回信息,这将会极大影响整个应用的TPS。于是kafka提供了另一个异步的方法 KafkaConsumer.commitAsync()。用 commitAsync() 之后,它会立即返回,不会阻塞,因此不会影响 Consumer 应用的 TPS。commitAsync提供了回调函数(callback)提交之后的逻辑,比如记录日志或处理异常等。但是commitAsync 出现问题时它不会自动重试。它是异步操作,如果提交失败后自动重试,那么它重试时提交的位移值可能早已经“过期”或不是最新值了。
手动提交可以将 commitSync 和 commitAsync 组合使用。如下调用 commitAsync() 避免程序阻塞,而在 Consumer 要关闭前调用 commitSync() 方法执行同步阻塞式的位移提交,以确保 Consumer 关闭前能够保存正确的位移数据。将两者结合后,我们既实现了异步无阻塞式的位移管理,也确保了 Consumer 位移的正确性。
try {
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
commitAysnc(); // 使用异步提交规避阻塞
}
} catch (Exception e) {
handle(e); // 处理异常
} finally {
try {
consumer.commitSync(); // 最后一次提交使用同步阻塞式提交
} finally {
consumer.close();
}
}
当一次性要处理大批量的消息时,肯定是不希望等消息完全消费完后再提交位移,如果中途发生点网络抖动或者Bug导致前功尽弃。于是可以一次消费一部分就提交。
Kafka Consumer API 提交提供了这样的方法:commitSync(Map<TopicPartition, OffsetAndMetadata>) 和 commitAsync(Map<TopicPartition, OffsetAndMetadata>)。它们的参数是一个 Map 对象,键就是 TopicPartition,值是一个 OffsetAndMetadata 对象,保存的主要是位移数据。
private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
int count = 0;
……
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record: records) {
process(record); // 处理消息
offsets.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1);
if(count % 100 == 0)
consumer.commitAsync(offsets, null); // 回调处理逻辑是 null
count++;
}
}