2021-01-30实习日报

启动kafka

下载并解压kafka
tar -xzf kafka_2.13-2.7.0.tgz
cd kafka_2.13-2.7.0
启动服务

运行kafka需要使用Zookeeper,可以使用kafka自带打包和配置好的zookeeper

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/zookeeper-server-start.sh  config/zookeeper.properties 
[2021-01-30 10:05:29,223] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
[2021-01-30 10:05:29,225] WARN config/zookeeper.properties is relative. Prepend ./ to indicate that you're sure! (org.apache.zookeeper.server.quorum.QuorumPeerConfig)

然后启动kafka服务

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-server-start.sh  config/server.properties &
[1] 16411
xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ [2021-01-30 10:08:06,822] INFO Registered kafka:type=kafka.Log4jController MBean (kafka.utils.Log4jControllerRegistration$)
[2021-01-30 10:08:07,703] INFO Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initia什么ted TLS renegotiation (org.apache.zookeeper.common.X509Util)什么

启动成功,kafka可以使用了

创建一个主题

创建一个名为“Test”的Topic,只有一个分区和一个备份

bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test

创建好运行以下命令,查看已经创建的topic信息

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-topics.sh  --list --zookeeper localhost:2181
test

或者除了手工创建topic外,也可以配置自己的broker,发布一个不存在的topic时自动创建topic

发送消息

kafka提供一个命令行的工具,可以从输入文件或者命令行中读取消息并发送给kafka集群,每一行是一条信息,运行producer,人后再控制台输入几条消息到服务器中

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
>This is a message
>This is another message
消费消息

kafka也提供了一个消费信息的命令行工具,将存储的信息输出出来

> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
This is a message
This is another message

在2台不同的终端上运行上诉命令,那么在运行生产者的时候,消费者就能消费到生产者发送的信息

设置多个broker集群

首先为每个broker创建一个配置文件

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ cp config/server.properties config/server-1.properties
xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ cp config/server.properties config/server-2.properties

现在编辑这些新建的文件,设置属性如下
config/server-1.properties:

    broker.id=1 
    listeners=PLAINTEXT://:9093 
    log.dir=/tmp/kafka-logs-1

config/server-2.properties:

    broker.id=2 
    listeners=PLAINTEXT://:9094 
    log.dir=/tmp/kafka-logs-2

broker.id是集群中每个节点的唯一且永久的名称,我们修改端口和日志目录是因为我们现在在同一台机器上运行,我们要防止broker在同一端口上注册和覆盖对方的数据
我们已经运行了zookeeper和刚才的一个kafka节点,所以我们只需要在启动两个新的kafka的节点

> bin/kafka-server-start.sh config/server-1.properties &
... 
> bin/kafka-server-start.sh config/server-2.properties &
...

现在,我们创建一个新topic,把备份设置为3

> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic
Error while executing topic command : Replication factor: 3 larger than available brokers: 1.
[2021-01-30 10:46:53,217] ERROR org.apache.kafka.common.errors.InvalidReplicationFactorException: Replication factor: 3 larger than available brokers: 1

发现出错,原来是刚才创建新的brokers的时候,地址正在使用中,原来是配置文件中的注释#号忘记去掉了,丢人

org.apache.kafka.common.KafkaException: Socket server failed to bind to 0.0.0.0:9092: 地址已在使用.

运行成功

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic
Created topic my-replicated-topic.

运行命令 describe topics可以知道每个集群都在做什么

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic
Topic: my-replicated-topic	PartitionCount: 1	ReplicationFactor: 3	Configs: 
Topic: my-replicated-topic	Partition: 0	Leader: 0	Replicas: 0,2,1	Isr: 0,2,1

输出解释:第一行是所有分区的摘要,其次每一行提供一个分区信息,因为我们只有一个分区,所以只有一行

  • “leader”:该节点负责该分区的所有读和写,每个节点的leader都是随机选择的
  • “replicas”:备份的节点列表,无论该节点是否是leader或者目前是否或者,只是显示
  • ”isr“:”同步备份的节点列表“,也就是活着的节点并且正在同步leader

