1. 什么是KafkaConsumer?

  应用程序使用KafkaConsul'le 「向Kafka 订阅主题,并从订阅的主题上接收消息。Kafka的消息读取不同于从其他消息系统读取数据,它涉及了一些独特的概念和想法。

  1.1 消费者和消费者群组

  单个的消费者就跟前面的消息系统的消费者一样,创建一个消费者对象,然后订阅一个主题并开始接受消息,然后做自己的业务逻辑,但是Kafka天生就是支持体量很大的数据消费,如果只是使用单个的消费者消费消息,当生产者写入消息的速度远远大于了消费者的速度,大量消息堆积在消费者上可能会导致性能反而降低或撑爆消费者,所以横向伸缩是很有必要的,就想多个生产者可以向相同的主题写消息一样,我们也可以使用多个消费者从同一个主题读取消息,对消息进行分流,这多个消费者就从属于一个消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。

  假设主题T1有四个分区,我们创建了消费者群组G1,创建了一个消费者C1从属于G1,它是G1里的唯一的消费者,此时订阅主题情况为,C1将会接收到主题中四个分区中的消息,如图:

  此时我们在消费者群组中新增一个消费者C2,那么每个消费者将分别从两个分区接受消息,如图:

  如果我们有四个消费者时,将会每个消费者都分到一个分区。

  如果群组中的消费者超过了主题的分区数,那么有一部分消费者就会被闲置,不会接收任何消息。如图:

  往群组里增加消费者是横向伸缩消费能力的主要方式。

  对于多个群组来说,每个群组都会从Kafka中接收到所有的消息,并且各个群组之间是互不干扰的。所以横向伸缩Kafka消费者和消费者群组并不会对性能造成负面影响。简而言之就是,为每一个需要获取一个或多个主题全部消息的应用程序创建一个消费者群组,然后往群组里添加消费者来伸缩读取能力和处理能力,群组里的每个消费者只处理一部分消息。如图:

  1.2  消费者群组和分区再均衡

  一个新的消费者加入群组时,它读取的是原本由其他消费者读取的消息。当一个消费者被关闭或发生奔溃时,它就离开群组,原本由它读取的分区将由群组里的其他消费者来读取。在主题发生变化时, 比如管理员添加了新的分区,会发生分区重分配。分区的所有权从一个消费者变成了里另一个消费者,这样的行为被称为再均衡。再均衡非常重要, 它为消费者群组带来了高可用性和伸缩性(我们可以放心地添加或移除消费者),不过在正常情况下,我们并不希望发生这样的行为。在再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。

  消费者通过向被指派为群组协调器的broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息(为了获取消息)或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。如果一个消费者发生崩溃,井停止读取消息,群组协调器会等待几秒钟,确认它死亡了才会触发再均衡。在这几秒钟时间里,死掉的消费者不会读取分区里的消息。在清理消费者时,消费者会通知协调器它将要离开群组,协调器会立即触发一次再均衡,尽量降低处理停顿。

2. 创建Kafka消费者并读取消息

  在创建KafkaConsumer之前,需要将消费者想要的属性存放到Properties中,然后再将properties传给KafkaConsumer。

  Consuer也有三个必须的属性。bootstrap.servers,这里跟Producer一样,另外两个key.deserializer和value.deserializer也与Producer类似,不过一个是序列化,一个是反序列化而已。

  还有一个group.id不是必须的,但是我们通常都会指定改消费者属于哪个群组,所以也可以认为是必须的。

  设置Properties的代码片段如下: 

 Properties kafkaPropertie = new Properties();
//配置broker地址,配置多个容错
kafkaPropertie.put("bootstrap.servers", "node2:9092,node1:9092,node1:9093");
//配置key-value允许使用参数化类型,反序列化
kafkaPropertie.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
kafkaPropertie.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
//指定消费者所属的群组
kafkaPropertie.put("group.id","one");

  接下来创建消费者,将Properties对象传入到消费者,然后订阅主题,如下:

  KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(kafkaPropertie);
/*订阅主题,这里使用的是最简单的订阅testTopic主题,这里也可以出入正则表达式,来区分想要订阅的多个指定的主题,如:
*Pattern pattern = new Pattern.compile("testTopic");
* consumer.subscribe(pattern);
*/ consumer.subscribe(Collections.singletonList("testTopic"));

  接下来轮询消息,如下:

 //轮询消息
