Kafka包括以下核心API:
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>3.0.0version>
dependency>
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-streamsartifactId>
<version>3.0.0version>
dependency>
KafkaProducer API 的核心部分是KafkaProducer
类,使用KafkaProducer
类发送数据。KafkaProducer是线程安全的,可以在多个线程之间共享生产者实例。
KafkaProducer类提供了send()
方法将消息异步发送到主题。在消息发送过程中,涉及两个线程:main
线程和Sender
线程,以及一个线程共享变量RecordAccumulator
。main线程将消息发送给RecordAccmulator,Sender线程不断地从RecordAccumulator中拉取消息发送给Kafka broker。
Kafka Producer API涉及到的类如下:
KafkaProducer
类:创建一个生产者对象,用来发送数据构造方法 | 说明 |
---|---|
KafkaProducer(Map |
通过提供一组键值对作为配置来实例化 |
KafkaProducer(Map |
通过提供一组键值对配置、键和值序列化器来实例化 |
KafkaProducer(java.util.Properties properties) | 通过提供Properties对象来实例化 |
KafkaProducer(Properties properties, Serializer keySerializer, Serializer valueSerializer) | 通过提供Properties对象、键和值序列化器来实例化 |
KafkaProducer实例方法如下:
send()
方法是异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。可以从返回的Future
对象中稍后获取发送的结果,ProducerRecord
、RecordMetadata
包含了返回的结果信息。
Future 是异步计算结果的容器接口,它提供了在等待异步计算完成时检查计算是否完成的状态,并在异步计算完成后获取计算结果而且只能通过
get
方法获取结果,如果异步计算没有完成则阻塞,当然你可以在异步计算完成前通过 cancel 方法取消,如果异步计算被取消则标记一个取消的状态。如果希望异步计算可以被取消而且不提供可用的计算结果,如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future> 形式类型、并返回 null 作为底层任务的结果。
如果需要使用同步发送,可以在每次发送之后使用get
方法,因为producer.send
方法返回一个Future类型的结果,Future的get()
方法会一直阻塞直到该线程的任务得到返回值,也就是broker返回发送成功。
kafkaTemplate.send("testJson", message).get();
metrics()
获取producer的实时监控指标数据,比如发送消息的速率。
ProducerConfig
类:获取所需要的一系列配置参数成员变量 | 说明 |
---|---|
BOOTSTRAP_SERVERS_CONFIG | kafka 服务端的主机名和端口号 |
ACKS_CONFIG | 确保生产者可靠性设置。 acks=0:不等待成功返回; acks=1:等Leader写成功返回; acks=all:等Leader和所有ISR中的Follower写成功返回 all也可以用-1代替(all性能最低但最可靠) |
RETRIES_CONFIG | 消息发送失败尝试重发次数 |
BATCH_SIZE_CONFIG | 每个RecordBatch可以缓存的最大字节数。生产者发送多个消息到broker上的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式来提交消息,可以通过这个参数来控制批量提交的字节数大小,默认大小是16384byte,也就是16kb,意味着当一批消息大小达到指定的batch.size的时候会统一发送 |
LINGER_MS_CONFIG | 一般情况下,记录会被立即发送出去,而不会等待缓存的填充。用户可以通过配置linger.ms来让producer等待一段时间再发送消息,以此在一个批次中聚合更多的Message请求 |
KEY_SERIALIZER_CLASS_CONFIG | key 序列化 |
VALUE_SERIALIZER_CLASS_CONFIG | value 序列化 |
RECEIVE_BUFFER_CONFIG | 接收缓冲区内存大小 |
SEND_BUFFER_CONFIG | 发送缓冲区内存大小 |
BUFFER_MEMORY_CONFIG | 所有RecordBatch的总共最大字节数,表示缓存的大小。当缓存空间耗尽,后续的消息send就会阻塞。阻塞时间超过max.block.ms设定的时间,就会抛出TimeoutException |
这里的ACk机制,不是生产者得到ACK返回信息才开始发送,ACK保证的是生产者不丢失数据,而是只要有消息数据,就向broker发送。
ProducerRecord
类
每条数据都要封装成一个ProducerRecord对象。ProducerRecord对象携带者topic,partition,message等信息,在Serializer中被序列化。ProducerRecord消息按照分配好的Partition发送到具体的broker中,broker接收保存消息,更新Metadata信息。
方法摘要如下:
方法名 | 说明 |
---|---|
key() | 消息的键 |
partition() | 消息要发送的分区 |
topic() | 消息要发送的主题 |
value() | 消息的值 |
headers() | 消息头 |
timestamp() | 时间戳 |
RecordMetadata
类
这个类用于保存服务器已确认的消息的元数据,它的实例方法如下:
异步向主题发送数据
@Test
public void testSend() throws ExecutionException, InterruptedException {
// 初始化一个生产者
KafkaProducer producer = new KafkaProducer(props());
// 生产数据
for (int i = 0; i < 10; i++) {
// ProducerRecord 中还可以设置 topic partition 时间戳 header 等信息
ProducerRecord record = new ProducerRecord("test", Integer.toString(i), "helllo kraft-" + i);
//方法是异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。
Future future = producer.send(record);
System.out.println("从future对象中获取时间戳:"+future.get().timestamp());
System.out.println("从future对象中获取topic:"+future.get().topic());
System.out.println("从future对象中获取分区:"+future.get().partition());
System.out.println("从future对象中获取偏移量:"+future.get().offset());
System.out.println("从future对象中获取value大小:"+future.get().serializedValueSize());
}
// 关闭生产者
producer.close();
}
异步发送数据并在确认后提供回调
@Test
public void callbackSend() throws ExecutionException, InterruptedException {
KafkaProducer producer = new KafkaProducer(props());
// 源码中设置的是:user-supplied callback to execute when the record has been acknowledged by the server
// 设置记录被确认的回调函数
producer.send(new ProducerRecord("test", "hello kafka"), (metadata, exception) -> {
if (metadata != null) {
// 获取分区和偏移量
System.out.println("分区:" + metadata.partition() + "---偏移量:" + metadata.offset());
}
});
producer.close();
}
向指定分区发送消息
分区机制
若消息有指定key,默认的分区器会根据key的哈希值来选择分区(将key的hash值与topic的partition数进行取余得到partition值),如果没有指定key就以轮询的方式选择分区。
自定义分区策略
创建一个类,实现org.apache.kafka.clients.producer.Partitioner接口
- 主要分区逻辑在Partitioner.partition中实现:通过topic key value 一同确定分区
在构造KafkaProducer得Properties中设置partitioner.class 为自定义类
@Test
public void customerPartitionProducer (){
Properties props = props();
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,CustomPartition.class);
KafkaProducer producer = new KafkaProducer(props);
producer.send(new ProducerRecord("test", "hello Partiton1"), (metadata, exception) -> {
if (metadata != null) {
// 获取分区和偏移量
System.out.println("分区:"+metadata.partition() + "---偏移量:" + metadata.offset());
}
});
producer.close();
}
同步发送消息
@Test
public void sycnSend(){
KafkaProducer producer = new KafkaProducer(props());
for (int i = 0; i < 10; i++) {
// ProducerRecord 中还可以设置 topic partition 时间戳 header 等信息
ProducerRecord record = new ProducerRecord("test", Integer.toString(i), "helllo kraft-" + i);
//同步发送 无限等待返回
try {
producer.send(record).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
producer.close();
}
自定义重试机制,对不同异常的处理
如果需要自定义重试机制,就要在回调里对不同异常区别对待,常见的异常有几种如下:
(1)可重试异常
- LeaderNotAvailableException:分区的leader不可用,可能由换届选举导致,重试几次就可恢复
- NotControllerException:Controller主要负责统一管理分区信息等,可能是选举导致
- NetWorkerException:瞬时网络故障导致
(2)不可重试异常
- SerializationException:序列化失败异常
- RecordToolLargeException:消息尺寸过大导致
示例代码如下:
public void exceptionHandle(){
KafkaProducer producer = new KafkaProducer<String, String>(props());
ProducerRecord myRecord = new ProducerRecord("test", "helllo kraft");
producer.send(myRecord,
new Callback() {
public void onCompletion(RecordMetadata metadata, Exception e) {
if(e ==null){
//正常处理逻辑
System.out.println("The offset of the record we just sent is: " + metadata.offset());
}else{
if(e instanceof RetriableException) {
//处理可重试异常
} else {
//处理不可重试异常
}
}
}
});
}
kafka客户端通过TCP长连接从集群中消费消息,并透明地处理kafka集群中出现故障服务器,透明地调节适应集群中变化的数据分区。也和服务器交互,平衡均衡消费者。
一旦consumer和kakfa集群建立连接,consumer会以心跳的方式来告诉集群自己还活着,如果session.timeout.ms
内心跳未到达服务器,服务器认为心跳丢失,会做rebalence。
消费方式
consumer采用pull
(拉)模式从broker中读取数据。push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer的消费速度赶不上broker的发送速度,从而会导致consumer拒绝服务以及网络拥塞。而pull模式则可以根据consumer自身的消费能力以适当的消费速率消费消息。
pull模式不足之处:kafka无消息时,消费者陷入循环,一直返回空数据。针对这一点,kafka的消费者在消费数据时会传入一个时长参数timeout
,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长就是timeout。
offset和消费者的位置
kafka为分区中的每条消息保存一个偏移量(offset),这个偏移量是该分区中一条消息的唯一标示符。也表示消费者在分区的位置。例如,一个位置是5的消费者(说明已经消费了0到4的消息),下一个接收消息的偏移量为5的消息。实际上有两个与消费者相关的“位置”概念:
消费者的位置给出了下一条记录的偏移量。它比消费者在该分区中看到的最大偏移量要大一个。 它在每次消费者在调用poll(long)
中接收消息时自动增长。
“已提交”的位置是已安全保存的最后偏移量,如果consumer 在消费过程中可能会出现断电宕机等故障,恢复之后消费者需要从故障前的位置继续消费。消费者可以选择定期自动提交偏移量,也可以选择通过调用commit API来手动的控制(如:commitSync
和 commitAsync
)。
这个区别是消费者来控制一条消息什么时候才被认为是已被消费的,控制权在消费者。
Consumer API 主要包含如下几个类:
常用方法说明如下:
方法名 | 说明 |
---|---|
assign(Collection partitions) | 手动将分区列表分配给此消费者。 |
assignment() | 获取当前分配给此消费者者的分区列表。 |
close() | 关闭消费者,无限期地等待任何需要的清理 |
commitAsync() | 提交上次poll()时为所有订阅的主题和分区列表返回的偏移量。 |
commitSync() | 同步阻塞提交上次 poll 的一批数据最高的偏移量 |
subscribe(Collection topics) | 订阅主题 |
listTopics() | 获取有关用户有权查看的所有主题的分区的元数据。 |
partitionsFor(String topic) | 获取给定主题的分区元数据。 |
pause(Collection partitions) | 暂停从请求的分区提取。 |
poll(Duration timeout) | 从broker中拉取数据,如果一直没有数据,最长等待timeout比如,poll(Duration.ofMillis(5000)) 表示如果一直没有数据则5秒后返回一次再拉取,否则立即返回再拉取 |
position(TopicPartition partition) | 获取给定分区的上次提交偏移量 |
resume(Collection partitions) | 恢复已暂停消费的指定分区 |
seek(TopicPartition partition, long offset) | 从分区的指定offset开始消费 |
unsubscribe() | 取消订阅 |
wakeup() | 将 Sender 线程从poll方法的阻塞中唤醒,并抛出 WakeupException异常 不需要处理 WakeupException,因为它只是用于跳出循环的一种方式 |
ConsumerConfig
类:获取所需的一系列配置参数,大部分配置和ProducerConfig类中的差不多,其他常用配置如下。
配置 | 说明 |
---|---|
GROUP_ID_CONFIG | 此消费者所属消费者组的唯一标识。如果消费者用于订阅或offset管理策略的组管理功能,则此属性是必须的。 |
ENABLE_AUTO_COMMIT_CONFIG | 消费者的offset将在后台自动周期性的提交(默认true) |
AUTO_COMMIT_INTERVAL_MS_CONFIG | 消费者偏移量自动提交给Kafka的频率(以毫秒为单位) |
HEARTBEAT_INTERVAL_MS_CONFIG | 心跳用于确保消费者的会话保持活动状态,并当有新消费者加入或离开组时方便重新平衡。该值必须必比session.timeout.ms小(默认3000) |
MAX_POLL_RECORDS_CONFIG | 在单次调用poll() 中返回的最大记录数(默认500) |
MAX_POLL_INTERVAL_MS_CONFIG | 使用消费者组管理时poll() 调用的最大延迟。消费者在获取更多记录之前可以空闲的时间量的上限。如果此超时时间期满之前poll() 没有调用,则消费者被视为失败,并且分组将重新平衡,以便将分区重新分配给别的成员。 |
AUTO_OFFSET_RESET_CONFIG | 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,则根据此项配置进行消费。默认latest,还可以设置为earliest和none。 |
SESSION_TIMEOUT_MS_CONFIG | 超过这个时间没有检测到心跳,broker将分组中移除消费者,触发rebalance |
ConsuemrRecord
类:每条数据都要封装成一个 ConsumerRecord 对象
ConsumerRecord用于从 Kafka 服务接收消息,由topic、partition(从中接收记录)和指向 Kafka 分区中记录的offset组成。它具有以下签名。
public ConsumerRecord(string topic,int partition, long offset,K key, V value)
实例:
自动提交偏移量
//自动提交偏移量,实际应用中不推荐
@Test
public void testConsumer(){
//创建consumer对象
KafkaConsumer consumer = new KafkaConsumer<>(consumerProps());
//消费者订阅的topic, 可同时订阅多个,指定topic名字
consumer.subscribe(Arrays.asList("test"));
while (true){
//每间隔一定时间去拉取消息
ConsumerRecords records = consumer.poll(Duration.ofMillis(5000));
for (ConsumerRecord record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
System.out.println("处理了一批数据!");
}
}
这里说明一下
auto.commit.interval.ms
以及何时提交消费者偏移量:设置
props.put("auto.commit.interval.ms","60000");
自动提交时间为一分钟,也就是你在这一分钟内拉取任何数量的消息都不会被提交消费的当前偏移量,如果你此时关闭消费者(一分钟内),下次消费还是从和第一次的消费数据一样,即使你在一分钟内消费完所有的消息,只要你在一分钟内关闭程序,导致提交不了offset,就可以一直重复消费数据。
虽然自动提交 offset 十分简介便利,但由于其是基于时间提交的,开发人员难以把握offset 提交的时机。因此 Kafka 还提供了手动提交 offset 的 API。
手动提交偏移量
不需要定时提交偏移量,可以自己控制offset,当消息已经被我们消费过后,再去手动提交他们的偏移量。这个很适合我们的一些处理逻辑。
手动提交offset的方法有两种:分别是commitSync
(同步提交) 和commitAsync
(异步提交)。两种方法都会将本次poll
的一批数据最高的偏移量提交;不同点是commitSync
会失败重试,一直到提交成功(如果有不可恢复的原因导致,也会提交失败),才去拉取新数据。而commitAsync
则没有重试机制(提交了就去拉取新数据,不管这次的提交有没有成功),故有可能提交失败。
//手动提交偏移量
@Test
public void manualCommit(){
Properties props = consumerProps();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");//不允许自动提交偏移量
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("test"));
while (true){
//每间隔一定时间去拉取消息
try {
ConsumerRecords<String,String> records = consumer.poll(Duration.ofMillis(5000));
records.forEach(record -> {
//消费的消息
System.out.printf("patition=%d, offset=%d, key=%s, value=%s%n",
record.partition(),record.offset(),record.key(),record.value());
});
System.out.println("提交偏移量!");
//手动控制offset异步提交
consumer.commitAsync();
//异步 回调
// consumer.commitAsync(((map, e) -> {
// map.forEach((topicPartition, offsetAndMetadata) -> {
// System.out.println("现在提交了偏移量————"+offsetAndMetadata);
// });
// }));
//同步提交
// consumer.commitSync();
} catch (Exception e) {
logger.error("consumer offset error",e);
}
}
}
虽然同步提交offset更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交offset的方式。
这些都是提交所有分区的偏移量,如果我们想更细致的控制偏移量提交,还可以自定义提交偏移量:
//手动提交偏移量,更细致的控制偏移量提交(以消费的分区为单位进行提交)
@Test
public void manualCommit2(){
Properties props = consumerProps();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");//关闭自动提交偏移量
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("test"));
while (true){
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(5000));
//遍历拉取到记录的分区
records.partitions().forEach(topicPartition -> {
//获取该分区的记录
List<ConsumerRecord<String, String>> partitionRecords = records.records(topicPartition);
//遍历该分区的记录
partitionRecords.forEach(record -> System.out.printf("消息的偏移量 = %d, 消息的key = %s, 消息的value = %s%n", record.offset(), record.key(), record.value()));
//获取该分区消费到的偏移量
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
//手动异步提交偏移量,带回调
consumer.commitAsync(Collections.singletonMap(topicPartition, new OffsetAndMetadata(lastOffset + 1)), (map, e) -> {
map.forEach((key,value) -> System.out.println("提交的分区:"+key.partition()+",提交的偏移量:"+value.offset()));
});
});
}
}
订阅指定分区
Kafka会通过分区分配分给消费者一个分区,但是也可以手动指定分区消费消息,要使用指定分区,只需要调用assign
方法消费指定的分区即可:
//订阅指定分区
@Test
public void consumePartition(){
Properties props = consumerProps();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
KafkaConsumer consumer = new KafkaConsumer<>(props);
// 可以指定多个不同topic的分区或者相同topic的分区 我这里只指定一个分区
TopicPartition topicPartition = new TopicPartition("test", 0);
// 调用指定分区用assign,消费topic使用subscribe
consumer.assign(Arrays.asList(topicPartition));
while (true){
ConsumerRecords records = consumer.poll(Duration.ofMillis(5000));
//遍历拉取到记录的分区
records.partitions().forEach(partition -> {
//获取该分区的记录
List> partitionRecords = records.records(partition);
//遍历该分区的记录
partitionRecords.forEach(record -> System.out.printf("消息的偏移量 = %d, 消息的key = %s, 消息的value = %s%n", record.offset(), record.key(), record.value()));
//获取该分区上一次消费到的偏移量
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
//手动异步提交偏移量,带回调
consumer.commitAsync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)), (map, e) -> {
map.forEach((key,value) -> System.out.println("提交的分区:"+key.partition()+",提交的偏移量:"+value.offset()));
});
});
}
}
一旦手动分配分区,可以在循环中调用poll。消费者分区任然需要提交offset,只是现在分区的设置只能通过调用assign 修改,因为手动分配不会进行分组协调,因此消费者故障或者消费者的数量变动都不会引起分区重新平衡。每一个消费者是独立工作的(即使和其他的消费者共享GroupId)。为了避免offset提交冲突,通常需要确认每一个consumer实例的groupId都是唯一的。
注意:
手动分配分区(assgin)和动态分区分配的订阅topic模式(subcribe)不能混合使用。
控制消费的位置
大多数情况下,消费者只是简单的从头到尾的消费消息,周期性的提交位置(自动或手动)。kafka也支持消费者去手动的控制消费的位置,可以消费之前的消息也可以跳过最近的消息。
有几种情况,手动控制消费者的位置可能是有用的:
kafka使用seek(TopicPartition, long)
指定新的消费位置。用于查找服务器保留的最早和最新的offset的特殊的方法也可用seekToBeginning(Collection)
和 seekToEnd(Collection)
。
/**
* 指定消费位置
*/
@Test
public void consumeOffset(){
Properties props = consumerProps();
props.put("enable.auto.commit","false");//自动提交offset
props.put("auto.offset.reset", "earliest");
KafkaConsumer consumer = new KafkaConsumer<>(props);
// 指定topic和分区
TopicPartition topicPartition = new TopicPartition("test", 0);
// assgin分区参数
consumer.assign(Arrays.asList(topicPartition));
// seek指定分区的偏移量
consumer.seek(topicPartition,5);
while (true){
ConsumerRecords records = consumer.poll(Duration.ofMillis(5000));
for (ConsumerRecord record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
//同步提交
consumer.commitSync();
System.out.println("处理了一批数据!");
}
}
要想指定分区并指定偏移量,必须同时使用assgin
和seek
,自定提交偏移量和手动提交都是可以的。
Admin API允许管理和检测Topic、broker以及其他Kafka实例,与Kafka自带的脚本命令作用类似。
构建AdminClient
操做AdminClient API的前提是须要建立一个AdminClient
实例,代码示例:
//构建AdminClient
public AdminClient adminClient(){
Properties properties = new Properties();
//配置Kafka服务的访问地址及端口
properties.setProperty(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.8.128:9092");
//建立AdminClient实例
return AdminClient.create(properties);
}
其中AdminClientConfig
配置类详细说明如下:
NAME | DESCRIPTION |
---|---|
BOOTSTRAP_SERVERS_CONFIG | host/port,用于和kafka集群建立初始化连接。此列表不需要包含完整的集群地址(但尽量多配置几个,以防止配置的服务器宕机)。 |
CLIENT_ID_CONFIG | 在发出请求时传递给服务器的id字符串。这样做的目的是通过允许在服务器端请求日志记录中包含逻辑应用程序名称来跟踪请求源的ip/port。 |
RECEIVE_BUFFER_CONFIG | 读取数据时使用的TCP接收缓冲区(SO_RCVBUF)的大小。如果值为-1,则将使用OS默认值。 |
REQUEST_TIMEOUT_MS_CONFIG | 配置控制客户端等待请求响应的最长时间。如果在超时之前未收到响应,客户端将在必要时重新发送请求,如果重试耗尽,则该请求将失败。 |
SEND_BUFFER_CONFIG | 发送数据时时使用TCP发送缓冲区(SO_SNDBUF)的大小。如果值为-1,则使用OS默认值。 |
METADATA_MAX_AGE_CONFIG | 强制更新元数据的时间段(以毫秒为单位),即使没有任何分区leader发生变化,主动发现任何新的broker或分区。 |
METRIC_REPORTER_CLASSES_CONFIG | 用作指标记录的类的列表。实现MetricReporter接口,以允许插入将被通知新的度量创建的类。JmxReporter始终包含在注册JMX统计信息中。 |
METRICS_NUM_SAMPLES_CONFIG | 用于计算度量维护的样例数。 |
METRICS_SAMPLE_WINDOW_MS_CONFIG | 时间窗口计算度量标准。 |
RECONNECT_BACKOFF_MAX_MS_CONFIG | 重新连接到重复无法连接的broker程序时等待的最大时间(毫秒)。如果提供,每个主机的回退将会连续增加,直到达到最大值。 计算后退增加后,增加20%的随机抖动以避免连接风暴。 |
RECONNECT_BACKOFF_MS_CONFIG | 尝试重新连接到给定主机之前等待的基本时间量。这避免了在频繁的重复连接主机。此配置适用于client对broker的所有连接尝试。 |
RETRIES_CONFIG | 在失败之前重试调用的最大次数 |
RETRY_BACKOFF_MS_CONFIG | 尝试重试失败的请求之前等待的时间。这样可以避免在某些故障情况下以频繁的重复发送请求。 |
建立AdminClient
实例对象之后,通过AdminClient提供的方法操作Kafka,常用方法如下表所示:
方法名 | 说明 |
---|---|
createTopics |
创建一个或多个Topic |
listTopics |
查询Topic列表 |
deleteTopics |
删除一个或多个Topic |
describeTopics |
查询Topic的描述信息 |
describeConfigs |
查询Topic、Broker等的全部配置信息 |
incrementalAlterConfigs |
修改Topic、Broker等的配置项信息 |
createPartitions |
用于调整Topic的Partition数量,只能增长不能减小或删除 也就是说新设置的Partition数量必须大于等于以前的Partition数量 |
close |
关闭AdminClient并释放所有关联的资源 |
Tips:
describeTopics
和describeConfigs
的意义主要是在监控上,这两个API能够获取到Topic自身和周边的详细信息
创建Topic实例
使用createTopics
方法能够建立Topic,传入的参数也与kafka-topics.sh
命令脚本的参数同样。如果创建的Topic已存在,会抛出TopicExistsException
异常。
//创建Topic实例
@Test
public void createTopic(){
AdminClient adminClient = adminClient();
//副本因子
Short re = 1;
NewTopic newTopic = new NewTopic("test-admin",1,re);//Topic名称,分区数,副本因子
CreateTopicsResult createTopicsResult = adminClient.createTopics(Arrays.asList(newTopic));
System.out.println("CreateTopicsResult : " + createTopicsResult);
adminClient.close();
}
NewTopic
类用于创建一个Topic,CreateTopicsResult
存储创建Topic返回的结果,创建完成之后可以从返回的这个类对象中获取关于topic的相关信息,其中CreateTopicsResult的实例方法如下。
查看Topic列表
listTopics()
方法有两种重载形式,无参的listTopics()方法不会列出内置topic信息,还有一种形式接收一个ListTopicsOptions
类型的参数。
KafkaAdminClient中基本所有的应用类方法都有一个类似XXXOptions类型的参数,这个类型一般只包含timeoutMs这个成员变量,用来设定请求的超时时间,如果没有指定则使用默认的request.timeout.ms参数值,即30000ms。
不过ListTopicsOptions
扩展了一个成员变量listInternal
,用来指明是否需要罗列内部Topic。
/**
* 获取topic列表
*/
@Test()
public void topicList() throws ExecutionException, InterruptedException {
AdminClient adminClient = adminClient();
//是否查看Internal选项
ListTopicsOptions options = new ListTopicsOptions();
options.listInternal(true);
//ListTopicsResult listTopicsResult = adminClient.listTopics();
ListTopicsResult listTopicsResult = adminClient.listTopics(options);
Set names = listTopicsResult.names().get();
//打印names
names.stream().forEach(System.out::println);
Collection topicListings = listTopicsResult.listings().get();
//打印TopicListing
topicListings.stream().forEach((topicList) -> {
System.out.println(topicList.toString());
});
adminClient.close();
}
与
ListTopicsOptions
对应,KafkaAdminClient
中基本所有的应用类方法都有一个类似XXXResult类型的返回值,其内部一般包含一个KafkaFuture
,用于异步发送请求之后等待操作结果。KafkaFuture
实现了Java中的Future
接口,用来支持链式调用以及其他异步编程模型。
ListTopicsResult
类用于存储罗列topic操作的结果,其实例方法如下:
删除topic
通过deleteTopics
方法删除一个或多个主题。
//删除Topic
@Test
public void deleteTopic() throws ExecutionException, InterruptedException {
AdminClient adminClient = adminClient();
DeleteTopicsResult deleteTopicsResult = adminClient.deleteTopics(Arrays.asList("test-admin"));
deleteTopicsResult.all().get();
}
描述topic
一个Topic会有自身的描述信息,例如:partition
的数量,副本集的数量,是否为internal
等等。AdminClient
中提供了describeTopics
方法来查询这些描述信息,返回DescribeTopicsResult
类型的对象,通过调用DescribeTopicsResult的all()
或者values()
方法可以获取相关Topic的描述信息。
/**
* 描述Topic
* name: yibo_topic
* desc: (name=test-admin,
* internal=false,
* partitions=
* (partition=0,
* leader=192.168.8.128:9092 (id: 0 rack: null),
* replicas=192.168.8.128:9092 (id: 0 rack: null),
* isr=192.168.8.128:9092 (id: 0 rack: null)),
* authorizedOperations=null)
* @throws ExecutionException
* @throws InterruptedException
*/
@Test
public void descTopic() throws ExecutionException, InterruptedException {
AdminClient adminClient = adminClient();
DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Arrays.asList("test-admin"));
Map descriptionMap = describeTopicsResult.all().get();
descriptionMap.forEach((key,value) -> {
System.out.println("name: " + key+" desc: " + value);
});
}
查询Topic配置信息
除了Kafka自身的配置项外,其内部的Topic也会有很是多的配置项,可以通过describeConfigs
方法来获取某个Topic中的配置项信息。代码示例:
/**
* 查询配置信息
* @throws ExecutionException
* @throws InterruptedException
*/
@Test
public void descConfig() throws ExecutionException, InterruptedException {
AdminClient adminClient = adminClient();
//TODO 这里做一个预留,集群时会讲到
//ConfigResource configResource = new ConfigResource(ConfigResource.Type.BROKER,TOPIC_NAME);
ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC,"test-admin");
DescribeConfigsResult describeConfigsResult = adminClient.describeConfigs(Arrays.asList(configResource));
Map resourceConfigMap = describeConfigsResult.all().get();
resourceConfigMap.forEach((key,value) -> {
System.out.println(key + " " + value);
});
}
修改Topic配置信息
AdminClient
还提供了相关方法来修改Topic配置项的值。在早期版本中,使用alterConfigs
方法来修改配置项,在新版本中则是使用incrementalAlterConfigs
方法来修改Topic的配置项,该方法使用起来相对于alterConfigs
要略微复杂一些,但所以功能更多、更灵活。代码示例:
//修改配置信息
@Test
public void alterConfig() throws Exception {
AdminClient adminClient = adminClient();
Map> configMap = new HashMap<>();
// 指定ConfigResource的类型及名称
ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC,"test-admin");
// 配置项一样以ConfigEntry形式存在,只不过增长了操做类型
// 以及可以支持操做多个配置项,相对来讲功能更多、更灵活
AlterConfigOp alterConfigOp = new AlterConfigOp(new ConfigEntry("preallocate","false"),AlterConfigOp.OpType.SET);
configMap.put(configResource,Arrays.asList(alterConfigOp));
AlterConfigsResult alterConfigsResult = adminClient.incrementalAlterConfigs(configMap);
alterConfigsResult.all().get();
}
//增加Partition数
public void addPartition() throws ExecutionException, InterruptedException {
AdminClient adminClient = adminClient();
Map partitionsMap = new HashMap<>();
NewPartitions newPartitions = NewPartitions.increaseTo(3);
//将主题test-admin的分区数修改为3
partitionsMap.put("test-admin",newPartitions);
adminClient.createPartitions(partitionsMap);
}
Kafka Stream是Apache Kafka从0.10版本引入的一个新Feature,它提供了对存储于Kafka内的数据进行流式处理和分析的功能。简而言之,Kafka Stream就是一个用来作流计算的类库,与Storm、Spark Streaming、Flink的做用相似,但要轻量得多。
Kafka Stream关键词:
Kafka Stream API重要的抽象:
KStream
:数据流抽象。创建方法如下:通过KstreamBuilder类的stream()
方法创建一个Kstream对象
StreamsBuilder builder = new StreamsBuilder();
KStream<String, Long> wordCounts = builder.stream(
"input-topic", // 输入的topic
Consumed.with(Serdes.String(), Serdes.String()) //key和value的序列化方式
);
无状态操作:只需要数据流过一遍就可以,不依赖前后的状态。
branch
:将一个Kstream分成多个
KStream<String, Long>[] branches = stream.branch(
(key, value) -> key.startsWith("A"), //branches[0]中只包含key以“A”开头的所有记录
(key, value) -> key.startsWith("B"), //branches[1]中只包含key以“B”开头的所有记录
(key, value) -> true //branches[2]中包含其他记录
);
filter
:过滤操作
// 过滤掉value不大于0的记录
KStream<String, Long> onlyPositives = stream.filter((key, value) -> value > 0);
filterNot
:反向过滤,与filter相反
flatMap
:将一条记录转换成0条、1条或多条记录
// 把一条记录转换成了两条记录。如: (345L, "Hello") -> ("HELLO", 1000), ("hello", 9000)
KStream<String, Integer> transformed = stream.flatMap((key, value) -> {
List<KeyValue<String, Integer>> result = new LinkedList<>();
result.add(KeyValue.pair(value.toUpperCase(), 1000));
result.add(KeyValue.pair(value.toLowerCase(), 9000));
return result;
});
flatMapValues
:作用和flatMap相同,但是只是对value操作,转换后记录的key同原来的key
// 通过空格拆分成单个单词
KStream<byte[], String> words = sentences.flatMapValues(value -> Arrays.asList(value.split("\\s+")));
foreach
:循环
// 循环打印出每条记录
stream.foreach((key, value) -> System.out.println(key + " => " + value));
groupByKey
:根据key分组
KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
GroupBy
: 分组
// 分组,并修改了key和value的类型
KGroupedStream<String, String> groupedStream = stream.groupBy(
(key, value) -> value, Serialized.with(Serdes.String(), Serdes.String())
);
map
:将一条记录转换成另一条记录
KStream<String, Integer> transformed
= stream.map(key, value) -> KeyValue.pair(value.toLowerCase(), value.length()));
mapValues
:作用同map,但是只是对value操作,转换后记录的key同原来的key
KStream<byte[], String> uppercased = stream.mapValues(value -> value.toUpperCase());
merge
:合并两个流
KStream<byte[], String> merged = stream1.merge(stream2);
peek
:对每条记录执行无状态操作,并返回未更改的流,也就是说peek中的任何操作,返回的都是以前的流,可以用来调试
KStream<byte[], String> unmodifiedStream
= stream.peek((key, value) -> System.out.println("key=" + key + ", value=" + value));
print
:打印流,可以用来调试
stream.print();
SelectKey
:重新构建key
//将key值改为value的第一个单词
KStream<String, String> rekeyed = stream.selectKey((key, value) -> value.split(" ")[0])
toStream
:将KTable转换成KStream
KStream<byte[], String> stream = table.toStream();
count:滚动聚合,按分组键统计记录数。
// Counting a KGroupedStream
KTable<String, Long> aggregatedStream = groupedStream.count();
对于KGroupedStream,会忽略具有空键或空值的记录。
Reduce:滚动聚合,通过分组键组合(非窗口)记录的值。当前记录值与最后一个减少的值组合,并返回一个新的减少值。与聚合不同,结果值类型不能更改。
KGroupedStream<String, Long> groupedStream = ...;
KGroupedTable<String, Long> groupedTable = ...;
KTable<String, Long> aggregatedStream = groupedStream.reduce(
(aggValue, newValue) -> aggValue + newValue );
KTable<String, Long> aggregatedTable = groupedTable.reduce(
(aggValue, newValue) -> aggValue + newValue,
(aggValue, oldValue) -> aggValue - oldValue );
Kafka Stream客户端库配置(注意,窗口可拖动)。
NAME | DESCRIPTION | TYPE | DEFAULT | VALID VALUES | IMPORTANCE |
---|---|---|---|---|---|
application.id | 流处理应用程序标识。必须在Kafka集群中是独一无二的。 1)默认客户端ID前缀,2)成员资格管理的group-id,3)changgelog的topic前缀 | string | high | ||
bootstrap.servers | 用于建立与Kafka集群的初始连接的主机/端口列表。 | list | high | ||
replication.factor | 流处理程序创建更改日志topic和重新分配topic的副本数 | int | 1 | high | |
state.dir | 状态存储的目录地址。 | string | /tmp/kafka-streams | high | |
cache.max.bytes.buffering | 用于缓冲所有线程的最大内存字节数 | long | 10485760 | [0,…] | low |
client.id | 发出请求时传递给服务器的id字符串。 这样做的目的是通过允许将逻辑应用程序名称包含在服务器端请求日志记录中,来追踪请求源的ip/port。 | string | “” | high | |
default.key.serde | 用于实现Serde接口的key的默认序列化器/解串器类。 | class | org.apache.kafka.common.serialization.Serdes$ByteArraySerde | medium | |
default.timestamp.extractor | 实现TimestampExtractor接口的默认时间戳提取器类。 | class | org.apache.kafka.streams.processor.FailOnInvalidTimestamp | medium | |
default.value.serde | 用于实现Serde接口的值的默认serializer / deserializer类。 | class | org.apache.kafka.common.serialization.Serdes$ByteArraySerde | medium | |
num.standby.replicas | 每个任务的备用副本数。 | int | 0 | low | |
num.stream.threads | 执行流处理的线程数。 | int | 1 | low | |
processing.guarantee | 应使用的加工保证。可能的值为at_least_once(默认)和exact_once。 | string | at_least_once | [at_least_once, exactly_once] | medium |
security.protocol | 用于与broker沟通的协议。 有效值为:PLAINTEXT,SSL,SASL_PLAINTEXT,SASL_SSL。 | string | PLAINTEXT | medium | |
application.server | host:port指向用户嵌入定义的末端,可用于发现单个KafkaStreams应用程序中状态存储的位置 | string | “” | low | |
buffered.records.per.partition | 每个分区缓存的最大记录数。 | int | 1000 | low | |
commit.interval.ms | 用于保存process位置的频率。 注意,如果’processing.guarantee’设置为’exact_once’,默认值为100,否则默认值为30000。 | long | 30000 | low | |
connections.max.idle.ms | 关闭闲置的连接时间(以毫秒为单位)。 | long | 540000 | medium | |
key.serde | 用于实现Serde接口的key的Serializer/deserializer类.此配置已被弃用,请改用default.key.serde | class | null | low | |
metadata.max.age.ms | 即使我们没有看到任何分区leader发生变化,主动发现新的broker或分区,强制更新元数据时间(以毫秒为单位)。 | long | 300000 | [0,…] | low |
metric.reporters | metric reporter的类列表。实现MetricReporter接口,JmxReporter始终包含在注册JMX统计信息中。 | list | “” | low | |
metrics.num.samples | 保持的样本数以计算度量。 | int | 2 | [1,…] | low |
metrics.recording.level | 日志级别。 | string | INFO | [INFO, DEBUG] | low |
metrics.sample.window.ms | 时间窗口计算度量标准。 | long | 30000 | [0,…] | low |
partition.grouper | 实现PartitionGrouper接口的Partition grouper类。 | class | org.apache .kafka.streams .processor .DefaultPartitionGrouper | medium | |
poll.ms | 阻塞输入等待的时间(以毫秒为单位)。 | long | 100 | low | |
receive.buffer.bytes | 读取数据时使用的TCP接收缓冲区(SO_RCVBUF)的大小。 如果值为-1,则将使用OS默认值。 | int | 32768 | [0,…] | medium |
reconnect.backoff.max.ms | 因故障无法重新连接broker,重新连接的等待的最大时间(毫秒)。如果提供,每个主机会连续增加,直到达到最大值。随机递增20%的随机抖动以避免连接风暴。 | long | 1000 | [0,…] | low |
reconnect.backoff.ms | 尝试重新连接之前等待的时间。避免在高频繁的重复连接服务器。 这种backoff适用于消费者向broker发送的所有请求。 | long | 50 | [0,…] | low |
request.timeout.ms | 控制客户端等待请求响应的最长时间。如果在配置时间内未收到响应,客户端将在需要时重新发送请求,如果重试耗尽,则请求失败。 | int | 40000 | [0,…] | low |
retry.backoff.ms | 尝试重试失败请求之前等待的时间。以避免了在某些故障情况下,在频繁重复发送请求。 | long | 100 | [0,…] | low |
rocksdb.config.setter | 一个Rocks DB配置setter类,或实现RocksDBConfigSetter接口的类名 | null | low | ||
send.buffer.bytes | 发送数据时要使用的TCP发送缓冲区(SO_SNDBUF)的大小。 如果值为-1,则将使用OS默认值。 | int | 131072 | [0,…] | low |
state.cleanup.delay.ms | 在分区迁移删除状态之前等待的时间(毫秒)。 | long | 60000 | low | |
timestamp.extractor | 实现TimestampExtractor接口的Timestamp抽取器类。此配置已弃用,请改用default.timestamp.extractor | class | null | low | |
windowstore.changelog.additional.retention.ms | 添加到Windows维护管理器以确保数据不会从日志中过早删除。默认为1天 | long | 86400000 | low |
应用实例
创建一个具有以下功能的Kafka流应用:
1.过滤以ABC开头的数据
2.将所有值转换为小写
public class MyFirstKafkaStream {
public static void main(String[] args) {
System.out.println("Start");
Properties config = new Properties();
config.put(StreamsConfig.APPLICATION_ID_CONFIG,"my-application");
config.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.8.128:9092");
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
config.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
config.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG,Serdes.String().getClass());
StreamsBuilder builder = new StreamsBuilder();//创建StreamsBuilder实例对象,用于创建Kafka流处理拓扑
KStream<String, String> inputKStream = builder.stream("input-kafka-topic");//从Kafka主题创建一个流处理
KStream<String, String> outputKStream = //根据一个KStream主题创建另一个KStream(因为无法修改流中的消息-消息是不可变的)
inputKStream.filterNot(((key, value) -> value.substring(0, 3).equals("ABC"))) //过滤开头为ABC的数据
.mapValues((ValueMapper<String, String>) String::toLowerCase); //转换为小写
outputKStream.to("output-kafka-topic", Produced.with(Serdes.String(),Serdes.String())); //把结果放到另一个topic中
KafkaStreams streams = new KafkaStreams(builder.build(), config); //实例化一个KafkaStream对象
streams.start(); //启动流
}
}
启动一个生产者,在主题input-kafka-topic中输入如下数据:
启动一个消费者,消费主题output-kafka-topic的数据:
Kafka客户端和Spring Kafka的版本对应关系如下:
引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.kafkagroupId>
<artifactId>spring-kafkaartifactId>
<version>2.7.7version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
一个简单的生产者实例
@Component
public class SimpleProducer {
@Resource
@Qualifier("kafkaTemplate")
private KafkaTemplate<String, MessageEntity> kafkaTemplate;
public void send(String topic,MessageEntity message){kafkaTemplate.send(topic,message);}
public void send(String topic,String key,MessageEntity entity){
ProducerRecord<String,MessageEntity> record = new ProducerRecord<>(
topic,
key,
entity
);
long startTime = Instant.now().toEpochMilli();
System.out.println("调用"+kafkaTemplate);
ListenableFuture<SendResult<String, MessageEntity>> future = kafkaTemplate.send(record);
future.addCallback(new ProducerCallback(startTime,key,entity));
}
}
一个简单的消费者实例
@Component
public class SimpleConsumer {
private static Logger log = LoggerFactory.getLogger( SimpleConsumer.class );
private final Gson gson = new Gson();
//在下面的代码中,负责消费消息的关键之处就是SpringKafka提供的@KafkaListener注解,
//在方法上使用该注解,并指定要消费的topic(也可以指定消费组以及分区号,支持正则表达式匹配),
//这样,消费者一旦启动,就会监听kafka服务器上的topic,实时进行消费消息。
//当然,我们可以在该类中定义多个不同的方法,并都在方法上使用 @KafkaListener ,为它指定不同的topic及分区信息,这样每个方法就相当于一个消费者了。
@KafkaListener(topics = "spring-kafka", containerFactory = "kafkaListenerContainerFactory")
public void receive(MessageEntity message){
log.info("消费信息:"+gson.toJson(message));
}
}
创建Topic:
在 Spring for Apache Kafka 框架中,AdminClient
有几种方式支持。有一个KafkaAdmin
类,它是AdminClient
, 它实现了一个 Spring FactoryBean
,用于维护和支持AdminClient
。然后是TopicBuilder
类,它提供了一个方便的 API 来创建主题配置。
@Bean
public KafkaAdmin admin() {
return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.8.128:9092"));
}
可以使用TopicBuilder
类实例化一个主题对象,并提供一些标准配置,例如分区数、副本数、压缩等。
public NewTopic topic1() {
return TopicBuilder.name("topic1")
.partitions(10)
.replicas(3)
.compact()
.build();
}
public NewTopic topic2() {
return TopicBuilder.name("topic2") //如果主题已经存在,则进行更新分区(如果它不存在,就会创建一个新主题)
.partitions(12)
.replicas(3)
.config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd") //指定压缩算法
.build();
}
此外,TopicBuilder
还提供了在创建主题时手动为主题分配副本的方法:
public NewTopic topic3() {
return TopicBuilder.name("thing3")
.assignReplicas(0, Arrays.asList(0, 1))
.assignReplicas(1, Arrays.asList(1, 2))
.assignReplicas(2, Arrays.asList(2, 0))
.config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd")
.build();
}
通过kafkaAdmin的createOrModifyTopics()
方法创建或修改主题:
KafkaAdmin kafkaAdmin = new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.8.128:9092"));
kafkaAdmin.createOrModifyTopics(topic1());
响应式Kafka是基于Kafka Consumer/Producer API的。Reactor Kafka API允许将消息发布到Kafka,并使用具有非阻塞背压和极低开销的API从Kafka使用。这使得使用响应式的应用程序能够将Kafka用作消息总线或流式平台,并与其他系统集成以提供端到端的反应管道。
Reactor Kafka是Kafka的一个功能性Java API。对于以功能性风格编写的应用程序,此API使Kafka交互能够轻松集成,而无需将非功能性异步生产或消费API合并到应用程序逻辑中。
<dependency>
<groupId>io.projectreactor.kafkagroupId>
<artifactId>reactor-kafkaartifactId>
<version>1.3.7version>
dependency>
KafkaSender
与KafkaProduce
关联,用于将消息传输到Kafka。
KafkSender
是使用SenderOptions
的实例创建的。在创建Kafkander实例之前,还可以配置响应式KafkSender
的其他配置选项,如maxInFlight
。Map<String, Object> producerProps = new HashMap<>();
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
SenderOptions<Integer, String> senderOptions =
SenderOptions.<Integer, String>create(producerProps) //指定基础KafkaProducer的属性
.maxInFlight(1024); //响应式Kafka配置选项
senderOptions
中已配置的选项创建新的KafkaSender
实例。KafkaSender sender = KafkaSender.create(senderOptions);
现在KafkaSender已经做好准备向Kafka发送消息,底层的KafkaProducer实例将在第一条消息即将发送的时候创建,此时只是创建了一个KafkaSender实例,但还没有和Kafka建立连接。
创建要发送给Kafka的消息,每条消息都应被封装成一个SenderRecord
对象中,SenderRecord
是对ProducerRecord
的进一步封装,带有一些额外的元数据,这些相关元数据不会发送到Kafka,但会在发送操作完成或失败时包含在为记录生成的SendResult
中。
当记录被发送到多个分区时,响应会按顺序到达每个分区,但来自不同分区的响应可能会交错。
Flux> outboundFlux =
Flux.range(1, 10)
.map(i -> SenderRecord.create(topic, partition, timestamp, i, "Message_" + i, i));
sender.send(outboundFlux) //发送
.doOnError(e-> log.error("Send failed", e)) //发送失败
.doOnNext(r -> System.out.printf("Message #%d send response: %s\n", r.correlationMetadata(), r.recordMetadata())) //打印Kafka返回的元数据和correlationMetadata()中的消息索引
.subscribe(); //订阅以触发从outboundFlux到Kafka的实际记录流。
在使用Reactive KafkSender
过程中还有以下需要注意的地方:
异常处理
在发送消息过程中,如果消息在配置的重试次数后发送失败,可以通过stopOnError()
设置如何处理:
public SenderOptions stopOnError(boolean stopOnError);
如果是false
,则每个发送记录都会返回一个成功或错误的响应,对于错误响应,将在SenderResult
指示发送失败的原因。如果为true
,则会为第一次失败的发送返回一个响应,流会立即终止并抛出异常。
不带元数据发送
如果每个发送请求不需要单独的结果,则可以使用KafkaOutbound
接口将ProducerRecord
发送到Kafka,而无需提供相关元数据。
KafkaOutbound send(Publisher extends ProducerRecord> outboundRecords);
sender.createOutbound()
.send(Flux.range(1, 10) //消息没有被包装成SenderRecord
.map(i -> new ProducerRecord(topic, i, "Message_" + i)))
.then() //通过调用then方法使流被订阅
.doOnError(e -> e.printStackTrace())
.doOnSuccess(s -> System.out.println("Sends succeeded"))
.subscribe(); //订阅实际发送的请求
可以使用KafkaOutbound
将多个发送连在一起。当订阅从then()
返回的Mono
时,将按照声明顺序依次调用发送。如果任何发送在配置的重试次数后失败,则序列将被取消。
sender.createOutbound()
.send(flux1) //按序发送flux1、flux2、flux3
.send(flux2)
.send(flux3)
.then()
.doOnError(e -> e.printStackTrace())
.doOnSuccess(s -> System.out.println("Sends succeeded")) //成功表示成功发送整个链中的所有记录
.subscribe(); //订阅以启动链中的发送序列
直接使用KafkaProducer
应用程序有时可能需要访问KafkaProducer
以执行KafkaSender
接口没有的操作。例如,需要知道主题中的分区数,以便选择要向其发送记录的分区。通过doOnProducer()
,使用KafkaSender
未直接提供的操作。
sender.doOnProducer(producer -> producer.partitionsFor(topic))
.doOnSuccess(partitions -> System.out.println("Partitions " + partitions))
.subscribe();
通过reactor.kafka.receiver.KafkaReceiver
使用存储在Kafka主题中的消息。KafkaReceiver
的每个实例都与KafkaConsumer
的单个实例相关联。KafkaReceiver
不是线程安全的,因为多个线程不能同时访问底层KafkanConsumer
。
Map consumerProps = new HashMap<>();
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "sample-group");
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
ReceiverOptions receiverOptions =
ReceiverOptions.create(consumerProps) //配置KafkaConsumer的配置
.subscription(Collections.singleton(topic)); //要订阅的主题
一旦在options实例上配置了所需的配置项,就可以使用这些ReceiverOptions
创建一个新的KafkaReceiver
实例来消费消息。
Flux> inboundFlux =
KafkaReceiver.create(receiverOptions)
.receive();
inboundFlux.subscribe(r -> {
System.out.printf("Received message: %s\n", r); //打印来自Kafka的每条消息
r.receiverOffset().acknowledge(); //确认已对记录进行处理,以便提交偏移量
});
指定消费分区:
receiverOptions = receiverOptions.assignment(Collections.singleton(new TopicPartition(topic, 0)));
//从topic的第一个分区开始消费
应用程序可以通过在消息被使用时确认消息并定期调用commit()
来提交已确认的偏移量来批量提交。
receiver.receive()
.doOnNext(r -> {
r.receiverOffset().commit();
});
在下面的步骤中,我们将部署一个3节点kafka集群并创建一个测试主题,并使用kafka生产者将数据生成到测试主题中,还将使用kafka消费者使用kafka主题中的数据。(用一台服务器的不同端口来模拟3个节点)
1.创建kafka集群配置
在3.0版本中我们采用Kraft模式搭建集群,基于zookeeper的搭建方式也类似,配置文件按照前面单机配置的方式修改就行(config/server.properties
)。
首先把config/kraft/server.properties
文件复制成3个新文件,分别命名为server1.properties,server2.properties和server3.properties,因为我们将创建一个3节点集群。
cd config/kraft
cp server.properties server1.properties
cp server.properties server2.properties
cp server.properties server3.properties
修改server1.properties以下属性,其他不要改动:
#节点Id,server2设为2,server3设为3
node.id=1
#所有可用的kafka控制器。我们将有3个kraft控制器节点分别在端口19091、19092和19091上运行
[email protected]:19091,[email protected]:19092,[email protected]:19093
#broker将使用端口9091,而kraft控制器将使用端口19091。其他2个节点分别用9092、19092和9093、19093端口
listeners=PLAINTEXT://192.168.8.128:9091,CONTROLLER://192.168.8.128:19091
#server2设为9092,server3设为9093
advertised.listeners=PLAINTEXT://192.168.8.128:9091
#日志目录,其他节点将server1改成对应节点名称
log.dirs=/opt/kafka_cluster/kraft-logs/server1/kraft-combined-logs
同样修改server2.properties、server3.properties上述属性。
2.创建kafka群集id和日志目录
在启动集群之前,需要先使用bin/kafka-storage.sh
脚本创建kafka集群id。执行下列命令,并记下运行生成的uuid:
bin/kafka-storage.sh random-uuid
#输出
sUDk0ZaaRJ6khfDbRfj-Sg
接下来格式化所有服务的存储目录,这时需要传入刚刚创建的集群id,务必保持三台服务的集群id相同:
#server1
bin/kafka-storage.sh format -t sUDk0ZaaRJ6khfDbRfj-Sg -c config/kraft/server1.properties
#server2
bin/kafka-storage.sh format -t sUDk0ZaaRJ6khfDbRfj-Sg -c config/kraft/server2.properties
#server3
bin/kafka-storage.sh format -t sUDk0ZaaRJ6khfDbRfj-Sg -c config/kraft/server3.properties
如上图所示,在kraft-cluster目录下多出一个kraft-logs目录,这就是我们在配置文件中指定的日志存放路径。
3.启动kafka服务器
接下来分别启动三台服务器:
#server1
./bin/kafka-server-start.sh -daemon ./config/kraft/server1.properties
#server2
./bin/kafka-server-start.sh -daemon ./config/kraft/server2.properties
#server3
./bin/kafka-server-start.sh -daemon ./config/kraft/server3.properties
使用jps命令查看,可以看到已经启动了三个Kafka服务:
使用以下命令通过broker1创建hello-kraft主题,指定分区数为3,副本数也为3
./bin/kafka-topics.sh --create --topic hello-kraft --partitions 3 --replication-factor 3 --bootstrap-server 192.168.8.128:9091
查看该topic详情,可以看到三个分区在不同的broker上。
5.生产和消费kafka数据
使用如下命令开启一个生产者:
bin/kafka-console-producer.sh --bootstrap-server 192.168.8.128:9091 --topic hello-kraft
在另一个终端中,使用以下命令启动消费者,注意,监听的集群另一个节点:
bin/kafka-console-consumer.sh --bootstrap-server 192.168.8.128:9092 --topic hello-kraft