看一下test节点

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
Topic: test	PartitionCount: 1	ReplicationFactor: 1	Configs: 
Topic: test	Partition: 0	Leader: 0	Replicas: 0	Isr: 0

刚才创建的主题没有Replicas,并且在服务器“0”上,我们创建它的时候,集群中只有一个服务器,所以是“0”。
发布一些信息在新的topic上

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic my-replicated-topic
>my test message 1
>my test message 2

消费这些消息

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic my-replicated-topic

[2021-01-30 11:09:56,877] INFO [GroupCoordinator 0]: Preparing to rebalance group console-consumer-38633 in state PreparingRebalance with old generation 0 (__consumer_offsets-44) (reason: Adding new member consumer-console-consumer-38633-1-6d283a4a-3848-4637-b885-e720982facdb with group instance id None) (kafka.coordinator.group.GroupCoordinator)
[2021-01-30 11:09:56,878] INFO [GroupCoordinator 0]: Stabilized group console-consumer-38633 generation 1 (__consumer_offsets-44) (kafka.coordinator.group.GroupCoordinator)
[2021-01-30 11:09:56,883] INFO [GroupCoordinator 0]: Assignment received from leader for group console-consumer-38633 for generation 1 (kafka.coordinator.group.GroupCoordinator)
my test message 1
my test message 2

测试集群的容错,kill掉leader,Broker1作为当前的leader,也就是kill掉Broker1

> ps | grep server-1.properties
7564 ttys002    0:15.91 /System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home/bin/java... 
> kill -9 7564

让备份节点之一成为新的leader,而broker1已经不再同步备份集合里了

> bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic
Topic:my-replicated-topic    PartitionCount:1    ReplicationFactor:3    Configs:
Topic: my-replicated-topic    Partition: 0    Leader: 2    Replicas: 1,2,0    Isr: 2,0

消息仍然没有丢

> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic my-replicated-topic
...
my test message 1
my test message 2
使用kafka Connect来导入/导出

Kafka Connect是导入和导出数据的一个工具。它是一个可扩展的工具,运行连接器,实现与自定义的逻辑的外部系统交互。在这个快速入门里,我们将看到如何运行Kafka Connect用简单的连接器从文件导入数据到Kafka主题,再从Kafka主题导出数据到文件。

首先我们首先创建一些种子数据用来测试

xfgg@xfgg-OptiPlex-3050:~/Downloads/kafka_2.13-2.7.0$ echo - e "foo\nbar" > test.txt

接下来我们开启2个连接器运行在独立的模式,这意味着它们运行在一个单一的,本地的,专用的进程。我们提供3个配置文件作为参数,首先是kafka Connect处理器的配置,包含常见的配置,例如要连接的kafka broker和数据的序列化格式,其余的配置文件都指定了要创建的连接器,包括连接器唯一名称,和要实例化的连接器类,以及连接器所需的任何其他配置

bin/connect-standalone.sh config/connect-standalone.properties config/connect-file-source.properties config/connect-file-sink.properties

启动过程可以看到一些日志消息,包括一些连接器实例化的说明,一旦kafka connect进程已经开始,导入连接器应该读取从test.txt和写入到topic connect-test,导出连接器从主题connect-test,读取消息写入到文件 test.sink.txt

我们可以通过验证输出文件的内容来验证数据已经全部导出

more test.sink.txt
 foo
 bar

导入的数据也在kafka主题所以可以使用命令查看这个主题

bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic connect-test --from-beginning
 {"schema":{"type":"string","optional":false},"payload":"foo"}
{"schema":{"type":"string","optional":false},"payload":"bar"}

连接器继续处理数据,因此我们可以添加数据到文件并通过管道移动
echo “Another line” >> test.txt

使用kafka stream来处理数据

Kafka Stream是kafka的客户端库,用于实时流处理和分析存储在kafka broker的数据
WordCountDemo的例子

KTable wordCounts = textLines
    // Split each text line, by whitespace, into words.
    .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("W+")))

    // Ensure the words are available as record keys for the next aggregate operation.
    .map((key, value) -> new KeyValue<>(value, value))

    // Count the occurrences of each word (record key) and store the results into a table named "Counts".
    .countByKey("Counts")

