消费者
消费者角色
在Kafka的架构中, 消费者与生产者一样,是 Kafka 数据处理的重要角色。消费者的主要任务是与 Kafka 集群建立连接,从相应的 Kafka broker读取消息记录。
多应用消费
多个应用程序可以从同一个 Kafka 主题中消费记录,每个应用程序都会获取该数据的一个独立副本,并按照各自的节奏进行读取。换句话说,一个应用程序的消费偏移量(消费偏移量的概念后面有介绍)可能与另一个应用程序的消费偏移量不同。Kafka在内部的“__consumer_offset”主题中记录了每个应用程序消费偏移量。
消费者组和消费者
在Kafka中,消费者组(Consumer Group)是一个非常重要的概念,消费者组是由一个或多个消费者实例组成的集合,这些消费者共同消费一个或多个主题中的消息。
每个消费 Kafka 数据的应用程序都被视为一个消费者组,应用程序的每个实例就是一个消费者。
下图描绘了一个只有一个消费者的消费者组。
一个消费者可以消费一个主题下的多个分区消息,下图中,Consumer 1消费了Topic A中的Partition 1和Partition 2.
一个分区只能被同一个消费组中的一个消费者消费,换句话说,同一个分区不会由同一个消费组中的两个消费者处理,理解这一点非常重要,如下图所示:
因此当消费者组中的消费者多于主题中的分区时,多余的消费者将会空闲,不被使用,下图中,Topic A有1个分区,Consumer Group 1虽然有2个消费者,但是只有1个消费者能够消费Topic A的这个分区消息。
当应用程序想要提高处理速度时,可以向消费者组添加更多消费者。Kafka负责管理每个消费者组中消费者的偏移量以及在添加或删除消费者时重新分配分区,下面的例子展示了往消费者组中新增消费者时重新分配分区的过程。
当Consumer Group 1只包含一个消费者时,这个消费者会消费 Topic T1中所有分区的消息。
如果新增一个消费者加入到Consumer Group 1,此时消费者组包含2个消费者,组中的每个消费者都被分配到Topic T1中一半的分区。
随着消费者的源源不断加入,Topic T1中的分区会尽可能被均衡分配到每个消费者上供其消费。直到消费者数量与分区数量一致。
一旦消费者数量超过分区数量,多余的消费者将处于闲置状态,不会收到任何消息,因为同一个消费组中的不同消费者不能消费同一个分区的消息。
如果需要,您还可以让多个消费者组读取同一Topic的消息。每个消费者组将维护自己的一组消费者偏移量,简单地说,Consumer Group 1 收到的消息也会被Consumer Group 2 收到。但同一个消费组里不同的消费者不能消费相同的消息,这里再次强调这点。
消费者偏移(Consumer Offset)
在 Kafka 中,消费者偏移(Consumer Offset)是一个很关键的概念,它是用于管理消费者在主题(Topic)分区(Partition)中的消费进度和状态,通过它可以了解到每个消费者已经从分区中消费了多少条消息以及下一步从哪里开始。
消费者偏移量是消费者在分区内读取的最后一条记录的位置的数值标识符。消费者需要定期提交它们到Broker上。Kafka 在内部维护了一个名为 “__consumer_offsets” 的主题,Kafka 就是将消费者位移存储在这个主题中。
假设你有一个 Kafka 主题叫 MyTopic,用来记录订单信息。这个主题有两个分区(分区 0 和分区 1)。你有两个消费者组:一个叫 Consumer Group A,负责处理订单;另一个叫 Consumer Group B,负责审计订单。
-
Consumer Group A 消费者组:这个消费者组的一个消费者从 MyTopic 的分区 0 中读取了消息,并处理到了偏移量 2。它提交了这个偏移量给 Kafka,Kafka 便把这个信息存储在 __consumer_offsets 主题中。于是,__consumer_offsets 记录了 Consumer Group A 组在 MyTopic 的分区 0 中最新的偏移量是 2。
-
Consumer Group B 消费者组:与此同时,Consumer Group B 组可能在审计数据,它的一个消费者从 MyTopic的分区 0 中读取并处理到了偏移量 8。它提交了这个偏移量,Kafka 同样把这个信息写入 __consumer_offsets 主题中。
消费者偏移量提交方式
Kafka 提供了两种主要的提交方式:自动提交和手动提交。
手动提交(Manual Commit)
手动提交消费者偏移量(enable.auto.commit参数设置为fale)就像手动保存文件,你可以完全控制什么时候保存进度。你可以在确保消息处理完毕后再提交偏移量。
在手动提交偏移量情况下,如果消费者在处理消息后但在提交其偏移量之前发生故障,就会导致重复消费消息,下图中,CONSUMER-1成功消费了PARTITION 0的消息0-4,但在提交偏移量之前crash了,这时经过重新平衡之后,组内的CONSUMER-2接管了PARTITION 0,重新从消息0开始消费。
自动提交(Auto Commit)
如果消费者设置为自动提交(enable.auto.commit参数设置为true),则意味着偏移量在poll()函数成功返回时已经提交了,而不管消费者是否对消息是否已经成功进行了业务处理,因此如果消费者在处理此消息时崩溃,会导致消息丢失。
在下图中,消费者设置了自动提交,CONSUMER-1成功拉取到了PARTITION 0的消息0-4,消费偏移量已经自动提交了,但是在真正处理消息的时候,CONSUMER-1宕机了,经过重新平衡之后,组内的CONSUMER-2接管了PARTITION 0,将会从消息5开始拉取消息,这样消息0-4用于也没机会消费了。
消费者启动和偏移量
当 Kafka 消费者启动时,它需要决定从哪个位置开始消费消息。这主要取决于两个因素:
- 消费者组的已提交偏移量
- auto.offset.reset 属性
1. 消费者组的已提交偏移量
如果消费者组之前已经消费过该分区的数据,Kafka 会查看 __consumer_offsets 主题中记录的最新偏移量。这是消费者上次停止时的进度,消费者会从这个位置开始继续消费,以确保不会重复消费消息。
2. auto.offset.reset 属性
如果消费者组是新的,或者消费者组从未处理过该分区的数据(例如,第一次消费该主题),Kafka 会根据 auto.offset.reset 属性来决定从哪个位置开始消费。
-
earliest(最早):如果 auto.offset.reset 设置为 earliest,消费者会从分区中最早的消息开始消费。这意味着它会从分区的起始位置开始读取所有可用的消息,包括消费者组未处理的旧消息。
-
latest(最新):如果 auto.offset.reset 设置为 latest,消费者会从分区中最新的消息开始消费。这意味着它会跳过已经存在的消息,直接从分区中新到达的消息开始读取。
消费者滞后
消费者滞后是指 Kafka 中消费者处理消息的速度跟不上生产者生产消息的速度,从而导致消息堆积在 Kafka 中的现象。通俗地讲,就像你在处理一堆快递包裹,而包裹送来的速度远远超过你拆包裹的速度,于是未拆的包裹越堆越多,这就是“滞后”。
消费者滞后可能给系统带来丢失消息的风险,broker在工作过程中可能移除旧日志段,如果消费者滞后过大,可能会导致一些旧消息还没被消费就已经被删除了。
可以使用kafka-consumer-groups工具查看和管理消费者滞后。
如何减少滞后?
- 提高消费者的处理速度:优化消费逻辑或增加消费者数量。
- 增加分区数量:通过更多分区,分散消息压力,增加并行处理能力。
- 监控滞后:使用 Kafka 提供的监控工具,及时发现和处理滞后问题。
Kafka消费者和协调器协议
协调员的角色
当一个消费者想要加入或创建一个消费者组时,它首先需要找到 Kafka 集群中负责管理该消费者组的协调员(Coordinator)。这个过程开始于消费者向一个引导服务器(Bootstrap Server)发送一个“Find Coordinator”的请求。
如果消费者群组的协调员还没有确定,Kafka 会根据特定的哈希公式计算出一个合适的 broker 作为该消费者组的协调员。然后,这个协调员的地址将作为对“Find Coordinator”请求的响应返回给消费者。
加入组
一旦消费者得知了协调员的地址,它就会向协调员发送一个“join group”的请求。协调员会处理这个请求,并返回消费者组的领导者和其他相关的元数据详细信息。如果这个消费者组还没有领导者,那么第一个加入的消费者会被选为领导者。
此外,消费者可以通过特定的配置来控制哪个消费者会被选为领导者
同步组
在消费者收到领导者的详细信息后,他们会向协调员发送一个“sync group”的请求。这一请求会触发消费者组内部的重新平衡过程,分配给消费者的分区将在“sync group”请求后发生变化。
重新平衡
重新平衡是在以下几种情况下触发的:当有新的消费者加入或已有的消费者退出群组,或者当某个消费者发送了“sync group”请求时。
在重新平衡过程中,消费者组中的所有消费者都会收到更新后的分区分配。这意味着消费者在重新平衡完成之前将暂停数据消费,以确保数据分配的有序性和一致性。
心跳
为了保持与协调员的连接,消费者组中的每个消费者都会定期向协调员发送心跳信号。如果协调员在一段时间内未收到某个消费者的心跳信号,就会认为该消费者已失联,并启动重新平衡过程,以重新分配该消费者负责的分区给其他活跃的消费者。
离开组
消费者可以随时通过发送“leave group”请求离开组。协调器将确认请求并启动重新平衡。如果消费者组中的领导节点离开组,组内将选出一个新的领导者并启动重新平衡。
Kafka 消费者交付语义(传递语义)
Kafka 消费者交付语义指的是 Kafka 消费者在处理消息时如何保证消息的可靠性和一致性。这涉及到消息是否被丢失、重复处理或者按顺序消费。
Kafka消费者交付语义有三种,即:
- 最多一次
- 至少一次
- 精确一次
当消费者组/消费者从 Kafka 消费数据时,仅支持最多一次和至少一次这两种语义。但是您可以通过选择适当的数据存储来实现类似于精确一次的交付语义,例如,任何键值存储、RDBMS(主键)、Elasticsearch或任何其他支持幂等写入的存储。
最多一次
在最多一次传递语义中,消息最多只能传递一次。在这种语义中,宁可丢失消息也不应重复传递消息。采用最多一次语义的应用程序可以轻松实现更高的吞吐量和较低的延迟。默认情况下,由于“enable.auto.commit”为 true,因此Kafka消费者设置为使用“最多一次”传递语义。
这种语义下,如果消费者在将消息提交为已读,但是在处理消息之前宕机了或者消息处理失败,则未处理的消息将丢失,并且不会再次读取,分区重新平衡将导致另一个消费者从上次提交的偏移量读取消息。
如下图所示,消息是分批读取的,批次中的部分或全部消息可能未处理,但仍已提交为已处理,这就造成了消息的丢失。
至少一次
在至少一次传递语义中,可以多次传递消息,但不应丢失任何消息。消费者确保所有消息都被读取和处理,即使这可能导致消息重复。为了在消费数据时做到至少一次语义,需要将“enable.auto.commit”值设置为“false”`,您可以选择在处理完消息后手动提交,这样你就掌握了消费偏移量提交的主动权,只有消费成功的消息偏移量才会提交。
如果消费者在处理消息之前发生故障,未处理的消息不会丢失,因为偏移量未提交,分区重新平衡将导致另一个消费者从上次提交的偏移量再次读取相同的消息进进行处理。
但是如果消费者在处理消息之后、提交消费偏移量之前发生故障,因为偏移量未提交,分区重新平衡将导致另一个消费者从上次提交的偏移量再次读取相同的消息进进行处理,这就导致这批消息会被重复消费。
精确一次
在精确一次传递语义中,一条消息只能传递一次,并且不能丢失任何消息。这是所有传递语义中最困难的。与其他两种语义相比,采用精确一次语义的应用程序可能具有较低的吞吐量和较高的延迟。
就像前面介绍的一样,可以通过选择适当的数据存储来实现类似于精确一次的语义。
我们可以通过选择支持幂等写入数据存储来实现。幂等写入意味着即使重复执行相同的写入操作,结果也不会改变。这可以确保在至少一次语义中,即使消息被多次处理,最终数据存储中的数据不会重复。
假设我们使用MySQL作为数据存储,并且每条消息都有一个唯一的ID(例如,消息偏移量)。我们将消息写入MySQL数据库的表中,并使用消息的唯一ID作为主键。
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
props.put("group.id", "my-group");
props.put("enable.auto.commit", "false"); // 禁用自动提交
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("myTopic"));
try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password")) {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
String insertSQL = "INSERT INTO my_table (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=?";
try (PreparedStatement ps = connection.prepareStatement(insertSQL)) {
ps.setLong(1, record.offset());
ps.setString(2, record.value());
ps.setString(3, record.value());
ps.executeUpdate();
}
}
consumer.commitSync();
}
} catch (SQLException e) {
e.printStackTrace();
}
下面图表展示了如何使用消息偏移量作为唯一标识符进行幂等写入:
Kafka Topic: myTopic
+-----------+-----------+-----------+
| Offset 0 | Offset 1 | Offset 2 |
| Message A | Message B | Message C |
+-----------+-----------+-----------+
消费者读取消息并将其写入MySQL数据库:
+-------------------------------+
| MySQL数据库: my_table |
+-----------+-------------------+
| ID (Offset) | Value |
+-----------+-------------------+
| 0 | Message A |
| 1 | Message B |
| 2 | Message C |
+-----------+-------------------+
如果消息重复处理(例如,Offset 1的Message B),由于使用了ON DUPLICATE KEY UPDATE,写入操作将更新现有记录,而不会插入重复记录。
订阅
要从Kafka主题读取记录,请创建一个Kafka消费者实例并订阅一个或多个Kafka主题。您可以使用正则表达式订阅一个主题列表,例如“myTopic.*”。
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String,
String>(props);
consumer.subscribe("myTopic.*");
Poll方法
消费者通过轮询新数据的方式从Kafka读取数据。Poll方法处理所有协调工作,如分区再平衡、心跳和数据获取。当auto-commit设置为true时,poll方法不仅读取数据,还提交偏移量,然后读取下一批记录。
v