kafka学习之(三)生产者消费者实例(API的应用)
kafka理论知识:http://www.jasongj.com/2015/03/10/KafkaColumn1/
我们将通过代码演示如何发布、订阅消息。
1、添加maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.2.0</version>
</dependency>
2,kafka发布消息
新的生产者是线程安全的,在线程之间共享单个生产者实例,通常单例比多个实例要快。
import java.util.Properties; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; /**** * 创建topic * */ public class KafkaProducerTest { public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); //ack是判别请求是否为完整的条件(就是是判断是不是成功发送了),指定了“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。 props.put("acks", "all"); //retries,如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。 props.put("retries", 0); /** * producer(生产者)缓存每个分区未发送的消息。缓存的大小是通过 batch.size 配置指定的。 * 值较大的话将会产生更大的批。并需要更多的内存(因为每个“活跃”的分区都有1个缓冲区)。 */ props.put("batch.size", 16384); props.put("linger.ms", 1); /*** * buffer.memory 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。 * 当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。 */ props.put("buffer.memory", 33554432); /**** * key.serializer和value.serializer示例,将用户提供的key和value对象ProducerRecord转换成字节, * 你可以使用附带的ByteArraySerializaer或StringSerializer处理简单的string或byte类型。 */ 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<String,String>(props); for(int i = 0; i < 100; i++) //send()方法是异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。 producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i))); //生产者的缓冲空间池保留尚未发送到服务器的消息,后台I/O线程负责将这些消息转换成请求发送到集群。如果使用后不关闭生产者,则会丢失这些消息。 producer.close(); } }
KafkaProducer支持两种模式:
幂等生产者和事务生产者。幂等生产者加强了Kafka的交付语义,从至少一次交付到精确一次交付。特别是生产者的重试将不再引入重复。事务性生产者允许应用程序原子地将消息发送到多个分区(和主题!)。
要启用幂等(idempotence),必须将enable.idempotence配置设置为true。 如果设置,则retries(重试)配置将默认为Integer.MAX_VALUE,acks配置将默认为all。
如果send(ProducerRecord)即使在无限次重试的情况下也会返回错误(例如消息在发送前在缓冲区中过期),那么建议关闭生产者,并检查最后产生的消息的内容,以确保它不重复。最后,生产者只能保证单个会话内发送的消息的幂等性。
要使用事务生产者和attendant API,必须设置transactional.id。如果设置了transactional.id,幂等性会和幂等所依赖的生产者配置一起自动启用。此外,应该对包含在事务中的topic进行耐久性配置。特别是,replication.factor应该至少是3,而且这些topic的min.insync.replicas应该设置为2。最后,为了实现从端到端的事务性保证,消费者也必须配置为只读取已提交的消息。
所有新的事务性API都是阻塞的,并且会在失败时抛出异常。下面的例子说明了新的API是如何使用的。它与上面的例子类似,只是所有100条消息都是一个事务的一部分。
send()
异步发送一条消息到topic,并调用callback(当发送已确认)。
send是异步的,并且一旦消息被保存在等待发送的消息缓存中,此方法就立即返回。这样并行发送多条消息而不阻塞去等待每一条消息的响应。
发送的结果是一个RecordMetadata,它指定了消息发送的分区,分配的offset和消息的时间戳。如果topic使用的是CreateTime,则使用用户提供的时间戳或发送的时间(如果用户没有指定指定消息的时间戳)如果topic使用的是LogAppendTime,则追加消息时,时间戳是broker的本地时间。
由于send调用是异步的,它将为分配消息的此消息的RecordMetadata返回一个Future。如果future调用get(),则将阻塞,直到相关请求完成并返回该消息的metadata,或抛出发送异常。
如果要模拟一个简单的阻塞调用,你可以调用get()方法。
byte[] key = "key".getBytes(); byte[] value = "value".getBytes(); ProducerRecord<byte[],byte[]> record = new ProducerRecord<byte[],byte[]>("my-topic", key, value) producer.send(record).get();
完全无阻塞的话,可以利用回调参数提供的请求完成时将调用的回调通知。
ProducerRecord<byte[],byte[]> record = new ProducerRecord<byte[],byte[]>("the-topic", key, value); producer.send(myRecord, new Callback() { public void onCompletion(RecordMetadata metadata, Exception e) { if(e != null) e.printStackTrace(); System.out.println("The offset of the record we just sent is: " + metadata.offset()); } });
发送到同一个分区的消息回调保证按一定的顺序执行,也就是说,在下面的例子中 callback1 保证执行 callback2 之前:
producer.send(new ProducerRecord<byte[],byte[]>(topic, partition, key1, value1), callback1); producer.send(new ProducerRecord<byte[],byte[]>(topic, partition, key2, value2), callback2);
Parameters:
record - 发送的记录(消息)
callback - 用户提供的callback,服务器来调用这个callback来应答结果(null表示没有callback)。
Throws:
InterruptException - 如果线程在阻塞中断。
SerializationException - 如果key或value不是给定有效配置的serializers。
TimeoutException - 如果获取元数据或消息分配内存话费的时间超过max.block.ms。
KafkaException - Kafka有关的错误(不属于公共API的异常)。
3,kafka消费topic实现
1)offset(偏移量)和消费者位置
kafka为分区中的每条消息保存一个偏移量(offset),这个偏移量是该分区中一条消息的唯一标示。也表示消费者在分区的位置。
消费者的位置给出了下一条消息的偏移量。它比消费者在该分区中看到的最大偏移量要大一个。它在每次消费者在调用poll(Duration)中接收消息时自动增长。
已提交的位置是已安全保存的最后偏移量,如果进程失败或重新启动时,消费者将恢复到这个偏移量。消费者可以选择定期自动提交偏移量,也可以选择通过调用commit API来手动的控制(如:commitSync 和 commitAsync)。
这个主要区别是消费者来控制一条消息什么时候才被认为是已被消费的,控制权在消费者
2)消费者组和主题订阅
Kafka的消费者组概念,通过 进程池 瓜分消息并处理消息。这些进程可以在同一台机器运行,也可分布到多台机器上,以增加可扩展性和容错性,相同group.id的消费者将视为同一个消费者组。消费者组的成员是动态维护的:如果一个消费者故障。分配给它的分区将重新分配给同一个分组中其他的消费者。同样的,如果一个新的消费者加入到分组,将从现有消费者中移一个给它。这被称为
重新平衡分组.assign(Collection)手动分配指定分区,如果使用手动指定分配分区,那么动态分区分配和协调消费者组将失效。3)发现消费者故障
订阅一组topic后,当调用poll(long)时,消费者将自动加入到组中。只要持续的调用poll,消费者将一直保持可用,并继续从分配的分区中接收消息。此外,消费者向服务器定时发送心跳。 如果消费者崩溃或无法在session.timeout.ms配置的时间内发送心跳,则消费者将被视为死亡,并且其分区将被重新分配。
还有一种可能,消费可能遇到“活锁”的情况,它持续的发送心跳,但是没有处理。为了预防消费者在这种情况下一直持有分区,我们使用max.poll.interval.ms活跃检测机制。
在此基础上,如果你调用的poll的频率大于最大间隔,则客户端将主动地离开组,以便其他消费者接管该分区。 发生这种情况时,你会看到offset提交失败(调用commitSync()引发的CommitFailedException)。这是一种安全机制,保障只有活动成员能够提交offset。所以要留在组中,你必须持续调用poll。消费者提供两个配置设置来控制poll循环:
-
max.poll.interval.ms:增大poll的间隔,可以为消费者提供更多的时间去处理返回的消息(调用poll(long)返回的消息,通常返回的消息都是一批)。缺点是此值越大将会延迟组重新平衡。 -
max.poll.records:此设置限制每次调用poll返回的消息数,这样可以更容易的预测每次poll间隔要处理的最大值。通过调整此值,可以减少poll间隔,减少重新平衡分组的
pause暂停分区,不会从poll接收到新消息,让线程处理完之前返回的消息(如果你的处理能力比拉取消息的慢,那创建新线程将导致你机器内存溢出)。4)手动控制偏移量(Manual Offset Control)
不需要定时的提交offset,可以自己控制offset,当消息认为已消费过了,这个时候再去提交它们的偏移量。这个很有用的,当消费的消息结合了一些处理逻辑,这个消息就不应该认为是已经消费的,直到它完成了整个处理。import java.time.Duration; import java.util.Arrays; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; /*** * 自动提交偏移量(Automatic Offset Committing) * @author * */ public class AutoCustomerTest { public static void main(String[] args) { Properties props = new Properties(); //集群是通过配置bootstrap.servers指定一个或多个broker。 //不用指定全部的broker,它将自动发现集群中的其余的borker(最好指定多个,万一有服务器故障)。 props.setProperty("bootstrap.servers", "localhost:9092"); //消费者组叫test。 props.setProperty("group.id", "test"); props.setProperty("enable.auto.commit", "true"); //设置enable.auto.commit,偏移量由auto.commit.interval.ms控制自动提交的频率。 props.setProperty("auto.commit.interval.ms", "1000"); //这个deserializer设置如何把byte转成object类型,例子中,通过指定string解析器,我们告诉获取到的消息的key和value只是简单个string类型。 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); //订阅了主题foo和bar。 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()); } } }
5)订阅指定的分区(Manual Partition Assignment)
assign(Collection)消费指定的分区即可。package org.source.dsmh; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.TopicPartition; /*** * 将消费一批消息并将它们存储在内存中。当我们积累足够多的消息后,我们再将它们批量插入到数据库中 * @author * */ public class ManualCustomerTest { public static void main(String[] args) { 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); //消费指定的分区 String topic = "foo"; TopicPartition partition0 = new TopicPartition(topic, 0); TopicPartition partition1 = new TopicPartition(topic, 1); consumer.assign(Arrays.asList(partition0, partition1)); //consumer.subscribe(Arrays.asList("foo", "bar")); 不指定分区消费 /**** * 消费者分组仍需要提交offset,只是现在分区的设置只能通过调用assign修改, * 因为手动分配不会进行分组协调,因此消费者故障不会引发分区重新平衡。 * 每一个消费者是独立工作的(即使和其他的消费者共享GroupId)。 * 为了避免offset提交冲突,通常你需要确认每一个consumer实例的gorupId都是唯一的。 * 注意,手动分配分区(即,assgin)和动态分区分配的订阅topic模式(即,subcribe)不能混合使用。 */ final int minBatchSize = 200; List<ConsumerRecord<String, String>> buffer = new ArrayList<>(); //手动提交偏移量 /*** * 注意:使用自动提交也可以“至少一次”。但是要求你必须下次调用poll(Duration)之前或关闭消费者之前,处理完所有返回的数据。 * 如果操作失败,这将会导致已提交的offset超过消费的位置,从而导致丢失消息。使用手动控制offset的优点是可以直接控制消息何时提交。 while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { buffer.add(record); } if (buffer.size() >= minBatchSize) { //insert db //所有收到的消息为”已提交",在某些情况下,你可以希望更精细的控制,通过指定一个明确消息的偏移量为“已提交” consumer.commitSync(); buffer.clear(); } } */ try { while(true) { 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))); /*** * 注意:已提交的offset应始终是你的程序将读取的下一条消息的offset。 * 因此,调用commitSync(offsets)时,你应该加1个到最后处理的消息的offset。 */ } } } finally { consumer.close(); } } }
6)控制消费的位置
7)消费者流量控制
poll(long)中使用pause(Collection) 和 resume(Collection) 来暂停消费指定分配的分区,重新开始消费指定暂停的分区。8)读取事务性消息
isolation.level=read_committed来实现。9)多线程处理
Kafka消费者不是线程安全的。所有网络I/O都发生在进行调用应用程序的线程中。用户的责任是确保多线程访问正确同步的。非同步访问将导致ConcurrentModificationException。
wakeup(),它可以安全地从外部线程来中断活动操作。在这种情况下,将从操作的线程阻塞并抛出一个WakeupException。这可用于从其他线程来关闭消费者。 package org.source.dsmh; import java.time.Duration; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.errors.WakeupException; public class KafkaConsumerRunner implements Runnable { private final AtomicBoolean closed = new AtomicBoolean(false); private final KafkaConsumer consumer; public KafkaConsumerRunner(KafkaConsumer consumer) { this.consumer = consumer; } @Override public void run() { try { consumer.subscribe(Arrays.asList("my-topic")); while (!closed.get()) { ConsumerRecords records = consumer.poll(Duration.ofMillis(10000)); // Handle new records } } catch (WakeupException e) { // Ignore exception if closing if (!closed.get()) throw e; } finally { consumer.close(); } } // 在单独的线程中,可以通过设置关闭标志和唤醒消费者来关闭消费者。 public void shutdown() { closed.set(true); consumer.wakeup(); } }
使用Java管理kafka集群
package org.source.dsmh; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutionException; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.CreatePartitionsResult; import org.apache.kafka.clients.admin.CreateTopicsResult; import org.apache.kafka.clients.admin.ListTopicsResult; import org.apache.kafka.clients.admin.NewPartitions; import org.apache.kafka.clients.admin.NewTopic; public class KafkaCreateTopic { /*** * 创建 * @param bootstrapServers */ private static void createTopics(String bootstrapServers) { Properties properties = new Properties(); properties.put("bootstrap.servers", bootstrapServers); properties.put("connections.max.idle.ms", 10000); properties.put("request.timeout.ms", 5000); try (AdminClient client = AdminClient.create(properties)) { CreateTopicsResult result = client.createTopics(Arrays.asList( new NewTopic("my-topic1", 1, (short) 1), new NewTopic("my-topic2", 1, (short) 1), new NewTopic("my-topic3", 1, (short) 1) )); try { result.all().get(); } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException(e); } } } /*** * 查询 * @param bootstrapServers */ private static void listTopics(String bootstrapServers) { Properties properties = new Properties(); properties.put("bootstrap.servers", bootstrapServers); properties.put("connections.max.idle.ms", 10000); properties.put("request.timeout.ms", 5000); try (AdminClient client = AdminClient.create(properties)) { ListTopicsResult result = client.listTopics(); try { result.listings().get().forEach(topic -> { System.out.println(topic); }); } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException(e); } } } /*** * 为已存在的topic增加分区 * @param bootstrapServers */ private static void addPartitions(String bootstrapServers,String topic) { Properties properties = new Properties(); properties.put("bootstrap.servers", bootstrapServers); properties.put("connections.max.idle.ms", 10000); properties.put("request.timeout.ms", 5000); try (AdminClient client = AdminClient.create(properties)) { Map newPartitions = new HashMap<>(); // 增加到2个 newPartitions.put(topic, NewPartitions.increaseTo(2)); CreatePartitionsResult rs = client.createPartitions(newPartitions); try { rs.all().get(); } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException(e); } } } public static void main(String[] args) { String bootstrapServers="localhost:9092"; createTopics(bootstrapServers); addPartitions(bootstrapServers,"my-topic1"); listTopics(bootstrapServers); } }
api接口文档:
package com.kafka.consumer; import java.io.IOException; import java.util.Collections; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; public class KafkaPhotoConsumerClient { public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "192.168.1.34:9092"); //每个消费者分配独立的组号 props.put("group.id", "kafka-consumer-test"); //如果value合法,则自动提交偏移量 props.put("enable.auto.commit", "true"); //设置多久一次更新被消费消息的偏移量 props.put("auto.commit.interval.ms", "1000"); //设置会话响应的时间,超过这个时间kafka可以选择放弃消费或者消费下一条消息 props.put("session.timeout.ms", "30000"); 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<String, String>(props); consumer.subscribe(Collections.singletonList("ThirdAccess")); System.out.println("Subscribed to topic " + "ThirdAccess"); while (true) { ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) { // print the offset,key and value for the consumer records. System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), record.value()); } } } }
浙公网安备 33010602011771号