- 一个从kafka集群消费消息的客户端
- 该客户端透明地处理Kafka代理的故障,并透明地适应其获取的主题分区在集群内迁移的情况。该客户端还与代理进行交互,以允许一组消费方使用消费方负载均衡消费。
- 跨版本兼容性 : 该客户端可以与0.10.0或更高版本的代理进行通信。较早或较新的broker可能不支持某些功能。当调用正在运行的代理版本上不可用的API时,您将收到UnsupportedVersionException。
- 偏移量和消费位移 : Kafka维护分区中每个记录的数字偏移量。此偏移量充当该分区内记录的唯一标识符,并且还指示使用者在分区中的位置。
- 例如,位置5的消费者使用了偏移量为0到4的记录,并且接下来将接收偏移量为5的记录。实际上,有两个与消费用户相关的位置概念 : position 。。。committed position
- position 使用者的position 给出了下一个记录的偏移量。这将比消费者在该分区中看到的最大偏移量大一个。每当使用者在对poll(long)的呼叫中收到消息时,它将自动加一。
- 提交的committed position是已安全存储的最后一个偏移量。如果该过程失败并重新启动,则这是使用者将恢复到的偏移量。使用者可以自动定期提交偏移量;也可以选择通过调用一种提交API(例如commitSync和commitAsync)来手动控制此提交位置。
- 消费者组和主题订阅
- Kafka使用消费者组的概念来允许一组流程来划分消费和处理记录的工作。这些进程可以在同一台计算机上运行,也可以分布在许多计算机上,以提供可伸缩性和容错性以进行处理。共享相同group.id的所有使用者实例将属于同一使用者组。
- 群组中的每个使用者都可以通过订阅API之一动态设置要订阅的主题列表。Kafka将订阅主题中的每条消息传递给每个消费者组中的一个流程。这是通过平衡使用者组中所有成员之间的分区来实现的,以便将每个分区分配给组中的一个使用者。因此,如果一个主题有四个分区,一个使用者组有两个进程,则每个进程将从两个分区中使用。
- 消费者组中的成员资格是动态维护的 如果某个进程失败,则分配给该进程的分区将重新分配给同一组中的其他使用者。同样,如果新消费者加入该组,则分区将从现有消费者移至新消费者
- 此外,当自动进行组重新分配时,可以通过ConsumerRebalanceListener通知消费者,这使他们可以完成必要的应用程序级逻辑,例如状态清除,手动偏移提交等。
- 使用者也可以使用assign(Collection)手动分配特定分区(类似于较早的“简单”使用者)。在这种情况下,动态分区分配和使用者组协调将被禁用。
- 检测消费者失败
- 订阅了一组主题后,使用者将在调用poll(long)时自动加入该组 。 poll API 确保消费者实例是活跃的。
- 只要你继续调用poll方法,使用者就将留在组中并继续从分配给它的分区中接收消息
- 使用者定期向服务器发送心跳。
- 如果使用者在session.timeout.ms期间崩溃或无法发送心跳,则该使用者将被视为已死,并且将重新分配其分区。
- 消费者也有可能遇到“活锁”情况,即它继续发送心跳,但没有取得进展。为了防止使用者在这种情况下无限期地抓住其分区,我们使用max.poll.interval.ms设置提供了活跃度检测机制
- 基本上,如果您不以至少与配置的最大间隔相同的频率调用轮询,则客户端将主动离开该组,以便另一个使用者可以接管其分区。
- 发生这种情况时,您可能会看到偏移提交失败(如对commitSync()的调用引发的CommitFailedException所示)。这是一种安全机制,可确保只有该组的活动成员才可以提交偏移量。因此,要保留在组中,您必须继续调用poll 方法。
- 使用者提供了两个配置设置来控制轮询循环的行为:
- max.poll.interval.ms:通过增加预期轮询之间的间隔,可以使使用者有更多时间处理从poll(long)返回的一批记录。缺点是增加此值可能会延迟组重新平衡,因为使用者只会在轮询调用中加入重新平衡。您可以使用此设置来限制完成重新平衡的时间,但是如果使用者实际上不能足够频繁地调用poll方法,则可能会有进度变慢的风险。
- max.poll.records 使用此设置可限制从单次轮询返回的总记录,这样可以更轻松地预测每个轮询间隔内必须处理的最大值。通过调整该值,您可能能够缩短轮询间隔,这将减少组重新平衡的影响。
- 对于消息处理时间意外变化的用例,这些选项可能都不足够。处理这些情况的推荐方法是将消息处理移到另一个线程,这使使用者可以在处理器仍在工作时继续调用轮询。
- 必须注意确保承诺的偏移量不超过实际位置,通常,只有在线程完成对记录的处理后,才必须禁用自动提交并手动提交记录的处理偏移量(取决于所需的传递语义)。还要注意,您将需要暂停分区,以便在线程完成处理之前返回的记录之前,不会从轮询中接收到任何新记录。
使用示例
- 消费者API提供了涵盖各种消费用例的灵活性。这里有一些示例来演示如何使用它们。
- 自动偏移提交
- 这个例子演示了Kafka的消费者api的简单用法,它依赖于自动偏移量提交。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
- 设置enable.auto.commit表示 偏移量将以配置auto.commit.interval.ms控制的频率自动提交。
- 手动偏移控制
- 用户也可以控制何时应该将记录视为已消费并因此提交其偏移量,而不是依赖于使用者定期提交消耗的偏移量。
- 当消息的消耗与某些处理逻辑耦合在一起时,这很有用,因此,在完成处理之前,不应将消息视为已消耗。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
buffer.add(record);
}
if (buffer.size() >= minBatchSize) {
insertIntoDb(buffer);
consumer.commitSync();
buffer.clear();
}
}
- 在此示例中,我们将使用一批记录并将其批处理到内存中。当我们有足够的记录批处理时,我们会将它们插入数据库。如果像前面的示例一样允许偏移量自动提交,则在将记录返回给用户轮询后,记录将被视为已消耗。在批处理记录之后但在将它们插入数据库之前,我们的过程可能会失败。
- 为避免这种情况,我们仅在将相应的记录插入数据库后才手动提交偏移量。这使我们可以精确控制何时将记录视为已消耗。这就提出了相反的可能性:该过程可能会在插入数据库之后但在提交之前的间隔内失败(即使这可能只是几毫秒,这也是有可能的)。在这种情况下,接管消耗的过程将从最后提交的偏移量中消耗能量,并将重复最后一批数据的插入。以这种方式使用Kafka提供了通常称为“至少一次”的交付保证,因为每条记录可能会交付一次,但在失败的情况下可以重复。
- 使用自动偏移量提交还可以“至少一次”交付,但要求是必须在每次后续调用之前或关闭使用者之前,使用每次调用poll(long)返回的所有数据。如果您不执行任何一个操作,则已提交的偏移量有可能超过消耗的位置,从而导致记录丢失。使用手动偏移控制的优点是您可以直接控制何时将记录视为“消耗”。
- 上面的示例使用commitSync将所有接收到的记录标记为已提交。在某些情况下,您可能希望通过显式指定偏移量来更好地控制已提交的记录。在下面的示例中,在处理完每个分区中的记录后,我们提交偏移量。
try {
while(running) {
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
System.out.println(record.offset() + ": " + record.value());
}
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
}
}
} finally {
consumer.close();
}
- 提交的偏移量应始终是应用程序将读取的下一条消息的偏移量。因此,在调用commitSync(offsets)时,应在最后处理的消息的偏移量上添加一个。
- 手动分区分配
- 在前面的示例中,我们订阅了我们感兴趣的主题,并让Kafka根据组中的活跃消费者为这些主题动态分配合理的分区份额。但是,在某些情况下,您可能需要更好地控制分配的特定分区。例如:
- 如果该进程正在维护与该分区关联的某种本地状态(例如本地磁盘上的键值存储),则该进程应仅获取其在磁盘上维护的分区的记录。
- 如果流程本身具有很高的可用性,并且在流程失败时将重新启动(也许使用群集管理框架,例如YARN,Mesos或AWS工具,或者作为流处理框架的一部分)。在这种情况下,Kafka无需检测故障并重新分配分区,因为使用过程将在另一台计算机上重新启动。
- 要使用此模式,只需使用指定要使用的分区的完整列表调用assign(Collection),而不是使用subscribe订阅主题。
String topic = "foo";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0, partition1));
-
分配后,可以像前面的示例一样循环调用poll来使用记录。
-
使用者指定的组仍用于提交偏移量,但是现在分区组将仅随着另一个分配调用而更改。
-
手动分区分配不使用组协调,因此使用者故障不会导致重新分配分配的分区。
-
即使每个使用者与另一个使用者共享一个groupId,每个使用者也独立执行操作。为了避免偏移提交冲突,通常应确保groupId对于每个使用者实例都是唯一的。
-
请注意,不可能通过主题订阅(即使用subscribe)将手动分区分配(即使用assign)与动态分区分配混合在一起。
-
在kafka外存储偏移
-
消费者应用程序不需要使用Kafka的内置偏移存储,它可以将偏移存储在自己选择的存储中。这样做的主要用例是允许应用程序以原子方式存储偏移量和消耗结果的方式存储在同一系统中。这并非总是可能的,但是当这样做时,将使消耗变得完全原子化,并且给出的“精确一次”语义要比使用Kafka的偏移提交功能获得的默认“至少一次”语义强。
-
例如:如果消耗的结果存储在关系数据库中,则将偏移量也存储在数据库中可以允许在单个事务中提交结果和偏移量。因此,交易将成功,并且偏移量将基于消耗的量进行更新,或者结果将不会被存储,并且偏移量不会被更新。
-
如果结果存储在本地存储中,则也可以将偏移量存储在本地存储中。例如,可以通过订阅特定分区并将偏移量和索引数据存储在一起来构建搜索索引。如果以原子方式完成此操作,则经常可能发生这样的情况:即使发生崩溃导致丢失未同步的数据,但剩下的任何内容也将存储相应的偏移量。这意味着在这种情况下,丢失了最近更新的索引过程将恢复索引,从而确保没有更新丢失。
-
每个记录都有其自己的偏移量,因此要管理自己的偏移量,您只需执行以下操作:
1.配置enable.auto.commit = false
2.使用每个ConsumerRecord随附的偏移量保存位置。
3. 重新启动时,使用seek(TopicPartition,long)恢复使用者的位置。
- 当分区分配也是手动完成时,这种类型的使用最为简单(在上述搜索索引用例中很可能会这样)。如果分区分配自动完成,则需要特别注意处理分区分配更改的情况。这可以通过在对subscribe(Collection,ConsumerRebalanceListener)和subscribe(Pattern,ConsumerRebalanceListener)的调用中提供ConsumerRebalanceListener实例来完成。例如,当从消费者获取分区时,消费者将希望通过实现ConsumerRebalanceListener.onPartitionsRevoked(Collection)来为那些分区提交其偏移量。将分区分配给使用者后,使用者将要查找那些新分区的偏移量,并通过实现ConsumerRebalanceListener.onPartitionsAssigned(Collection)将使用者正确初始化到该位置。
- ConsumerRebalanceListener的另一个常见用途是刷新应用程序为移动到其他位置的分区维护的所有缓存。
- 控制消费者的位置
- 在大多数使用情况下,使用者将简单地从头到尾使用记录,定期(自动或手动)提交其位置。但是,Kafka允许用户手动控制其位置,随意在分区中前进或后退。这意味着使用者可以重新使用较旧的记录,或者跳到最新的记录,而无需实际使用中间记录。
- 在某些情况下,手动控制消费者的位置可能会有用。
- 一种情况是对时间敏感的记录处理,这可能对于一个落后于足够远的用户而不是试图赶上所有记录的处理,而只是跳到最近的记录是有意义的。
- 另一个用例是用于维护本地状态的系统,如上一节所述.在这样的系统中,消费者将希望在启动时将其位置初始化为本地存储中包含的内容。同样,如果本地状态被破坏(例如,由于磁盘丢失),则可以通过重新使用所有数据并重新创建状态(假设Kafka保留了足够的历史记录)在新计算机上重新创建状态。
- Kafka允许使用seek(TopicPartition,long)指定位置以指定新位置。还可以使用特殊方法来查找服务器维护的最早和最新偏移(分别为seekToBeginning(Collection)和seekToEnd(Collection))。
- 消费流量控制
- 如果为消费者分配了多个分区以从中获取数据,它将尝试同时从所有分区中进行消费,从而有效地赋予这些分区相同的消费优先级。但是,在某些情况下,消费者可能希望首先集中精力以全速从分配的分区的某些子集中获取数据,而仅在这些分区需要消耗很少或没有数据的情况下才开始获取其他分区。
- 其中一种情况是流处理,其中处理器从两个主题中获取数据并在这两个流上执行联接。当一个主题长时间落后于另一个主题时,处理器希望暂停从前面的主题中获取内容,以使落后的主题赶上来。
- 另一个示例是在消费者启动时进行引导,那里有很多历史数据需要追赶,应用程序通常希望在考虑获取其他主题之前获取某些主题的最新数据。
- Kafka通过使用pause(Collection)和resume(Collection)在指定的分配分区上暂停消耗并在将来的poll(long)调用中分别在指定的已暂停分区上恢复消耗,来支持消耗流的动态控制。
- Reading Transactional Messages 事务读消息
- 事务是在Kafka 0.11.0中引入的,其中应用程序可以原子地写入多个主题和分区。
- 为了使它起作用,应该将从这些分区读取的使用者配置为仅读取已提交的数据。这可以通过在使用者的配置中设置isolation.level = read_committed来实现。
- 在read_committed模式下,使用者将仅读取已成功提交的那些事务性消息。
- 它将像以前一样继续读取非事务性消息。
- 在read_committed模式下没有客户端缓冲。
- 取而代之的是,针对read_committed使用者的分区的结束偏移量将是该分区中属于未清事务的第一条消息的偏移量。此偏移称为“最后稳定偏移”(LSO)。
- 一个read_committed使用者将只读取LSO并过滤掉所有已中止的事务性消息。LSO还影响read_committed使用者的seekToEnd(Collection)和endOffsets(Collection)的行为,详细信息在每个方法的文档中。
- 最后,对于滞后提交的使用者,将获取延迟度量也调整为相对于LSO。
- 具有事务性消息的分区将包括表示事务处理结果的提交或中止标记。这些标记未返回给应用程序,但在日志中具有偏移量。结果,从带有事务性消息的主题中读取的应用程序将看到消耗的偏移量中的缺口。这些丢失的消息将成为事务标记,并且在两个隔离级别中都为消费者过滤掉它们。
- 另外,使用read_committed使用者的应用程序也可能会因事务异常中止而出现间隙,因为这些消息将不会由使用者返回,但具有有效的偏移量。
- 多线程处理
- Kafka消费者不是线程安全的。所有网络I / O都发生在进行调用的应用程序线程中。 用户有责任确保正确同步多线程访问。不同步的访问将导致ConcurrentModificationException。
- 该规则的唯一例外是wakeup(),可以从外部线程安全地使用它来中断活动操作。在这种情况下,将从该操作的线程阻塞中抛出WakeupException。这可用于从另一个线程关闭使用者。以下代码段显示了典型模式:
public class KafkaConsumerRunner implements Runnable {
private final AtomicBoolean closed = new AtomicBoolean(false);
private final KafkaConsumer consumer;
public void run() {
try {
consumer.subscribe(Arrays.asList("topic"));
while (!closed.get()) {
ConsumerRecords records = consumer.poll(10000);
// Handle new records
}
} catch (WakeupException e) {
// Ignore exception if closing
if (!closed.get()) throw e;
} finally {
consumer.close();
}
}
// Shutdown hook which can be called from a separate thread
public void shutdown() {
closed.set(true);
consumer.wakeup();
}
}
- 然后,在单独的线程中,可以通过设置已关闭标志并唤醒使用者来关闭使用者。
closed.set(true);
consumer.wakeup();
- 请注意,虽然可以使用线程中断代替wakeup()来中止阻塞操作(在这种情况下,将引发InterruptException),但我们不鼓励使用它们,因为它们可能会导致消费者的正常关机而中止。对于无法使用wakeup()的情况,例如,主要支持中断。当使用者线程由不了解Kafka客户端的代码管理时。
- 我们有意避免实现用于处理的特定线程模型。这为实现记录的多线程处理留下了几个选择。
- 每个线程一个消费者
- 一个简单的选择是为每个线程提供自己的使用者实例。这是此方法的优缺点:
- PRO优点:这是最容易实现的
- PRO:由于不需要线程间协调,因此通常是最快的
- PRO:它使按分区的有序处理非常容易实现(每个线程仅按接收消息的顺序处理消息)。
- CON缺点:更多的使用者意味着到群集的更多TCP连接(每个线程一个)。通常,Kafka非常有效地处理连接,因此这通常是很小的成本。
- CON缺点:多个使用者意味着更多的请求被发送到服务器,而数据的批处理则略有减少,这可能会导致I / O吞吐量下降。
- CON缺点:所有进程中的线程总数将受分区总数的限制。
- 消费与处理解耦
- 另一种选择是让一个或多个使用者线程完成所有数据消费,并将ConsumerRecords实例移交给实际处理记录处理的处理器线程池消耗的阻塞队列。此选项同样具有优点和缺点:
- PRO优点:此选项允许独立缩放使用方和处理器的数量。这样就可以有一个提供多个处理器线程的单一使用者,从而避免了对分区的任何限制。
- CON缺点:保证处理器之间的顺序需要特别注意,因为线程将独立执行,由于线程执行时机的运气,较早的数据块可能会在较晚的数据块之后实际处理。对于没有订购要求的处理,这不是问题。
- CON缺点 :手动提交位置会变得更加困难,因为它要求所有线程进行协调以确保对该分区的处理完成。
- 这种方法有很多可能的变化。例如,每个处理器线程可以拥有自己的队列,而使用方线程可以使用TopicPartition哈希到这些队列中,以确保按顺序使用并简化提交。