它实现了wordcount算法,从输入的文本计算出一个词出现的次数。然而,不像其他的WordCount的例子,你可能会看到,在有限的数据之前,执行的演示应用程序的行为略有不同,因为它的目的是在一个无限的操作,数据流。类似的有界变量,它是一种动态算法,跟踪和更新的单词计数。然而,由于它必须假设潜在的无界输入数据,它会定期输出其当前状态和结果,同时继续处理更多的数据,因为它不知道什么时候它处理过的“所有”的输入数据。

现在准备输入数据的卡夫卡的topic中,随后kafka Stream应用处理这个topic的数据

> echo -e "all streams lead to kafka\nhello kafka streams\njoin kafka summit" > file-input.txt

接下来1是哟给你控制台的producer将输入的数据发送到制定的topic中

> bin/kafka-topics.sh --create \
            --zookeeper localhost:2181 \
            --replication-factor 1 \
            --partitions 1 \
            --topic streams-file-input

现在运行WordCount处理输入数据

./bin/kafka-run-class org.apache.kafka.streams.examples.wordcount.WordCountDemo

不会有任何的STDOUT输出,除了日志,结果不断地写回另一个topic(streams-wordcount-output),demo运行几秒,然后,不像典型的流处理应用程序,自动终止。
现在我们检查WordCountDemo应用,从输出的topic读取。

./bin/kafka-console-consumer --zookeeper localhost:2181 
            --topic streams-wordcount-output 
            --from-beginning 
            --formatter kafka.tools.DefaultMessageFormatter 
            --property print.key=true 
            --property print.key=true 
            --property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer 
            --property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer

Kafka核心API

Kafka有5个核心API:
Producer API 允许应用程序发送数据流到kafka集群中的topic。
Consumer API 允许应用程序从kafka集群的topic中读取数据流。
Streams API 允许从输入topic转换数据流到输出topic。
Connect API 通过实现连接器(connector),不断地从一些源系统或应用程序中拉取数据到kafka,或从kafka提交数据到宿系统(sink system)或应用程序。
Admin API 用于管理和检查topic,broker和其他Kafka对象。
今天先学两个

生产者Java客户端

maven依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.7.0</version>
</dependency>

kafka客户端发布record(消息)到kafka集群
新的生产者是线程安全的,在县城之间共享单个生产者的实力,通常单例比多个实例要快
一个简单的例子,使用producer发送一个有序的key/value键值对

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

Producer<String, String> producer = new KafkaProducer<>(props);
for(int i = 0; i < 100; i++)
    producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));

producer.close();

生产者的缓冲空间池保留尚未发送到服务器的消息,后台I/O线程负责将这些消息转换成请求发送到集群。如果使用后不关闭生产者,则会丢失这些消息。

各个参数的意义

  • send()方法是异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。
  • ack是判别请求是否为完整的条件(就是是判断是不是成功发送了)。我们指定了“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。
  • retries,如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。
  • producer(生产者)缓存每个分区未发送的消息。缓存的大小是通过 batch.size 配置指定的。值较大的话将会产生更大的批。并需要更多的内存(因为每个“活跃”的分区都有1个缓冲区)。

默认缓冲可立即发送,即便缓冲空间还没有满,但是,如果你想减少请求的数量,可以设置linger.ms大于0。这将指示生产者发送请求之前等待一段时间,希望更多的消息填补到未满的批中。这类似于TCP的算法,例如上面的代码段,可能100条消息在一个请求发送,因为我们设置了linger(逗留)时间为1毫秒,然后,如果我们没有填满缓冲区,这个设置将增加1毫秒的延迟请求以等待更多的消息。需要注意的是,在高负载下,相近的时间一般也会组成批,即使是 linger.ms=0。在不处于高负载的情况下,如果设置比0大,以少量的延迟代价换取更少的,更有效的请求。

  • buffer.memory 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。

  • key.serializer和value.serializer示例,将用户提供的key和value对象ProducerRecord转换成字节,你可以使用附带的ByteArraySerializaer或StringSerializer处理简单的string或byte类型。

