一篇文章了解 Kafka 幂等性的原理及实践

640?wx_fmt=jpeg

01 幂等性如此重要

Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。

02 哪些因素影响幂等性

使用Kafka时,需要保证exactly-once语义。要知道在分布式系统中,出现网络分区是不可避免的,如果kafka broker 在回复ack时,出现网络故障或者是full gc导致ack timeout,producer将会重发,如何保证producer重试时不造成重复or乱序?又或者producer 挂了,新的producer并没有old producer的状态数据,这个时候如何保证幂等?即使Kafka 发送消息满足了幂等,consumer拉取到消息后,把消息交给线程池workers,workers线程对message的处理可能包含异步操作,又会出现以下情况:

  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失

  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行

  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失

本文将针对以上问题作出讨论

03 Kafka保证发送幂等性

       针对以上的问题,kafka在0.11版新增了幂等型producer和事务型producer。前者解决了单会话幂等性等问题,后者解决了多会话幂等性。

单会话幂等性

为解决producer重试引起的乱序和重复。Kafka增加了pid和seq。Producer中每个RecordBatch都有一个单调递增的seq; Broker上每个tp也会维护pid-seq的映射,并且每Commit都会更新lastSeq。这样recordBatch到来时,broker会先检查RecordBatch再保存数据:如果batch中 baseSeq(第一条消息的seq)比Broker维护的序号(lastSeq)大1,则保存数据,否则不保存(inSequence方法)。

ProducerStateManager.scala

private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {	
    validationType match {	
      case ValidationType.None =>	
      case ValidationType.EpochOnly =>	
        checkProducerEpoch(producerEpoch, offset)	
      case ValidationType.Full =>	
        checkProducerEpoch(producerEpoch, offset)	
        checkSequence(producerEpoch, firstSeq, offset)	
    }	
}	
private def checkSequence(producerEpoch: Short, appendFirstSeq: Int, offset: Long): Unit = {	
  if (producerEpoch != updatedEntry.producerEpoch) {	
    if (appendFirstSeq != 0) {	
      if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {	
        throw new OutOfOrderSequenceException(s"Invalid sequence number for new epoch at offset $offset in " +	
          s"partition $topicPartition: $producerEpoch (request epoch), $appendFirstSeq (seq. number)")	
      } else {	
        throw new UnknownProducerIdException(s"Found no record of producerId=$producerId on the broker at offset $offset" +	
          s"in partition $topicPartition. It is possible that the last message with the producerId=$producerId has " +	
          "been removed due to hitting the retention limit.")	
      }	
    }	
  } else {	
    val currentLastSeq = if (!updatedEntry.isEmpty)	
      updatedEntry.lastSeq	
    else if (producerEpoch == currentEntry.producerEpoch)	
      currentEntry.lastSeq	
    else	
      RecordBatch.NO_SEQUENCE	
    if (currentLastSeq == RecordBatch.NO_SEQUENCE && appendFirstSeq != 0) {	

	
ne throw mew UnknownProducerIdException(s"Local producer state matches expected epoch $producerEpoch " +	
        s"for producerId=$producerId at offset $offset in partition $topicPartition, but the next expected " +	
        "sequence number is not known.")	
    } else if (!inSequence(currentLastSeq, appendFirstSeq)) {	
      throw new OutOfOrderSequenceException(s"Out of order sequence number for producerId $producerId at " +	
        s"offset $offset in partition $topicPartition: $appendFirstSeq (incoming seq. number), " +	
        s"$currentLastSeq (current end sequence number)")	
    }	
  }	
}	
  private def inSequence(lastSeq: Int, nextSeq: Int): Boolean = {	
    nextSeq == lastSeq + 1L || (nextSeq == 0 && lastSeq == Int.MaxValue)	
  }


引申:Kafka producer 对有序性做了哪些处理

假设我们有5个请求,batch1、batch2、batch3、batch4、batch5;如果只有batch2 ack failed,3、4、5都保存了,那2将会随下次batch重发而造成重复。我们可以设置max.in.flight.requests.per.connection=1(客户端在单个连接上能够发送的未响应请求的个数)来解决乱序,但降低了系统吞吐。

