Kafka的消息模型

本文介绍了Kafka的消息模型,包括生产者、消费者和broker的角色。强调了Kafka消费者作为一组进程的特性,以及消息在生产、存储、消费三个阶段保证不丢失的策略。此外,还讨论了如何处理重复消息以及Kafka的exactly once语义,提供了实现幂等性的方法。


kafka采用的是标准的发布-订阅模型,主要用在异步、解耦的场景、流量控制(削峰填谷)的场景。

生产者和消费者

一个典型的发布订阅系统长这个样子,如图1所示。
在这里插入图片描述
图1 发布-订阅模型
在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。


在kafka中,生产者自己选择发布数据到相应的topics。存储消息的服务器叫broker,一般来说,Kafka集群有多个broker,用来做负载均衡。生产者发布数据也可以采取简单的循环方式进行负载均衡,也可以依据一定的分区策略(比如基于record中的key)。


图2展示了Kafka的整体架构。生产者发布消息到topic,每个topic又分成多个partition,每台服务器(broker)拥有0个或多个topic的partition。每个partition是一个有读写顺序的log文件,存储在硬盘上。
在这里插入图片描述
图2 kafka架构图
相比于传统的 发布/订阅 系统,Kafka中的消费者可以看做是一组合作的进程。topic中的每个partition传送给消费者组中的一个消费者。因此,partition的个数是topic的并行个数,也决定了消费者的并行程度 – 个数。
由于broker本身是无状态的,所以需要Zookeeper来维护服务器的状态。在Kafka里把消费者按标签分成消费者组(consumer group),每一条发布到topic的记录被发送到一个消费者组。消费者组中的实例可以在不同的线程或不同的机器上。

每个partition都会对应一个消费者组,消费者组之间是隔离的,partition会为每个消费者组记录当前消费的位置,而消费者组中的消费实例对消息的消费是竞争关系,一个消费者消费的消息,其他消费者就无法再消费了。

保证消息不丢失

一条消息从产生到消息要经历 生产、存储转发、消费 三个过程,要保证消息的不丢失,那在这三个阶段都需要做一些事情,来避免消息的丢失。
在这里插入图片描述
图3 消息生产-存储-消费

  • 生产阶段: 在这个阶段,从消息在 Producer 创建出来,经过网络传输发送到 Broker 端。
  • 存储阶段: 在这个阶段,消息在 Broker 端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。
  • 消费阶段: 在这个阶段,Consumer 从 Broker 上拉取消息,经过网络传输发送到 Consumer 上。

生产阶段

在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递:当你的代码调用发消息方法时,消息队列的客户端会把消息发送到 Broker,Broker 收到消息后,会给客户端返回一个确认响应,表明消息已经收到了。客户端收到响应后,完成了一次正常消息的发送。
只要 Producer 收到了 Broker 的确认响应,就可以保证消息在生产阶段不会丢失。
在编写发送消息代码时,需要注意,正确处理返回值或者捕获异常,就可以保证这个阶段的消息不会丢失。如果有异常抛出,或返回值不正确,要根据业务场景进行重复或补偿。
对于异步发送消息的方式,要使用带回调接口的发送方式,在回调接口中处理发送失败的情况,比如使用 producer.send(msg, callback) 接口而不是 producer.send(msg)

存储阶段

在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。
如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。
对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,
如果是 Broker 是由多个节点组成的集群,需要将 Broker 集群配置成:至少将消息发送到 2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的 Broker 可以替代宕机的 Broker,也不会发生消息丢失。

消费阶段

在消费端拉取到消息后,不要立即ack,进行成功的消费逻辑处理后再进行ack。kafka消费端,对参数 enable.auto.commit ,最好设置成 false ,手动来处理 offset 的更新。
如果消费逻辑比较复杂耗时的话,也可以考虑把消息存储到数据库中,存储成功后进行应答,然后消费线程再消费存到库中的数据。

如何处理重复消息