幂等和事务

KafkaProducer又支持两种模式:幂等生产者和事务生产者。幂等生产者加强了Kafka的交付语义,从至少一次交付到精确一次交付。特别是生产者的重试将不再引入重复。事务性生产者允许应用程序原子地将消息发送到多个分区(和主题!)
要启用幂等(idempotence),必须将enable.idempotence配置设置为true。 如果设置,则retries(重试)配置将默认为Integer.MAX_VALUE,acks配置将默认为all。API没有变化,所以无需修改现有应用程序即可利用此功能。
此外,如果send(ProducerRecord)即使在无限次重试的情况下也会返回错误(例如消息在发送前在缓冲区中过期),那么建议关闭生产者,并检查最后产生的消息的内容,以确保它不重复。最后,生产者只能保证单个会话内发送的消息的幂等性。
要使用事务生产者和attendant API,必须设置transactional.id。如果设置了transactional.id,幂等性会和幂等所依赖的生产者配置一起自动启用。此外,应该对包含在事务中的topic进行耐久性配置。特别是,replication.factor应该至少是3,而且这些topic的min.insync.replicas应该设置为2。最后,为了实现从端到端的事务性保证,消费者也必须配置为只读取已提交的消息。
transactional.id的目的是实现单个生产者实例的多个会话之间的事务恢复。它通常是由分区、有状态的应用程序中的分片标识符派生的。因此,它对于在分区应用程序中运行的每个生产者实例来说应该是唯一的。
所有新的事务性API都是阻塞的,并且会在失败时抛出异常。下面的例子说明了新的API是如何使用的。它与上面的例子类似,只是所有100条消息都是一个事务的一部分。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());

producer.initTransactions();

try {
    producer.beginTransaction();
    for (int i = 0; i < 100; i++)
        producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), Integer.toString(i)));
    producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
    // We can't recover from these exceptions, so our only option is to close the producer and exit.
    producer.close();
} catch (KafkaException e) {
    // For all other exceptions, just abort the transaction and try again.
    producer.abortTransaction();
}
producer.close();

如示例所示,每个生产者只能有一个未完成的事务。在beginTransaction()和commitTransaction()调用之间发送的所有消息都将是单个事务的一部分。当指定transactional.id时,生产者发送的所有消息都必须是事务的一部分。

事务生产者使用异常来传递错误状态。特别是,不需要为producer.send()指定回调,也不需要在返回的Future上调用.get():如果任何producer.send()或事务性调用在事务过程中遇到不可恢复的错误,就会抛出KafkaException。

消费者Java客户端

kafka客户端从kafka集群中获取消息,并透明地处理kafka集群中出现故障broker,透明地调节适应集群中变化的数据分区,也和broker交互,负载平衡消费者
public class kafkaConsumer<K,V>
extends Object
implements Consumer<K,V>
消费者维护着与broker的TCP连接来获取消息,如果在使用后没有关闭消费者,则会泄漏这些连接,消费者不是线程安全的

offset(偏移量)和消费者位置

kafka为分区中的每条消息保存一个偏移量(offset),这个偏移量是该分区中一条消息的唯一标示。也表示消费者在分区的位置。例如,一个位置是5的消费者(说明已经消费了0到4的消息),下一个将接收消息的偏移量为5的消息。实际上这有两个与消费者相关的 “位置” 概念:

消费者的位置给出了下一条消息的偏移量。它比消费者在该分区中看到的最大偏移量要大一个。它在每次消费者在调用poll(Duration)中接收消息时自动增长。

已提交的位置是已安全保存的最后偏移量,如果进程失败或重新启动时,消费者将恢复到这个偏移量。消费者可以选择定期自动提交偏移量,也可以选择通过调用commit API来手动的控制(如:commitSync 和 commitAsync)。

这个主要区别是消费者来控制一条消息什么时候才被认为是已被消费的,控制权在消费者

消费者组和主题订阅

