你说你 Kafka 用了很多年了,但是位移提交的这些细节你未必清楚。
不久前有个同事出去面试了,他说在 Kafka 问题上被面试官藐视了。
同事:我做了一个消息总线,简化了业务团队消息的收发,并且提供了消息管理功能。
面试官:你做的这个东西,有什么深入价值吗?
同事:目的在于封装技术的复杂度,使业务团队更加专注于业务。
面试官:这个价值并没有真实打动我,这不是很简单吗?
简单吗?那我问你:Kafka 是如何进行位移提交的?(你不一定能答得全面)
回到问题本身,Kafka 位移提交的方式有两种:有自动提交和手动提交。
一、自动提交
在默认情况下,Consumer 每 5 秒自动提交一次位移。现在,我们假设提交位移之后的 3 秒发生了 Rebalance 操作。在 Rebalance 之后,所有 Consumer 从上一次提交的位移处继续消费,但该位移已经是 3 秒前的位移数据了,故在 Rebalance 发生前 3 秒消费的所有数据都要重新再消费一次。
虽然你能够通过减少 auto.commit.interval.ms 的值来提高提交频率,但这么做只能缩小重复消费的时间窗口,不可能完全消除它。这是自动提交机制的一个缺陷。
弥补这个问题,就要用到手动提交了。
二、手动同步提交
下面这段代码展示了 commitSync() 的使用方法。
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
try {
consumer.commitSync();
} catch (CommitFailedException e) {
handle(e); // 处理提交失败异常
}
}
手动提交位移的好处就在于更加灵活,你完全能够把控位移提交的时机和频率,可以在消费完成后立马提交位移。
但是,它也有一个缺陷,就是在调用 commitSync() 时,Consumer 程序会处于阻塞状态,直到远端的 Broker 返回提交结果,这个状态才会结束。在任何系统中,因为程序而非资源限制而导致的阻塞都可能是系统的瓶颈,会影响整个应用程序的 TPS。
三、手动异步提交
由于它是异步的,Kafka 提供了回调函数(callback),供你实现提交之后的逻辑,比如记录日志或处理异常等。下面这段代码展示了调用 commitAsync() 的方法。
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
handle(exception);
}
});
}
commitAsync 的问题在于,出现问题时它不会自动重试。因为它是异步操作,倘若提交失败后自动重试,那么它重试时提交的位移值可能早已经“过期”或不是最新值了。因此,异步提交的重试其实没有意义,所以 commitAsync 是不会重试的。
那同步提交、异步提交那我该怎么选呢?
小孩子才做选择题,成年人当然是全部都要啊!
四、手动同异步结合提交
下面这段代码,将两个 API 方法结合使用进行手动提交。
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
consumer.commitAysnc(); // 使用异步提交规避阻塞
}
} catch (Exception e) {
handle(e); // 处理异常
} finally {
try {
consumer.commitSync(); // 最后一次提交使用同步阻塞式提交
} finally {
consumer.close();
}
}
对于常规性、阶段性的手动提交,我们调用 commitAsync() 避免程序阻塞,而在 Consumer 要关闭前,我们调用 commitSync() 方法执行同步阻塞式的位移提交,以确保 Consumer 关闭前能够保存正确的位移数据。将两者结合后,我们既实现了异步无阻塞式的位移管理,也确保了 Consumer 位移的正确性。
所以,如果你需要自行编写代码开发一套 Kafka Consumer 应用,那么我推荐你使用上面的代码范例来实现手动的位移提交。
(看到这里,面试官陷入了深深的沉默中...)