HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同

幂等

实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:

唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识
记录下已处理过的请求标识:光有唯一标识还不够,还需要记录下那些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复交易,拒绝掉

Kafka幂等性实现原理

为了实现 Producer 的幂等语义,Kafka 引入了Producer ID(即PID)和Sequence Number。每个新的 Producer 在初始化的时候会被分配一个唯一的 PID,该 PID 对用户完全透明而不会暴露给用户。

对于每个 PID,该 Producer 发送数据的每个<Topic, Partition>都对应一个从 0 开始单调递增的Sequence Number。

类似地,Broker 端也会为每个<PID, Topic, Partition>维护一个序号,并且每次 Commit 一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比 Broker 维护的序号(即最后一次 Commit 的消息的序号)大一,则 Broker 会接受它,否则将其丢弃:

  • 如果消息序号比 Broker 维护的序号大一以上,说明中间有数据尚未写入,也即乱序,此时 Broker 拒绝该消息,Producer 抛出InvalidSequenceNumber
  • 如果消息序号小于等于 Broker 维护的序号,说明该消息已被保存,即为重复消息,Broker 直接丢弃该消息,Producer 抛出DuplicateSequenceNumber
实现

为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。

  • PID:每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。
    Sequence Numbler:(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number

Kafka可能存在多个生产者,会同时产生消息,但对Kafka来说,只需要保证每个生产者内部的消息幂等就可以了,所有引入了PID来标识不同的生产者。

对于Kafka来说,要解决的是生产者发送消息的幂等问题。也即需要区分每条消息是否重复。
Kafka通过为每条消息增加一个Sequence Numbler,通过Sequence Numbler来区分每条消息。每条消息对应一个分区,不同的分区产生的消息不可能重复。所有Sequence Numbler对应每个分区

Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号比Broker缓存中序号大于1则接受它,否则将其丢弃。这样就可以实现了消息重复提交了。但是,只能保证单个Producer对于同一个<Topic, Partition>的Exactly Once语义。不能保证同一个Producer一个topic不同的partion幂等。

流程

KafkaProducer启动时,会初始化一个
TransactionManager 实例,它的作用有以下几个部分:

记录本地的事务状态(事务性时必须)
记录一些状态信息以保证幂等性,比如:每个 topic-partition 对应的下一个 sequence numbers 和 last acked batch(最近一个已经确认的 batch)的最大的 sequence number 等;
记录 ProducerIdAndEpoch 信息(PID 信息)。

幂等性时,Producer 的发送流程如下:

1)调用kafkaProducer的send方法将数据添加到 RecordAccumulator 中,添加时会判断是否需要新建一个 ProducerBatch,这时这个 ProducerBatch 还是没有 PID 和 sequence number 信息的;

2)Producer 后台发送线程 Sender,在 run() 方法中,会先根据 TransactionManager 的 shouldResetProducerStateAfterResolvingSequences() 方法判断当前的 PID 是否需要重置,重置的原因是因为:如果有topic-partition的batch已经超时还没处理完,此时可能会造成sequence number 不连续。因为sequence number 有部分已经分配出去了,而Kafka服务端没有收到这部分sequence number 的序号,Kafka服务端为了保证幂等性,只会接受同一个pid的sequence number 等于服务端缓存sequence number +1的消息,所有这时候需要重置Pid来保证幂等性

3)Sender线程调用maybeWaitForProducerId()方法判断是否要申请Pid,如果需要,会阻塞直到成功申请到Pid

5)最后调用sendProduceRequest方法将消息发送出去

kafka幂等客户端和服务端配置 kafka幂等性配置_幂等

事务性保证

上述幂等设计只能保证单个 Producer 对于同一个<Topic, Partition>的Exactly Once语义。

另外,它并不能保证写操作的原子性——即多个写操作,要么全部被 Commit 要么全部不被 Commit。

更不能保证多个读写操作的的原子性。尤其对于 Kafka Stream 应用而言,典型的操作即是从某个 Topic 消费数据,经过一系列转换后写回另一个 Topic,保证从源 Topic 的读取与向目标 Topic 的写入的原子性有助于从故障中恢复。