Kafka的消费者组概念,通过 进程池 瓜分消息并处理消息。这些进程可以在同一台机器运行,也可分布到多台机器上,以增加可扩展性和容错性,相同group.id的消费者将视为同一个消费者组

组中的每个消费者都通过subscribe API动态的订阅一个topic列表。kafka将已订阅topic的消息发送到每个消费者组中。并通过平衡分区在消费者分组中所有成员之间来达到平均。因此每个分区恰好地分配1个消费者(一个消费者组中)。所有如果一个topic有4个分区,并且一个消费者分组有只有2个消费者。那么每个消费者将消费2个分区。

消费者组的成员是动态维护的:如果一个消费者故障。分配给它的分区将重新分配给同一个分组中其他的消费者。同样的,如果一个新的消费者加入到分组,将从现有消费者中移一个给它。这被称为重新平衡分组,并在下面更详细地讨论。当新分区添加到订阅的topic时,或者当创建与订阅的正则表达式匹配的新topic时,也将重新平衡。将通过定时刷新自动发现新的分区,并将其分配给分组的成员。

从概念上讲,你可以将消费者分组看作是由多个进程组成的单一逻辑订阅者。作为一个多订阅系统,Kafka支持对于给定topic任何数量的消费者组,而不重复。

这是在消息系统中常见的功能的略微概括。所有进程都将是单个消费者分组的一部分(类似传统消息传递系统中的队列的语义),因此消息传递就像队列一样,在组中平衡。与传统的消息系统不同的是,虽然,你可以有多个这样的组。但每个进程都有自己的消费者组(类似于传统消息系统中pub-sub的语义),因此每个进程都会订阅到该主题的所有消息。

此外,当分组重新分配自动发生时,可以通过ConsumerRebalanceListener通知消费者,这允许他们完成必要的应用程序级逻辑,例如状态清除,手动偏移提交等

它也允许消费者通过使用assign(Collection)手动分配指定分区,如果使用手动指定分配分区,那么动态分区分配和协调消费者组将失效。

发现消费者故障

订阅一组topic后,当调用poll(long)时,消费者将自动加入到组中。只要持续的调用poll,消费者将一直保持可用,并继续从分配的分区中接收消息。此外,消费者向服务器定时发送心跳。 如果消费者崩溃或无法在session.timeout.ms配置的时间内发送心跳,则消费者将被视为死亡,并且其分区将被重新分配。

还有一种可能,消费可能遇到“活锁”的情况,它持续的发送心跳,但是没有处理。为了预防消费者在这种情况下一直持有分区,我们使用max.poll.interval.ms活跃检测机制。 在此基础上,如果你调用的poll的频率大于最大间隔,则客户端将主动地离开组,以便其他消费者接管该分区。 发生这种情况时,你会看到offset提交失败(调用commitSync()引发的CommitFailedException)。这是一种安全机制,保障只有活动成员能够提交offset。所以要留在组中,你必须持续调用poll。

消费者提供两个配置设置来控制poll循环

  1. max.poll.interval.ms:增大poll的间隔,可以为消费者提供更多的时间去处理返回的消息(调用poll(long)返回的消息,通常返回的消息都是一批),缺点是此值越大将会延迟组重新平衡
  2. max.poll.records:此设置限制每次调用poll返回的消息数,这样可以更容易的预测每次poll间隔要处理的最大值。通过调整此值,可以减少poll间隔,减少重新平衡分组的

对于消息处理时间不可预测地的情况,这些选项是不够的。 处理这种情况的推荐方法是将消息处理移到另一个线程中,让消费者继续调用poll。 但是必须注意确保已提交的offset不超过实际位置。另外,你必须禁用自动提交,并只有在线程完成处理后才为记录手动提交偏移量(取决于你)。 还要注意,你需要pause暂停分区,不会从poll接收到新消息,让线程处理完之前返回的消息(如果你的处理能力比拉取消息的慢,那创建新线程将导致你机器内存溢出)。

自动提交偏移量(Automatic Offset Committing)