新版本kafka设置enable.idempotence=true后能够动态调整max-in-flight-request。正常情况下max.in.flight.requests.per.connection大于1。当重试请求到来且时,batch 会根据 seq重新添加到队列的合适位置,并把max.in.flight.requests.per.connection设为1,这样它 前面的 batch序号都比它小,只有前面的都发完了,它才能发。

    private void insertInSequenceOrder(Deque<ProducerBatch> deque, ProducerBatch batch) {	
        // When we are requeing and have enabled idempotence, the reenqueued batch must always have a sequence.	
        if (batch.baseSequence() == RecordBatch.NO_SEQUENCE)	
            throw new IllegalStateException("Trying to re-enqueue a batch which doesn't have a sequence even " +	
                "though idempotency is enabled.");	
        if (transactionManager.nextBatchBySequence(batch.topicPartition) == null)	
            throw new IllegalStateException("We are re-enqueueing a batch which is not tracked as part of the in flight " +	
                "requests. batch.topicPartition: " + batch.topicPartition + "; batch.baseSequence: " + batch.baseSequence());	
        ProducerBatch firstBatchInQueue = deque.peekFirst();	
        if (firstBatchInQueue != null && firstBatchInQueue.hasSequence() && firstBatchInQueue.baseSequence() < batch.baseSequence()) {	
            List<ProducerBatch> orderedBatches = new ArrayList<>();	
            while (deque.peekFirst() != null && deque.peekFirst().hasSequence() && deque.peekFirst().baseSequence() < batch.baseSequence())	
                orderedBatches.add(deque.pollFirst());	
            log.debug("Reordered incoming batch with sequence {} for partition {}. It was placed in the queue at " +	
                "position {}", batch.baseSequence(), batch.topicPartition, orderedBatches.size())	

	
            deque.addFirst(batch);	
            // Now we have to re insert the previously queued batches in the right order.	
            for (int i = orderedBatches.size() - 1; i >= 0; --i) {	
                deque.addFirst(orderedBatches.get(i));	
            }	
            // At this point, the incoming batch has been queued in the correct place according to its sequence.	
        } else {	
            deque.addFirst(batch);	
        }	
    }

多会话幂等性

在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。

Kafka事务通过隔离机制来实现多会话幂等性

kafka事务引入了transactionId 和Epoch,设置transactional.id后,一个transactionId只对应一个pid, 且Server 端会记录最新的 Epoch 值。这样有新的producer初始化时,会向TransactionCoordinator发送InitPIDRequest请求, TransactionCoordinator 已经有了这个 transactionId对应的 meta,会返回之前分配的 PID,并把 Epoch 自增 1 返回,这样当old producer恢复过来请求操作时,将被认为是无效producer抛出异常。     如果没有开启事务,TransactionCoordinator会为新的producer返回new pid,这样就起不到隔离效果,因此无法实现多会话幂等。

private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {	
    validationType match {	
      case ValidationType.None =>	
      case ValidationType.EpochOnly =>	
        checkProducerEpoch(producerEpoch, offset)	
      case ValidationType.Full => //开始事务,执行这个判断	
        checkProducerEpoch(producerEpoch, offset)	
        checkSequence(producerEpoch, firstSeq, offset)	
    }	
}	
private def checkProducerEpoch(producerEpoch: Short, offset: Long): Unit = {	
    if (producerEpoch < updatedEntry.producerEpoch) {	
      throw new ProducerFencedException(s"Producer's epoch at offset $offset is no longer valid in " +	
        s"partition $topicPartition: $producerEpoch (request epoch), ${updatedEntry.producerEpoch} (current epoch)")	
    }	
  }

04 Consumer端幂等性

如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:

  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失

  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行

  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失

对此我们常用的方法时,works取到消息后先执行如下code:

if(cache.contain(msgId)){	
  // cache中包含msgId,已经处理过	
        continue;	
}else {	
  lock.lock();	
  cache.put(msgId,timeout);	
  commitSync();	
  lock.unLock();	
}	
// 后续完成所有操作后,删除cache中的msgId,只要msgId存在cache中,就认为已经处理过。Note:需要给cache设置有消息

猜你喜欢

1、看完这篇还不会kafka,我跪榴莲!

2、Kylin 在满帮集团千亿级用户访问行为分析中的应用

3、如何在 Kylin 中优雅地使用 Spark

4、流系统Spark/Flink/Kafka/DataFlow端到端一致性实现对比

640?wx_fmt=png过往记忆大数据技术交流群,请添加个人微信:fangzhen0219,备注进群。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kafka幂等性生产者是一种可以保证消息只被发送一次的生产者。Kafka幂等性生产者可以避免由于生产者重试机制导致的重复消息和由于网络问题导致的消息丢失等问题,提高了Kafka的可靠性和稳定性。 Kafka幂等性生产者主要通过以下两个机制来实现: 1. 序列号:每个消息都有一个唯一的序列号,序列号由生产者自动生成。生产者在发送消息时,会将消息的序列号和其他元数据一起发送到Kafka broker。Kafka broker会根据序列号来判断消息是否重复,并保证重复消息不会被写入到Kafka的日志中。 2. 重试缓存:如果生产者在发送消息时发生错误,会进行重试。Kafka幂等性生产者会将需要重试的消息缓存到重试缓存中,并在下一次重试时将缓存中的消息重新发送。由于消息具有唯一的序列号,重试缓存可以避免重复发送相同的消息。 Kafka幂等性生产者在保证消息只被发送一次的同时,也需要注意以下几点: 1. 序列号的唯一性:为了保证序列号的唯一性,生产者需要为每个消息生成唯一的序列号。可以使用时间戳、UUID等方式来生成序列号。 2. 序列号的连续性:为了保证序列号的连续性,生产者需要在发送消息之前查询数据库或者使用缓存等方式来获取上一条消息的序列号。可以使用AtomicLong等数据结构来保证序列号的连续性。 3. 性能影响:由于需要对每条消息进行序列化和添加序列号等操作,Kafka幂等性生产者的性能会受到一定的影响。因此,在使用Kafka幂等性生产者时,需要根据具体的应用场景和需求来权衡可靠性和性能的平衡。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值