while (true) {
//获取ConsumerRecords,一秒钟轮训一次
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
//消费消息,遍历records
for (ConsumerRecord<String, String> r : records) {
LOGGER.error("partition:", r.partition());
LOGGER.error("topic:", r.topic());
LOGGER.error("offset:", r.offset());
System.out.println(r.key() + ":" + r.value());
}
Thread.sleep(1000);
}

  生产者发送消息,然后查看消费者打印情况:

 KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world0
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world1
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world2
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world3
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world4
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world5
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world6
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world7
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world8
KafkaConsuerDemo - partition:
KafkaConsuerDemo - topic:
KafkaConsuerDemo - offset:
key1:hello world9

  只存在一个组群和一个消费者时:

  当我们启动两个消费者,同一个组群,并在Topic上创建两个Partition(分区),发送消息

   final ProducerRecord<String, String> record = new ProducerRecord<String, String>("one",i %  2,"key3","hello world" + i); 
将消息分发到0和1两个partition

  此时两个消费者消费的消息总和等于发送的消息的总和,使用不同的群组的不同的订阅同一个topic,每个消费者群组都能收到所有的消息。

  轮询不只是获取数据那么简单。在第一次调用新消费者的poll ()方法时,它会负责查找GroupCoordinator , 然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。当然,心跳也是从轮询里发送出去的。所以,我们要确保在轮询期间所做的任何处理工作都应该尽快完成。

  消费者完整代码如下:

package com.wangx.kafka.client;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.time.Duration;
import java.util.Collections;
import java.util.Properties; public class KafkaConsuerDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsuerDemo.class);
public static void main(String[] args) throws InterruptedException {
Properties kafkaPropertie = new Properties();
//配置broker地址,配置多个容错
kafkaPropertie.put("bootstrap.servers", "node2:9092,node1:9092,node1:9093");
//配置key-value允许使用参数化类型,反序列化
kafkaPropertie.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
kafkaPropertie.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
//指定消费者所属的群组
kafkaPropertie.put("group.id","1");
//创建KafkaConsumer,将kafkaPropertie传入。
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(kafkaPropertie);
/*订阅主题,这里使用的是最简单的订阅testTopic主题,这里也可以出入正则表达式,来区分想要订阅的多个指定的主题,如:
*Pattern pattern = new Pattern.compile("testTopic");
* consumer.subscribe(pattern);
*/ consumer.subscribe(Collections.singletonList("one"));
//轮询消息
while (true) {
//获取ConsumerRecords,一秒钟轮训一次
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
//消费消息,遍历records
for (ConsumerRecord<String, String> r : records) {
LOGGER.error("partition:", r.partition());
LOGGER.error("topic:", r.topic());
LOGGER.error("offset:", r.offset());
System.out.println(r.key() + ":" + r.value());
}
Thread.sleep(1000);
}
}
}

3. 消费者的配置

  1. fetch.min.bytes: 该属性指定了消费者从服务器获取记录的最小字节数。

  2. fetch.max.wait.ms:我们通过 fetch.min.byte告诉Kafka ,等到有足够的数据时才把它返回给消费者。

而 fetch.max.wait.ms则用于指定broker 的等待时间

  3. max.partition.fetch.bytes:默认值是1MB,该属性指定了服务器从每个分区里返回给消费者的最大字节数.

  4. session.timeout.ms: 默认3s,该属性指定了消费者在被认为死亡之前可以与服务器断开连接的时

  5. auto.offset.reset:该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长时间失效,包含偏移量的记录已经过时井被删除)该作何处

  6. enable.auto.commit:该属性指定了消费者是否自动提交偏移量,默认值是true。

  7. partition.assignment.strategy: 分区分配给消费者群组的分配策略,有如下两种策略:

  Range:该策略会把主题的若干个连续的分区分配给消费者.

  RoundRobin:该策略把主题的所有分区逐个分配给消费.

  8. client.id:该属性可以是任意字符串, broker 用它来标识从客户端发送过来的消息,通常被用在日志、度量指标和配额里。

  9. max.poll.records: 该属性用于控制单次调用call () 方法能够返回的记录数量,可以帮你控制在轮询里需要处理的数据量。

  10.  receive.buffer.bytes 和send.buffer.bytes: socket 在读写数据时用到的TCP 缓冲区也可以设置大小。如果它们被设为-1,就使用操作系统的默认值。如果生产者或消费者与broker处于不同的数据中心内,可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。