简单的kafka消费者API

Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "test");
props.setProperty("enable.auto.commit", "true");
props.setProperty("auto.commit.interval.ms", "1000");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("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(Duration.ofMillis(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控制自动提交的频率

集群是通过配置bootstrap.servers指定一个或多个broker。不用指定全部的broker,它将自动发现集群中的其余的borker(最好指定多个)

这个例子中,客户端订阅了主题foo和bar,消费者组叫test
broker通过心跳机器自动检测test组中失败的进程,消费者会自动ping集群,告诉进群它还活着。只要消费者能够做到这一点,它就被认为是活着的,并保留分配给它分区的权利,如果它停止心跳的时间超过session.timeout.ms,那么就会认为是故障的,它的分区将被分配到别的进程。
这个deserializer设置如何把byte转成object类型,例子中,通过指定string解析器,告诉获取到的消息的key和value只是简单的string类型

手动控制偏移量

不需要定时的提交offset,可以自己控制offset,当消息认为已消费过了,这个时候再去提交它们的偏移量。这个很有用的,当消费的消息结合了一些处理逻辑,这个消息就不应该认为是已经消费的,直到它完成了整个处理

Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "test");
props.setProperty("enable.auto.commit", "false");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("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(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        buffer.add(record);
    }
    if (buffer.size() >= minBatchSize) {
        insertIntoDb(buffer);
        consumer.commitSync();
        buffer.clear();
    }
}

我们将消费一批消息并将它们存储在内存中。当我们积累足够多的消息后,我们再将它们批量插入到数据库中。如果我们设置offset自动提交(之前说的例子),消费将被认为是已消费的。这样会出现问题,我们的进程可能在批处理记录之后,但在它们被插入到数据库之前失败了。

我们将在相应的记录插入数据库之后再手动提交偏移量。这样我们可以准确控制消息是成功消费的。提出一个相反的可能性:在插入数据库之后,但是在提交之前,这个过程可能会失败(即使这可能只是几毫秒,这是一种可能性)。在这种情况下,进程将获取到已提交的偏移量,并会重复插入的最后一批数据。这种方式就是所谓的“至少一次”保证,在故障情况下,可以重复。

无法执行这些操作,可能会使已提交的偏移超过消耗的位置,从而导致缺少记录。 使用手动偏移控制的优点是,您可以直接控制记录何时被视为“已消耗”

注意:使用自动提交也可以“至少一次”。但是要求你必须下次调用poll(Duration)之前或关闭消费者之前,处理完所有返回的数据。如果操作失败,这将会导致已提交的offset超过消费的位置,从而导致丢失消息。使用手动控制offset的有点是,你可以直接控制消息何时提交。

try {
    while(running) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(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();
}

注意:已提交的offset应始终是你的程序将读取的下一条消息的offset。因此,调用commitSync(offsets)时,你应该加1个到最后处理的消息的offset。

订阅指定的分区(Manual Partition Assignment)

在前面的例子中,我们订阅我们感兴趣的topic,让kafka提供给我们平分后的topic分区。但是,在有些情况下,你可能需要自己来控制分配指定分区,例如:

如果这个消费者进程与该分区保存了某种本地状态(如本地磁盘的键值存储),则它应该只能获取这个分区的消息。

如果消费者进程本身具有高可用性,并且如果它失败,会自动重新启动(可能使用集群管理框架如YARN,Mesos,或者AWS设施,或作为一个流处理框架的一部分)。 在这种情况下,不需要Kafka检测故障,重新分配分区,因为消费者进程将在另一台机器上重新启动。

要使用此模式,,你只需调用assign(Collection)消费指定的分区即可:

String topic = "foo";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0, partition1));

一旦手动分配分区,你可以在循环中调用poll(跟前面的例子一样)。消费者分组仍需要提交offset,只是现在分区的设置只能通过调用assign修改,因为手动分配不会进行分组协调,因此消费者故障不会引发分区重新平衡。每一个消费者是独立工作的(即使和其他的消费者共享GroupId)。为了避免offset提交冲突,通常你需要确认每一个consumer实例的gorupId都是唯一的。

注意,手动分配分区(即,assgin)和动态分区分配的订阅topic模式(即,subcribe)不能混合使用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值