kafka版本声明
- 使用的是
kafka 0.10.0.1
版本 - 参考文献
- 《kafka权威指南》
基础
Push(推送)
方式是broker
接收到消息后,主动把消息推送到Consumer(消费者)
- 优点:实时性高
- 缺点:
- 加大
broker
的工作量,影响broker
性能. Consumer(消费者)
的处理能力各不相同,Consumer(消费者)
的状态不受broker
控制,如果Consumer(消费者)
不能及时处理,broker
推送过来的消息,可能会造成各种问题,比如缓冲区溢出、内存溢出等
- 加大
Pull(拉取)
方式是Consumer(消费者)
循环地从broker
拉取消息,拉取多少消息,什么时候拉取都是由Consumer(消费者)
决定,处理完毕再继续拉取,这样可以达到限流的目的,不会出现处理不过来的情况。- 优点:
Consumer(消费者)
自己控制流量 - 缺点: 拉取消息的时间间隔不好控制,间隔太短就处在一个忙等的状态,浪费资源,时间间隔太长,
broker
的消息不能及时处理
- 优点:
kafka
采用pull(拉取)
模型,由Consumer
自己记录消费状态,每个Consumer
互相独立地顺序读取每个Partition(分区)
的消息。kafka
服务端有一个__consumer_offsets
的内部Topic(主题)
,用来记录消费者已经提交的offset(偏移量)
(如果消费者消费了但是并未提交,是不记录的)
消费组
-
为每一个需要获取一个或者多个
Topic(主题)
全部消息的应用程序创建一个ConsumerGroup(消费组)
,然后往ConsumerGroup(消费组)
里添加Consumer(消费者)
来伸缩读取能力和处理能力,ConsumerGroup(消费组)
里的每个Consumer(消费者)
只处理一部分消息。不要让Consumer(消费者)
的数量超过Topic(主题)
分区的数量,多余的Consumer(消费者)
只会闲置
-
线程安全
- 在同一个
ConsumerGroup(消费组)
里,一个Consumer(消费者)
使用一个线程。无法让一个线程运行多个Consumer(消费者)
,也无法让多个线程安全地共享一个Consumer(消费者)
- 在同一个
-
程序中使用
group.id
唯一标识一个ConsumerGroup(消费组)
,group.id
是一个自定义的字符串。ConsumerGroup(消费组)
可以有一个或多个Consumer
实例,Consumer
实例可以是一个进程,也可以是一个线程
rebalance
ConsumerGroup(消费组)
里的Consumer(消费者)
共同读取topic(主题)
的partition(分区)
,一个新的Consumer(消费者)
加入ConsumerGroup(消费组)
时,读取的是原本由其他Consumer(消费者)
读取的消息。当一个Consumer(消费者)
被关闭或发生奔溃时,它就离开ConsumerGroup(消费组)
,原本由它读取的分区将有ConsumerGroup(消费组)
的其他Consumer(消费者)
来读取。在topic
发生变化时(比如添加了新的分区),会发生Partition
重分配,Partition
的所有权从一个Consumer(消费者)
转移到另一个Consumer(消费者)
的行为被称为rebalance(再均衡)
。rebalance(再均衡)
本质上是一种协议,规定了ConsumerGroup(消费组)
中所有Consumer(消费者)
如何达成一致来消费topic(主题)
下的partition(分区)
rebalance(再均衡)
为ConsumerGroup(消费组)
带来了高可用性和伸缩性(可以安全的添加或移除消费者),在rebalance(再均衡)
期间,Consumer(消费者)
无法读取消息,造成整个Consumer(消费者)
一段时间的不可用Consumer(消费者)
通过向GroupCoordinator(群组协调器)
(不同的ConsumerGroup(消费组)
可以有不同的)发送心跳
来维持它们与群组的从属关系以及它们对分区的所有权关系。Consumer(消费者)
会在轮询消息或者提交偏移量时发送心跳(kafka0.10.1
之前的版本),在kafka0.10.1
版本里,心跳线程是独立的- 分配分区的过程
Consumer(消费者)
加入ConsumerGroup(消费组)
时,会向GroupCoordinator(群组协调器)
发送一个JoinGroup请求
,第一个加入群组的Consumer(消费者)
将会成为群主,群主从GroupCoordinator(群组协调器)
获得ConsumerGroup(消费组)
的成员列表(此列表包含所有最新正常发送心跳的活跃的Consumer(消费者)
),并负责给每一个Consumer(消费者)
分配分区(PartitionAssignor
的实现类来决定哪个分区被分配给哪个Consumer(消费者)
)- 群主把分配情况列表发送给
GroupCoordinator(群组协调器)
,GroupCoordinator(群组协调器)
再把这些信息发送给ConsumerGroup(消费组)
里所有的Consumer(消费者)
。每个Consumer(消费者)
只能看到自己的分配信息,只有群主知道ConsumerGroup(消费组)
里所有消费者的分配信息。
rebalance(再均衡)
触发条件ConsumerGroup(消费组)
里的Consumer(消费者)
发生变更(主动加入、主动离开、崩溃)- 订阅
topic(主题)
的数量发生变更(比如使用正则表达式的方式订阅) - 订阅
topic(主题)
的partition(分区)
数量发生变更
提交偏移量
-
更新
Partition
当前位置操作叫做commit(提交)
。当Consumer(消费者)
发生崩溃或者有新的Consumer(消费者)
加入ConsumerGroup(消费组)
,就会触发rebalance(再均衡)
,完成rebalance(再均衡)
之后每个Consumer(消费者)
可能分配到新的Partition(分区)
,为了能够继续处理之前的消息,Consumer(消费者)
需要读取每个Partition(分区)
最后一次提交的Offset(偏移量)
,然后从指定的Offset(偏移量)
位置继续处理 -
Consumer(消费者)
提交的Offset
与Consumer(消费者)
处理的Offset
- 提交偏移量小于客户端处理的偏移量: 两个偏移量之间的消息就会被重复处理
- 提交偏移量大于客户端处理的偏移量: 处于两个偏移量之间的消息将会丢失
- 提交偏移量小于客户端处理的偏移量: 两个偏移量之间的消息就会被重复处理
-
在当前使用的
kafka 0.10.0.1
版本中,心跳不是独立的,消息处理时间过长, poll的时间间隔很长, 导致不能及时在poll发送心跳, 且offset
也不能提交, 客户端被超时被判断为挂掉,未提交offset
的消息会被其他Consumer(消费者)
重新消费.
测试不提交情况
-
不提交
static Properties props = new Properties(); @BeforeClass public static void testBefore() { props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092"); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); } @Test public void testConsumerNoCommit() { props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group"); KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props); String topic = "testTopic"; consumer.subscribe(Collections.singletonList(topic)); while (true) { //如果缓冲区中没有数据会阻塞 ConsumerRecords<Integer, String> records = consumer.poll(1000); for (ConsumerRecord<Integer, String> record : records) { System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset()); } //consumer.commitSync(); 注释手动提交 } }
-
查看服务端的
test_group
的状态,可以看到服务存储的test_group
已经提交的偏移量是unknown
,即还是开始状态bash-4.3# bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG OWNER test_group testTopic 0 unknown 0 unknown consumer-1_/172.17.0.1 test_group testTopic 1 unknown 0 unknown consumer-1_/172.17.0.1 test_group testTopic 2 unknown 100 unknown consumer-1_/172.17.0.1 test_group testTopic 3 unknown 0 unknown consumer-1_/172.17.0.1 test_group testTopic 4 unknown 0 unknown consumer-1_/172.17.0.1
-
重启再次运行
Consumer
(不更改消费组的名称),此时会重复消费,即从开始位置消费。 为什么要重启Consumer
呢?因为kafka
的offset
的记录会有两份,服务端会记录一份,本地的Consumer
也会记录一份,已经提交的offset
会告诉服务端,但是本地的offset
,无论提交与否都会记录,所以如果不重启不会重复消费。 -
提交一部分,
commitSync()
和commitAsync()
只会提交最后一个offset
,如果想提交一部分,则不能直接使用commitSync()
,需要使用commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets)
//错误的写法 @Test public void testConsumerNoCommit() { props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_2"); KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props); String topic = "testTopic"; consumer.subscribe(Collections.singletonList(topic)); int count = 0; while (true) { ConsumerRecords<Integer, String> records = consumer.poll(1000); for (ConsumerRecord<Integer, String> record : records) { System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset()); count++; if (count == 50) { System.out.println(count); //commitSync只会提交最后一个offset,所以提交的偏移量是100,而不是50,因为服务端有100条消息 consumer.commitSync(); } } } } # bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group_2 GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG OWNER test_group_2 testTopic 0 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 1 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 2 100 100 0 consumer-1_/172.17.0.1 test_group_2 testTopic 3 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 4 unknown 0 unknown consumer-1_/172.17.0.1 //正确的写法 @Test public void testConsumerOffset() { props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_2"); KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props); String topic = "testTopic"; consumer.subscribe(Collections.singletonList(topic)); //跟踪已经正确处理的偏移量 Map<TopicPartition, OffsetAndMetadata> offsetAndMetadataMap = new HashMap<>(); int count = 0; while (true) { ConsumerRecords<Integer, String> records = consumer.poll(1000); for (ConsumerRecord<Integer, String> record : records) { System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset()); offsetAndMetadataMap.put( new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1, "") ); //当为50时提交map中的偏移量 if (count == 50) { consumer.commitSync(offsetAndMetadataMap); } count++; } } } # bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group_2 GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG OWNER test_group_2 testTopic 0 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 1 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 2 51 100 49 consumer-1_/172.17.0.1 test_group_2 testTopic 3 unknown 0 unknown consumer-1_/172.17.0.1 test_group_2 testTopic 4 unknown 0 unknown consumer-1_/172.17.0.1