Kafka学习笔记(7)----Kafka使用Cosumer接收消息的更多相关文章

  1. Kafka学习笔记之Kafka性能测试方法及Benchmark报告

    0x00 概述 本文主要介绍了如何利用Kafka自带的性能测试脚本及Kafka Manager测试Kafka的性能,以及如何使用Kafka Manager监控Kafka的工作状态,最后给出了Kafka ...

  2. Kafka学习笔记之Kafka Consumer设计解析

    0x00 摘要 本文主要介绍了Kafka High Level Consumer,Consumer Group,Consumer Rebalance,Low Level Consumer实现的语义,以 ...

  3. Kafka学习笔记之Kafka三款监控工具

    0x00 概述 在之前的博客中,介绍了Kafka Web Console这 个监控工具,在生产环境中使用,运行一段时间后,发现该工具会和Kafka生产者.消费者.ZooKeeper建立大量连接,从而导 ...

  4. Kafka学习笔记之Kafka背景及架构介绍

    0x00 概述 本文介绍了Kafka的创建背景,设计目标,使用消息系统的优势以及目前流行的消息系统对比.并介绍了Kafka的架构,Producer消息路由,Consumer Group以及由其实现的不 ...

  5. Kafka学习笔记之Kafka High Availability(上)

    0x00 摘要 Kafka在0.8以前的版本中,并不提供High Availablity机制,一旦一个或多个Broker宕机,则宕机期间其上所有Partition都无法继续提供服务.若该Broker永 ...

  6. Kafka学习笔记之Kafka High Availability(下)

    0x00 摘要 本文在上篇文章基础上,更加深入讲解了Kafka的HA机制,主要阐述了HA相关各种场景,如Broker failover,Controller failover,Topic创建/删除,B ...

  7. Kafka学习笔记1——Kafka的安装和启动

    一.准备工作 1. 安装JDK 可以用命令 java -version 查看版本

  8. Kafka学习笔记之Kafka自身操作日志的清理方法(非Topic数据)

    0x00 概述 本文主要讲Kafka自身操作日志的清理方法(非Topic数据),Topic数据自己有对应的删除策略,请看这里. Kafka长时间运行过程中,在kafka/logs目录下产生了大量的ka ...

  9. Kafka学习笔记之Kafka日志删出策略

    0x00 概述 kafka将topic分成不同的partitions,每个partition的日志分成不同的segments,最后以segment为单位将陈旧的日志从文件系统删除. 假设kafka的在 ...

  10. 【kafka学习笔记】kafka的基本概念

    在了解了背景知识后,我们来整体看一下kafka的基本概念,这里不做深入讲解,只是初步了解一下. kafka的消息架构 注意这里不是设计的架构,只是为了方便理解,脑补的三层架构.从代码的实现来看,kaf ...

随机推荐

  1. 洛谷P1002 过河卒【dp】

    棋盘上AA点有一个过河卒,需要走到目标BB点.卒行走的规则:可以向下.或者向右.同时在棋盘上CC点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点.因此称之为"马拦过河卒 ...

  2. case...when...then if 用法

    select case when if 的一些用法 概述:sql语句中的case语句与高级语言中的switch语句,是标准sql的语法,适用于一个条件判断有多种值的情况下分别执行不同的操作. 首先,让 ...

  3. Linux启用ftp服务及连接

    虚拟机的系统是centos6.3 第一步.启动ftp service vsftpd restart 提示 vsftpd: 未被识别的服务 解决方法是升级vsftpd服务 yum install vsf ...

  4. 从零开始创建一个 PHP 扩展

    创建一个扩展的基本步骤都有哪些.示例中,我们将实现如下功能: <?phpecho say();?> 输出内容: $ php ./test.php$ hello word 在扩展中实现一个s ...

  5. 通过fmt标签格式化el表达式中的日期、小数

    首先引入fmt外部文件包 <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" % ...

  6. 关于使用sudo命令后找不到JAVA_HOME的问题

    今天在虚拟机搭建zookeeper环境,结果死活运行不起来,唯一的信息只有out文件里“没有java命令”这一个提示,找来找去发现是找不到java运行环境.可是很奇怪,明明我已经配置了,而且在终端执行 ...

  7. HDU 5176

    这道题以前好像在哪遇到过. 注意树的每一条边都是桥,所以,桥两端的点要到达对方是必须通过这条边的.于是,可以把边由小到大排序,利用并查集,这样,每加一条边就连通了一部分,而随着权值的增大,必定是桥两端 ...

  8. Java内部静态类与内部非静态类

    Java内部静态类与内部非静态类 把类看成一个属性,稍微容易理解一些:在main方法中,不会去直接引用一个非static的变量,对于类也一样. 学习了:http://blog.csdn.net/zer ...

  9. 例题 2-1 aabb 2-2 3n+1问题

    例题2-1  aabb 输出全部形如aabb的四位全然平方数(即前两位数字相等,后两位数字也相等) #include <stdio.h> #include <stdlib.h> ...

  10. luogu3942 将军令 贪心

    题目大意:给你一个地图(树),共有1~n个驿站(点),编号分别为1~n,告诉你第ui个驿站与第vi个驿站有一条长度为1的路(边),每个小队(可以放在任意驿站上)最多有k的覆盖长度,问最多要放置多少个小 ...