交付(传递)语义
所谓的交付语义(也叫传递语义) 是指在分布式消息系统(如 Kafka)中,用来描述消息从生产者到达消息队列并最终被消费者消费时的可靠性保证。它主要涉及到消息是否能正确地被投递,及在什么情况下可能会出现消息丢失或重复的问题。
一般的消息队列,支持三种交付语义:
- 最多一次(At Most Once)
- 至少一次(At Least Once)
- 有且仅一次(Exactly Once)
最多一次
在最多一次交付语义中,生产者在发送消息后不会做太多的检查,如果消息因为网络问题或者系统崩溃没有送达,那这条消息就永远丢失了。
在这种交付语义下,系统只尝试发送一次消息,不需要处理复杂的重试逻辑,也不需要复杂的检查和确认机制。因此系统具有较高的吞吐量和较低的延迟。
这种交付语义适用于指标收集、日志收集等场景,这是因为:
- 指标收集:在收集大量的指标数据时,丢失少量数据不会对总体分析产生重大影响,但保证高吞吐量和低延迟非常重要。
- 日志收集:在日志收集中,丢失某些日志条目可能不会对系统的整体健康造成重大影响,重点是确保日志的实时性和收集的效率。
至少一次
在至少一次交付语义中,可以多次传递一条消息,但不应丢失任何消息。生产者确保所有消息都已传递,即使这可能导致消息重复。这是所有交付语义中使用场景最多的。
有且仅一次
在“有且仅一次”交付语义中,一条消息只能传递一次,并且不能丢失任何消息。这是所有传递语义中最困难的。与其他两种语义相比,采用“有且仅一次”语义的系统需要额外的复杂性来确保消息的唯一性和正确处理,通常涉及去重机制和精确的确认机制。
因此这种交付语义适用于金融交易系统、订单处理系统等场景,这是因为:
- 金融交易系统:在金融交易中,确保每笔交易被处理恰好一次是至关重要的。重复处理可能会导致资金错误或不准确的记录,而丢失交易会导致财务损失或交易失败。
- 订单处理系统:在处理订单时,确保每个订单只处理一次是重要的。如果订单被处理多次,可能会导致多次发货或库存错误。相反,丢失订单会导致客户不满和销售损失。
注意:其实"有且仅一次"适用的场景 也适用于 “至少一次”,只是需要消费者在消费消息的时候做好去重处理就可以了。例如订单处理系统中,。如果系统采用“至少一次”的消息交付语义,可能会出现订单创建请求被多次传递的情况。但如果消费者能够检测到重复的订单请求(例如通过订单号或事务ID),并在处理时进行去重,那么最终的效果依然是“有且仅一次”的。
交付语义总结
下表总结了所有交付语义的行为。
Kafka生产者交付语义
在 Kafka 中可以使用生产者的 Acks 属性和broker的 min.insync.replica 属性实现不同的交付语义。
具体来说,acks属性决定了生产者在发送消息时等待多少确认,而min.insync.replicas 属性决定了一个分区必须有多少个同步副本才能接受写操作。
acks = 0
当 acks 属性设置为0时,生产者将获得最多一次交付语义。Kafka 生产者将消息发送给broker,不会等待任何确认响应。在此设置下,即使发送的消息丢失也不会进行重试。
这种语义下,生产者采用“发送即忘”的方式。这意味着生产者只负责将消息发送出去,而不会去关心消息是否成功到达broker,也不需要处理消息发送失败的情况。
这种交付语义下,数据丢失的可能性很高:
- 由于网卡故障,消息无法到达broker;
- 即使消息发送到broker的leader副本,但是当leader副本发生故障时,由于新选出的leader没有这条消息,这样也会导致数据丢失。
因此这种交付语义适合对丢失消息容忍度较高、需要低延迟的场景,如某些日志记录或监控系统。
acks = all(-1)
设置 Kafka 生产者 acks=all 和重试策略,可以确保消息至少发送一次。
当acks设置为 all,生产者发送消息后,会等待来自broker的确认。如果生产者在设定的时间内没有收到确认响应,则生产者将根据重试配置(retries 属性)重试发送消息。重试属性默认为 0,请确保将其设置为所需数字或 MAX.INT。
props.put(ProducerConfig.RETRIES_CONFIG, "30");
这种模式下,消息可能会重复,当生产者发送消息到 Kafka 并等待所有副本(acks=all)确认时,可能会遇到网络故障或超时。如果生产者在预定时间内没有收到来自 Kafka 的确认,可能会误以为消息没有成功发送,然后自动重试发送消息。实际上,消息可能已经成功到达 Kafka,只是确认消息因网络问题未能及时返回。因此,生产者重发的消息会导致消息在 Kafka 中重复。
因此这种交付语义适合需要确保消息传递的场景,如金融交易系统其中丢失消息不被接受,但重复消息是可以的(消费者去重处理即可)。
acks = all(-1)且 enable.idempotence=true
当 acks 属性设置为 all 且enable.idempotence=true时,可以实现仅一次的交付语义。这个配置的工作流程如下:
-
acks设置为 all:在这种模式下,生产者发送消息后,会等待来自broker的确认。broker在确保ISR中 所有的副本都已成功写入后,才会发送确认响应。
-
重试机制:如果生产者在设定的时间内没有收到确认响应,它会根据重试配置(设置的重试次数)重新尝试发送消息。确保生产者不会因为偶发的网络问题或临时故障而丢失消息。
-
复制设置:min.insync.replicas 表示在处理请求时需要存在的最少同步副本数量。这样,即使某些副本发生故障,只要剩余副本能够正常工作,数据也不会丢失。
-
幂等性:为了实现**“恰好一次”**的交付语义,broker必须支持幂等性(配置enable.idempotence=true)。 这样即使生产者重试发送同一条消息,Kafka 也只会保存一次,避免重复数据的问题。
//开启冥等性
//生产者代码中设置
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // 启用幂等性
props.put(ProducerConfig.ACKS_CONFIG, "all"); // 设置acks为all以确保数据可靠性
注意:这里再次强调min.insync.replicas 这个配置,它是broker上的配置,它指定了当acks=all时,ISR 中必须存在的最少同步副本数量。如果 ISR 中的同步副本数量小于这个值,写入操作将会失败。
误区:
一个常见的误解是:认为min.insync.replicas表示有多少副本需要收到消息才能让leader响应ack给生产者。
事实并非如此,实际上,min.insync.replicas表示在处理请求时需要存在的最少同步副本数量。
以下图为例。即使配置了min.insync.replicas=2,leader也不会在仅有2个副本确认的情况下响应ack给生产者,而是等待所有3个副本确认才会响应ack给生产者。