事务保证可使得应用程序将生产数据和消费数据当作一个原子单元来处理,要么全部成功,要么全部失败,即使该生产或消费跨多个<Topic, Partition>。

另外,有状态的应用也可以保证重启后从断点处继续处理,也即事务恢复。

为了实现这种效果,应用程序必须提供一个稳定的(重启后不变)唯一的 ID,也即Transaction ID。Transactin ID与PID可能一一对应。区别在于Transaction ID由用户提供,而PID是内部的实现对用户透明。

另外,为了保证新的 Producer 启动后,旧的具有相同Transaction ID的 Producer 即失效,每次 Producer 通过Transaction ID拿到 PID 的同时,还会获取一个单调递增的 epoch。由于旧的 Producer 的 epoch 比新 Producer 的 epoch 小,Kafka 可以很容易识别出该 Producer 是老的 Producer 并拒绝其请求。

有了Transaction ID后,Kafka 可保证:

  • 跨 Session 的数据幂等发送。当具有相同Transaction ID的新的 Producer 实例被创建且工作时,旧的且拥有相同Transaction ID的 Producer 将不再工作。
  • 跨 Session 的事务恢复。如果某个应用实例宕机,新的实例可以保证任何未完成的旧的事务要么 Commit 要么 Abort,使得新实例从一个正常状态开始工作。

需要注意的是,上述的事务保证是从 Producer 的角度去考虑的。从 Consumer 的角度来看,该保证会相对弱一些。尤其是不能保证所有被某事务 Commit 过的所有消息都被一起消费,因为:

  • 对于压缩的 Topic 而言,同一事务的某些消息可能被其它版本覆盖
  • 事务包含的消息可能分布在多个 Segment 中(即使在同一个 Partition 内),当老的 Segment 被删除时,该事务的部分数据可能会丢失
  • Consumer 在一个事务内可能通过 seek 方法访问任意 Offset 的消息,从而可能丢失部分消息
  • Consumer 可能并不需要消费某一事务内的所有 Partition,因此它将永远不会读取组成该事务的所有消息

事务性消息传递

这一节所说的事务主要指原子性,也即 Producer 将多条消息作为一个事务批量发送,要么全部成功要么全部失败。

为了实现这一点,Kafka 0.11.0.0 引入了一个服务器端的模块,名为Transaction Coordinator,用于管理 Producer 发送的消息的事务性。

该Transaction Coordinator维护Transaction Log,该 log 存于一个内部的 Topic 内。由于 Topic 数据具有持久性,因此事务的状态也具有持久性。

Producer 并不直接读写Transaction Log,它与Transaction Coordinator通信,然后由Transaction Coordinator将该事务的状态插入相应的Transaction Log。

Transaction Log的设计与Offset Log用于保存 Consumer 的 Offset 类似。

事务中 Offset 的提交

许多基于 Kafka 的应用,尤其是 Kafka Stream 应用中同时包含 Consumer 和 Producer,前者负责从 Kafka 中获取消息,后者负责将处理完的数据写回 Kafka 的其它 Topic 中。

为了实现该场景下的事务的原子性,Kafka 需要保证对 Consumer Offset 的 Commit 与 Producer 对发送消息的 Commit 包含在同一个事务中。否则,如果在二者 Commit 中间发生异常,根据二者 Commit 的顺序可能会造成数据丢失和数据重复:

如果先 Commit Producer 发送数据的事务再 Commit Consumer 的 Offset,即At Least Once语义,可能造成数据重复。
如果先 Commit Consumer 的 Offset,再 Commit Producer 数据发送事务,即At Most Once语义,可能造成数据丢失。

用于事务特性的控制型消息

为了区分写入 Partition 的消息被 Commit 还是 Abort,Kafka 引入了一种特殊类型的消息,即Control Message。该类消息的 Value 内不包含任何应用相关的数据,并且不会暴露给应用程序。它只用于 Broker 与 Client 间的内部通信。

对于 Producer 端事务,Kafka 以 Control Message 的形式引入一系列的Transaction Marker。Consumer 即可通过该标记判定对应的消息被 Commit 了还是 Abort 了,然后结合该 Consumer 配置的隔离级别决定是否应该将该消息返回给应用程序。

kafka幂等客户端和服务端配置 kafka幂等性配置_数据_02