上一篇提到了如何利用ISR完成“消息不丢失”,接下来看看如何整体来说,如何实现Kafka的交付语义。
Kafka 或者所有的消息队列中都存在的交付语义:最多一次
、至少一次
、精确一次
,如何去理解这些语义,并用在合适的业务场景是十分重要的,看Kafka 社区中经常有吐槽丢消息等,其实通常来说不是Kafka 丢消息,而是用户用的不是那么明白,没有选择实现合适的交付语义,没有按照Kafka 规范来使用交付策略,下面具体来看看这几种交付语义。
最多一次(at most once)&使用场景 (Kafka 提供的不是这种机制)
最多一次是指,消息存在丢失的可能性,但能保证最多只处理一次。在一些消息宁愿丢失也不愿多处理一次的场景就比较合适了,但是如果存在精准一次的实现的话,还是精准一次比较合适。但是相对消息是否丢失没有那么执着的话,最多一次就够了,毕竟实现精准一次的代价是比起最多一次要高的。
至少一次(at least once)&使用场景(Kafka 默认实现方式)
至少一次是指,消息肯定会被处理,但是存在被处理多次的可能。这个在实现精准一次处理之前的场景,并且在Kafka 0.11.0.0 版本之前经常使用的就是这种模式,但是同一条消息被消费多次(这里的消费指的是成功拉取到消息并且返回,而不是说业务上对这个消息已经成功使用),因为网络抖动等原因可能会导致没有收到对应的响应而重复发送导致消息的多次消费或上传。Kafka 最起码的保证就是至少一次,因为ISR机制,Kafka消息一旦提交成功(产生副本之后),这条消息近乎可以认为是不可能丢失的,所以至少一次被消费。
精准一次(exactly once)&使用场景(Kafka 支持机制)
精确一次指的是消息被处理且只会被处理一次。在0.11.0.0 之后支持事务和幂等性之后,使用较广的就变成精确一次了。
幂等性producer(Idempotent producer)
首先幂等性的概念:若一个操作执行n次(n>1)与执行一次的结果是相同的,那么这个操作就是幂等操作。在producer 端,当出现发送消息无响应或者响应超时之后,不管消息成功没,都会有一个重试策略,这就导致了消息的重复提交问题,那如何实现幂等性呢,Kafka提供了一个enable.idempotent参数,设置为true时,就开启幂等了。
幂等的实现方式是给所提交的消息都赋予一个序列号用于消息去重(TCP 的方式),但是和TCP 实现不同的是,这个序列号不会舍弃,始终随消息持久化保存,可以简单的理解为消息的一部分。这么做的目的是 防止leader副本挂掉之后,没法儿进行去重操作。并且强制要求用户显指定一个producer id(严格单调递增的),这样一来,一个(producer id,分区号)都有一个对应的序列号值,这就为去重操作提供了便利,当发送消息的序列化小于或者等于broker端保存的序列号时,broker就会拒绝这条消息。
当实现上述的Idempotent producer 就保证了消息可以重试n次直到提交成功,并且提交多次也仅会成功保存一次,进而从producer端保证了,消息只会被成功提交一次。
事务
事务在Kafka中是指,把一组消息放入一个原子性单元中一次性统一处理。为实现Kafka的事物 client端必须提供一个唯一的id来标示事务(TransactionalId),并且要求在所有的会话上是唯一的(简单就理解为全局唯一吧),这个事务id是由producer自行分配的(注意区分开,transactionalId和producer id 不是一个东西)
当存在Kafka事务之后,就能完成的保证跨程序会话之间的幂等发送语义了,也支持了跨会话间的事务恢复(当某个producer挂掉了,能够保证下个实例完成之前未完成的事务)
就上面幂等性和事务来说,好像是仅仅对于producer 来说的,consumer 因为本身特性的原因对于这些特性的支持要弱一些(我觉着更合适的描述是 consumer的使用场景并没有说对事务的强要求或者必要),因为producer 是一种提交并持久化的过程,而consumer是一种消费并删除的过程,当事务中的部分消息被删除(比如事务跨日志段导致老的日志段被删除)、seek定位事务的指定位置、仅消费部分消息。
Kafka 为实现事务提供了一个叫做事务控制消息的消息,这个和普通消息的差异就是消息属性字段是否为1。消费/生产 到每个分区的消息都带着对应的事务控制信息,来完成具体的事务控制。