Kafka支持的三种消息投递语义:

  • at most once:至多一次,消息可能会丢,但不会重复
  • at least once:至少一次,消息肯定不会丢失,但可能重复
  • exactly once:有且只有一次,消息不丢失不重复,且只消费一次。
    在这里插入图片描述
    图4 消息发送的理想路径
    先来看个例子,如图4,假设只有一个生产者和消费者,对应的partition也只有一个,生产者把消息发送给broker,broker进行应答,消费者从broker拉取消息,理想情况下,消息不会丢失,不会重复。
    但实际的应用中,可能存在各种问题破坏这种理想的传送路径,比如:
  1. broker有可能挂掉,这就需要对partition进行持久和冗余,只要集群中有一个broker能正常工作,就可以保证partition可用;
  2. producer和broker之间的通信可能发生故障。比如producer消息发送给topic的过程中网络断开或超时,或者broker应答producer失败,通常producer会重新发一条消息,以避免消息的丢失,那么partition中就可能存在重复的消息了;
  3. consumer端也会发生类似的问题,消费失败或提交offset失败;


基于以上所述的实际应用中存在的一些棘手的问题,常用at least once 这种语义,保证消息不丢失,让消费端自己去处理消息重复的情况,大多数消息中间件都支持 at least once 。kafka官方文档上说kafka支持 exactly once 语义,实际上它所支持的 at least once 并不是完整的,这个文章后面再分析。
但在这种语义下,消费端可能收到重复的消息,通常消费端采用 幂等 的方式来避免重复消息带来的副作用。
所谓幂等是指: 多次操作和一次操作产生的副作用是一样的。
我们可以用如下几种方式来实现“幂等”:

  • 利用数据库的唯一索引的约束

假设消费者听的消息是用户注册的消息,然后处理一些新用户奖励等逻辑,那么可以把用户id作为数据库的唯一索引,如果有两条消息的用户id是一样的,有了唯一性的约束,就能保证最终一个用户只会处理一次。

  • 利用CAS的原理,比较并更新

java并发包中的原子类 – AtomicLong等 – 是采用CAS (compare and swap) 的原理实现在多线程下的数据更新,我们可以借鉴这个思路,比如说更新一个工单的状态,在where条件中加上期望的更新前的状态,也可以达到幂等的效果。如果说你的场景更新的逻辑比较复杂,无法使用整数比较,那么可以用版本号,更新的时候对比消息中的版本号和数据的版本号,如果版本号不一致,就放弃此次更新。

  • 先查询校验,再更新

给每条消息增加一个全局唯一的id,在消费消息的时候,先利用这个id查出数据,校验是否已经消费过了,如果没有消费过,再消费。这种方法在高并发的场景下也会有问题,比如两个线程都各自收到了某个id的消息,查出来的数据状态都是未消费过,那这两个线程再去消费,就重复消费了。所以,这种方法还要借助分布式锁来保证。

kafka的exactly once语义

  • 从0.11.0.0版本开始,broker会给每个producer分配一个id,发送给broker的每条消息有一个序列号,如果一条消息重复发送到某个partition中,broker可以根据消息的序列号来丢弃重复的消息,这样就保证了在一个partition中消息的唯一性。在生产端配置 enable.idempotence=true 打开这种幂等性配置。
  • 同时,在0.11.0.0版本中kafka支持不同partition间的事务,发送给不同partition的一批消息,要么都成功,要么都失败;

比如下面的代码所示:

producer.initTransactions();
try {
  producer.beginTransaction();
  producer.send(record1);
  producer.send(record2);
  producer.commitTransaction();
} catch(ProducerFencedException e) {
  producer.close();
} catch(KafkaException e) {
  producer.abortTransaction();
}
  • 使用Kafka Stream,消费消息,经过计算后,发送到另外一个topic,可以实现exactly once,详细描述可以参考下官方文档


上面的方式只能保证producer端的exactly once,对于消费端来说,还是需要借助两阶段提交等手段来实现exactly once

扫二维码关注我,一起讨论算法。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值