文章目录
- 1 Kafka集群
- 1.1 分区与备份
- 1.2 集群的消费问题
- 2 基础使用
- 2.1 生产者生产消息
- 2.1.1 Java客户端的生产者
- 2.1.2 生产者的同步&异步发送
- 2.1.3 生产者端ACK的相关配置
- 2.1.4 发送消息的缓冲区机制
- 2.2 消费者消费消息
- 2.2.1 Java客户端的消费者
- 2.2.2 offset的自动提交和手动提交
- 自动提交
- 手动提交
- 2.2.3 消费者poll消息的细节
- 2.2.4 消费者的健康状态检查
- 2.2.5 消费者指定分区偏移量和时间消费
- 2.2.6 新消费组消费offset的规则
1 Kafka集群
1.1 分区与备份
搭建三个节点的Kafka集群,创建1个Topic,两个Partition,三个副本
副本:是对Partition的备份,集群中不同的副本会被部署在不同的Broker上
在三个Broker的集群中创建名为my-replicated-topic的Topic,有两个分区,三个副本,Topic的详细信息:
集群中的三台机器分别为0,1,2 上述的信息可以看到
Topic的第一个分区Partition0的Leader是2号机器 这个分区的三个副本在2,0,1上
Topic的第二个分区Partition1的Leader是0号机器 这个分区的三个副本在0,1,2上上述的Isr代表可以同步的Broker节点和已同步的Broker节点集合,Leader宕机后从Isr集合中选取新Leader
如果某台Follower机器同步效率低,其中数据滞后于Leader且在Leader宕机后被选做新的Leader
这是不合理的,为此可以将其从Isr集合中摘除 不将它作为待上位成为Leader的Follower
Partition0有3个副本分别位于0,1,2三台机器上 且有Leader和Followers的关系
Leader负责读写并和Followers同步数据,一旦Leader所处的机器宕机,其他的Follower可以成为该分区的Leader
Producer生产的消息存放到Leader分区,Consumer从Leader分区消费消息
Follower只做备份或在Leader宕机后成为Leader,Leader不断向Follower同步数据以保证一致性
1.2 集群的消费问题
1,一个Consumer来消费
2,一个ConsumerGroup来消费(单播)
来看这样的一个场景:
集群中两个Broker,一个Topic,四个Partition,两个ConsumerGroup
这样的场景消费的规则是什么?
一个Partition只能被一个ConsumerGroup里的一个Consumer消费,从而保证消费顺序
Kafka只在Partition的范围内保消费信息的局部顺序性,不能在一个Topic中多个Partition中保证消费顺序性
一个Consumer是可以消费多个Partition的用这样一个例子来说明,下图并不完整只是举例
但牢记单播的原则,一个Partition只能被一个ConsumerGroup中的一个Consumer消费
生产者依次生产了1-9 九条消息被分发到不同的Partition中 不同ConsumerGroup中的Consumer来消费这些消息
Consumer1可以消费到五条消息,但是这五条消息不能保证有序,不能保证是1,2,4,7,8
但能保证来自Partition0的消息顺序是1,2,8 来自Partition2的消息顺序是4,7,这就是局部顺序性
一个Partition只能被一个ConsumerGroup中的一个Consumer消费就是为了保证局部顺序性
一旦Partition可以被一个ConsumerGroup中多个Consumer消费,假设Partition2可以被Consumer3和Consumer4消费
那么Partition2发来的消息4,7 可能把4发给了Consumer3,7发给了Consumer4 这里的数字消息并无顺序意义
此时就无法保证局部顺序性,不知道是7应该是先被消费还是4该先被消费,某些业务场景无法接受
再考虑这样一个问题假设Consumer3异常宕机,此时Partition0和Partition2在 ConsumerGroup2中此时没有消费者
此时根据Kafka集群的rebalance机制,会自动选择其他的Consumer来消费Partition0和Partition2
不违反单播/多播原则即可
2 基础使用
2.1 生产者生产消息
2.1.1 Java客户端的生产者
首先引入依赖:
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
Java代码实现一个生产者简单发送一条消息:
public class MyProducer {
// 发送的目的Topic
private final static String TOPIC_NAME = "my-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 配置生产者需要的KV
Properties properties = new Properties();
// 发送的目的集群 及集群中机器通信的端口
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9002,127.0.0.1:9003,127.0.0.1:9004");
// 把发送的消息key从字符串序列化为字符数组
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 把发送的消息value从字符串序列化为字符数组
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建Producer对象 即发送消息的客户端 使用之前的properties的配置
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
// ProducerRecord即被发送的消息记录 将消息包装到该对象中 由Producer来send
// 这里未指定发送分区 具体发到Topic的哪个分区 根据hash(key)%partitionCount计算决定
// 指明Topic和被发送消息的KV K在未指定分区时可以根据公式计算出发到哪个partition V就是具体消息内容
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>
(TOPIC_NAME, "hello", "helloKafka");
// 通过Producer发送消息 这里采用同步方式 返回的RecordMetadata是本次发送后该消息的元数据
RecordMetadata metadata = producer.send(producerRecord).get();
// 打印该消息被发向的Topic partition 和该消息的offset
System.out.println("同步方式发送消息结束-> " + "topic:" + metadata.topic() +
" partition:" + metadata.partition() + " offset:" + metadata.offset());
}
}
想要将消息发送到指定分区上只需更改:
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>
(TOPIC_NAME, 0, "hello", "helloKafka");
此时消息被发送到partition0上
2.1.2 生产者的同步&异步发送
同步发送:
生产者发送消息后没有收到ACK,就会导致生产者阻塞
阻塞X秒后如果还没有收到ACK,会重发消息,重发的次数最多为Y次
try {
RecordMetadata data = producer.send(producerRecord).get();
// 在收到ACK前 这里被阻塞
System.out.println("同步方式发送消息结束-> " + "topic:" + metadata.topic() +
" partition:" + metadata.partition() + " offset:" + metadata.offset());
} catch (InterruptedException e) {
// 发送失败 记录日志
e.printStackTrace();
// 设置间隔3s后 再次尝试同步发送 再失败人工介入
Thread.sleep(3000);
try {
RecordMetadata data = producer.send(producerRecord).get();
} catch (Exception e1) {
// 手动处理
}
} catch (ExecutionException e) {
e.printStackTrace();
}
异步发送:
异步发送时生产者发完后就可以执行之后的业务,
borker收到消息后异步调用生产者提供的callback()告知结果
// 创建一个计数器 发几条消息初始化几 这里只发1条
final CountDownLatch countDownLatch = new CountDownLatch(1);
producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e != null) {
System.err.println("发送消息失败: " + e.getStackTrace());
}
if(recordMetadata != null) {
System.out.println("同步方式发送消息结束-> " + "topic:" + metadata.topic() +
" partition:" + metadata.partition() + " offset:" + metadata.offset());
}
// 每成功1次 计数器-1
countDownLatch.countDown();
}
});
// 判断countDownLatch是不是0 不是则等待5s
countDownLatch.await(5, TimeUnit.SECONDS);
producer.close();
这两种方式,生产中更多的使用同步而非异步的方式
因为异步存在消息丢失的可能,异步虽然可以提升发送性能,但丢失消息是部分业务不能容忍的
2.1.3 生产者端ACK的相关配置
以同步方式发送消息时,每发送一条都要等待Broker回传相应的ACK,未收到前都会被阻塞
ACK可以配置三个参数:
ACK回传策略
-ack=0 该参数代表消息到达Broker后,就回传ACK 最容易丢失消息,效率最高
-ack=1 该参数代表当Leader收到消息后并把消息写入本地日志,再返回ACK 性能均衡
-ack=-1 该参数代表当Leader收到消息后并把消息同步到X-1台机器后,再返回ACK 最安全,但效率最低
里面的X通过 min.insync.replicas=X(X默认为1,推荐X设置>=2)
// 配置ACK回传策略 此种方式当Leader收到消息并将消息写入本地日志后 即可回传ACK
properties.put(ProducerConfig.ACKS_CONFIG, "1");
且当消息发送失败后(没有收到ACK),生产者会重发消息,重发的间隔时间和重发最大次数也可以配置:
// 生产者发送消息失败,重发消息的时间间隔
properties.put(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG, 300);
// 生产者发送失败后,重发的最大次数
properties.put(ProducerConfig.RETRIES_CONFIG, "3");
注意,重发机制可以保证消息的可靠性,但也可能由于网络延迟等因素,造成消息的重复发送
因此需要在接收方做好接收消息的幂等性处理
2.1.4 发送消息的缓冲区机制
当生产者生产好数据后,并不会立刻发送
Kafka会默认创建一个缓冲区,并开启一个本地线程每隔一段时间从缓冲区中取数据并发给对应Partition
// 设置发送消息的本地缓冲区 如果设置了缓冲区 消息先写到缓冲区中 缓冲区大小默认32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// 设置批量发送消息的大小,默认16K 即一个batch满了16K就发送
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// 10ms后batch中的数据无论满没满 都要将batch中的消息发送 不能导致太高时延
properties.put(ProducerConfig.LINGER_MS_CONFIG, 10);
2.2 消费者消费消息
2.2.1 Java客户端的消费者
Java代码实现一个消费者,该消费者订阅了1个Topic,每隔1s拉取一次消息
public class MyConsumer {
// 消费的目的Topic
private final static String TOPIC_NAME = "my-topic";
// Consumer隶属的消费者组
private final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
// 配置消费者端需要的KV
Properties properties = new Properties();
// 消费的目的集群 及集群中机器通信的端口
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9002,127.0.0.1:9003,127.0.0.1:9004");
// 把消费的消息key从字符串序列化为字符数组
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 把消费的消息value从字符串序列化为字符数组
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 消费者所属ConsumerGroup名称
properties.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
// 创建一个消费者的客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
// 消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
while (true) {
// poll()是拉取消息的长轮询 Consumer可以在1s内拉取n次 1s结束后开始消费拉取n次取到的消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
// 每条ConsumerRecord就是一条具体的消息 可以得到该消息被发到的Partition 在Partition中的偏移量 以及消息的核心生产者设置的KV
for(ConsumerRecord<String, String> record : consumerRecords) {
System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
record.partition(), record.offset(), record.key(), record.value());
}
}
}
}
2.2.2 offset的自动提交和手动提交
Consumer收到消息后,需要向Broker的_consumer_offset主题提交当前主题-分区-消息的偏移量
以记录Topic中哪些消息已经被消费,哪些尚没有被消费
提交的方式有两种,自动提交和手动提交
自动提交
// 配置消费者是否自动提交offset 默认为true
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// 配置自动提交offset的间隔时间
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
自动提交是默认开启的,消费者poll到消息后默认自动向Broker0的_consumer_offset提交偏移量
poll消息时,定位当前应该拉取哪些消息,也是根据offset来决定的
但是自动提交可能会丢失消息 因为如果Consumer还没消费完poll下来的消息就提交了偏移量
那么此时如果Consumer宕机,poll下来的有些消息就没有被消费成功
即丢失了一部分消息,但系统却认为这些消息已被消费
手动提交
手动提交即消费完消息后再提交该消息的offset
// 关闭默认开启的自动提交 采用手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
手动提交又可以用同步或异步的方式完成
手动同步提交:
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String, String> record : consumerRecords) {
System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
record.partition(), record.offset(), record.key(), record.value());
}
// 手动同步提交offset 提交成功前阻塞后面逻辑 但一般消费完也不做什么事
if(consumerRecords.count() > 0) {
consumer.commitAsync();
}
}
手动异步提交:
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String, String> record : consumerRecords) {
System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
record.partition(), record.offset(), record.key(), record.value());
}
// 手动异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
if(e != null) {
System.err.println("commit failed for " + offsets);
System.err.println("commit failed exception " + e.getStackTrace());
}
}
});
}
生产中一般都会使用手动提交,且使用同步的方式来完成
因为消费完一般消费者端就没有逻辑了,采用异步的方式意义并不大
2.2.3 消费者poll消息的细节
关于poll消息的过程,有如下几个配置:
// 一次poll最多拉取的消息条数500 可以根据消费消息的快慢来设置
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
// 如果两次poll的间隔超过了30s Kafka会任务当前的Consumer消费能力过弱 将其踢出消费者组
// 通过rebalance机制选择消费者组中新的Consumer来消费当前的Partition reblance机制又会造成额外开销
properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
再看拉取消息并消费的代码:
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String, String> record : consumerRecords) {
System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
record.partition(), record.offset(), record.key(), record.value());
}
// 手动同步提交
if(consumerRecords.count() > 0) {
consumer.commitAsync();
}
}
其实拉取消息的过程是这样的:
循环中每次拉取消息的总时间只有1s,但可以再这1s内拉取n次
如果一次poll到了500条,结束poll直接开始for循环
如果这次poll拉不到500条,且总时间在1s内,继续poll
1s结束后,开始消费这n次拉取的消息,消费结束后 提交offset
2.2.4 消费者的健康状态检查
先看这样两个配置:
// 配置Consumer给Broker发送心跳的间隔时间
properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
// Broker如果超过10s没有收到Consumer的心跳 会将Consumer踢出ConsumerGroup
// 并通过rebalance机制选择其他Consumer消费Partition
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
Consumer每隔1s向Kafka集群发送心跳,集群如果发现某个Consumer已经超过10s没有回传心跳
可以将失联的Consumer踢出消费者组,并触发该消费者组的rebalance机制
将分区交给别的Consumer消费
2.2.5 消费者指定分区偏移量和时间消费
1,为Consumer指定分区消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
2,消息回溯消费 即让消费者从某个分区的第一条消息开始消费
即从某个Partition的offset=0的消息开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
3,指定offset消费 即让消费者从某个分区的第n个消息开始消费 此处n=10
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAEM, 0), 10);
4,从指定时间点开始消费 如从1小时前开始消费 即消费1小时前到现在的消息
根据时间去所有的Partition中确认该时间对应的offset 然后在所有Partition中找到该offset之后的消息开始消费
2.2.6 新消费组消费offset的规则
考虑如下一个场景,原先只有一个ConsumerGroup消费某个Topic中的消息
现在增加一个ConsumerGroup到系统中并消费Topic
那么新加的ConsumerGroup能消费到哪些消息?
// 当新加了ConsumerGroup消费某个主题时 新ConsumerGroup中的Consumer有两种消费方式
// 1, latest 默认值 只消费自己启动后发到Topic的消息
// 2, earliest 一开始从头开始消费 以后按照offset正常消费
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");