文章目录
Kafka版本
- kafka版本1.1.1,可能绝大部分也适用于kafka 0.10.x及以上版本。
pull与push模型
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(偏移量)
(如果消费者消费了但是并未提交,是不记录的)
消费组
-
消费组:消费者使用一个消费组名(group.id)来标记自己,topic的每条消息都只会被发送到每个订阅它的消息组的一个消费者实例上
-
为每一个需要获取一个或者多个
Topic(主题)
全部消息的应用程序创建一个ConsumerGroup(消费组)
,然后往ConsumerGroup(消费组)
里添加Consumer(消费者)
来伸缩读取能力和处理能力,ConsumerGroup(消费组)
里的每个Consumer(消费者)
只处理一部分消息。不要让Consumer(消费者)
的数量超过Topic(主题)
分区的数量,多余的Consumer(消费者)
只会闲置
-
线程安全:在同一个
ConsumerGroup(消费组)
里,一个Consumer(消费者)
使用一个线程。无法让一个线程运行多个Consumer(消费者)
,也无法让多个线程安全地共享一个Consumer(消费者)
-
程序中使用
group.id
唯一标识一个ConsumerGroup(消费组)
,group.id
是一个自定义的字符串。ConsumerGroup(消费组)
可以有一个或多个Consumer
实例,Consumer
实例可以是一个进程,也可以是一个线程 -
Consumer分类
- 消费组(ConsumerGroup):多个comsumer共同执行消费,topic每条消息只能被组中的一个consumer消费。
- 如果所有的消费者都隶属于同一个ConsumerGroup,那么所有的消息都会被均衡地投递给每 一个Consumer,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
- 如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用 。
- 独立消费者(standalone consumer):单独执行消费操作
- 消费组(ConsumerGroup):多个comsumer共同执行消费,topic每条消息只能被组中的一个consumer消费。
偏移量
-
更新
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(消费者)
重新消费.
__consumers_offsets
-
__consumers_offsets
是kafka自己创建的(系统内部的topic),默认情况下有50个文件夹,编号从0-49。它存在的唯一目的就是保存 consumer 提交的位移。 -
__consumers_offsets
中存放的是KV格式的消息,key=group.id+topic+分区号,value就是最后一次提交的offset的值。每当offset被提交,就会写入一条含有最新offset的值。kafka会定期对该topic执行compact操作,为每个消息key只保留最新offset的消息。
Java API
- Consumer消费的一般步骤
- 构造Properties配置对象
- 创建KafkaConsumer实例
- 调用KafkaConsumer的subscribe订阅感兴趣的topic
- 循环调用KafkaConsumer的poll()获取ConsumerRecord
- 处理获取的ConsumerRecord
- 关闭KafkaConsumer,调用KafkaConsumer.close()
- 从Kafka Consumer的角度来说,poll方法返回即表示消费成功。
- 如果poll返回消息的速度很慢,可以调节参数来提升poll的效率
- 如果消息的业务逻辑处理很慢,应该把业务逻辑放到单独的线程中执行
- java Consumer是一个多线程的java进程,不是线程安全的,主线程创建KafkaConsumer以及poll的调用( 消费者组执行 rebalance、消息获取、 coordinator管理、异步任务结果的处理甚至位移提交等操作都是运行在用户主线程中的),consumer在后台会创建一个心跳线程,该线程被称为后台心跳线程。
poll的使用
-
consumer.poll(1000)。这里的 1000 是一个超时设定(timeout) 。 通常情况下如果 consumer 拿到了足够多的可用数据,那么它可以立即从该方法返回;但若当前没有足够多的数据可供返回, consumer会处于阻塞状态 。 这个超时参数即控制阻塞的最大时间。1000表示即使没有足够的数据,最多等待1000ms。这个超时的设定,可以在消费的同时做一些其他事情,比如定时任务,每隔10s记录一下消费情况
-
poll的使用
-
Consumer如果需要定期执行其他任务,推荐使用poll(timeout)
try { while (isRunning) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset()); } } }finally { consumer.close(); }
-
Consumer不需要定期执行其他任务,推荐poll(MAX_VALUE) +捕获 WakeupException的方式。需要在另外一个线程中调用consumer.wakeup()。在Consumer中只有wakeup()是线程安全的,其他方法不是线程安全的。WakeupException异常时在poll方法中抛出,所以主线程并不能立马响应wakeup并退出,必须等待下一次poll调用的时候才可以(所以不推荐很繁重的消息处理逻辑放入 poll主线程执行 )
try { while (true) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset()); } } }catch(WakeupException e){ //ignore }finally { consumer.close(); }
-
consumer关闭
-
优雅关闭
@Test public void testConsumerShutdown() { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000"); 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"); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_shutdown"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); String topic = "testTopic"; final Thread mainThread = Thread.currentThread(); /* * 退出循环需要通过另一个线程调用consumer.wakeup()方法 * 调用consumer.wakeup()可以退出poll(),并抛出WakeupException异常 * 我们不需要处理 WakeupException,因为它只是用于跳出循环的一种方式 * consumer.wakeup()是消费者唯一一个可以从其他线程里安全调用的方法 * 如果循环运行在主线程里,可以在 ShutdownHook里调用该方法 */ Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { logger.info("Consumer Starting exit..."); consumer.wakeup(); try { // 主线程继续执行,以便可以关闭consumer,提交偏移量 mainThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); try { consumer.subscribe(Collections.singletonList(topic)); while (true) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { logger.info("key:{},value:{},offset:{}", record.key(), record.value(), record.offset()); //异步提交 consumer.commitAsync((offsets, exception) -> { if (exception != null) { logger.error(exception.getMessage(), exception); } if (offsets != null) { offsets.forEach((key, value) -> { logger.info("key:{},value:{}", key, value); }); } }); } } } catch (WakeupException e) { // 不处理异常 } finally { /** * 在退出线程之前调用consumer.close()是很有必要的,它会提交任何还没有提交的东西,并向组协调器发送消息, * 告知自己要离开群组。接下来就会触发再均衡,而不需要等待会话超时。 */ consumer.commitSync(); consumer.close(); logger.info("Closed consumer done!"); } }
订阅topic
- 一个Consumer可以订阅一个或者多个Topic,如果Consumer连续订阅两次不同的topic集合,则以最后一次订阅的为准
- 如果消费者采用正则表达式的方式订阅,如果后面又创建新的topic,并且与正则表达式匹配,则consumer可以消费新topic中的消息。
- consumer订阅只有在下次poll调用时才会真正的生效
同步提交
-
同步提交,当commitSync()执行结束才会继续执行下一行代码。只要没有发生不可恢复的错误,commitSync方法会一直尝试直至提交成功。如果提交失败,我们也只能把异常记录到错误日志里。
-
commitSync()只会提交拉取操作中分区消息的最大偏移量加1,假设当前消费者已经消费了n位置,那么提交到是n+1的offset
-
同步提交代码(简单示例)
/** * 同步提交 */ @Test public void testConsumerSync() { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094"); props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000"); 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"); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_sync"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); String topic = "testTopic"; //后续的订阅会覆盖之前的,所以此方法只调用一次即可 consumer.subscribe(Collections.singletonList(topic)); try { while (true) { //如果缓冲区中没有数据会阻塞,timeout设置为1000ms ConsumerRecords<String, String> records = consumer.poll