文章目录
1. 消费者、消费组
消费者从订阅的主题消费消息,消费消息的偏移量保存在Kafka的名字是 __consumer_offsets 的主题中。
消费者还可以将自己的偏移量存储到Zookeeper,需要设置offset.storage=zookeeper。
推荐使用Kafka存储消费者的偏移量。因为Zookeeper不适合高并发。
多个从同一个主题消费的消费者可以加入到一个消费组中。 消费组中的消费者共享group_id。configs.put(“group.id”, “xxx”);
group_id一般设置为应用的逻辑名称。比如多个订单处理程序组成一个消费组,可以设置group_id 为"order_process"。
group_id通过消费者的配置指定: group.id=xxxxx 消费组均衡地给消费者分配分区,每个分区只由消费组中一个消费者消费。
如下为 一个拥有四个分区的主题,包含一个消费者的消费组。
此时,消费组中的消费者消费主题中的所有分区。并且没有重复的可能。如果在消费组中添加一个消费者2,则每个消费者分别从两个分区接收消息。
如果消费组有四个消费者,则每个消费者可以分配到一个分区。
如果向消费组中添加更多的消费者,超过主题分区数量,则有一部分消费者就会闲置,不会接收任何消息。
向消费组添加消费者是横向扩展消费能力的主要方式。必要时,需要为主题创建大量分区,在负载增长时可以加入更多的消费者。但是不要让消费者的数 量超过主题分区的数量。
除了通过增加消费者来横向扩展单个应用的消费能力之外,经常出现多个应用程序从同一个主题消费的情况。
此时,每个应用都可以获取到所有的消息。只要保证每个应用都有自己的消费组,就可以让它们获取到主题所有的消息。
横向扩展消费者和消费组不会对性能造成负面影响。
为每个需要获取一个或多个主题全部消息的应用创建一个消费组,然后向消费组添加消费者来横向 扩展消费能力和应用的处理能力,则每个消费者只处理一部分消息。
2. 消费再均衡及心跳机制
重平衡可以说是kafka为人诟病最多的一个点了。
重平衡其实就是一个协议,它规定了如何让消费者组下的所有消费者来分配topic中的每一个分区。 比如一个topic有100个分区,一个消费者组内有20个消费者,在协调者的控制下让组内每一个消费者分 配到5个分区,这个分配的过程就是重平衡。
重平衡的触发条件主要有三个:
- 消费者组内成员发生变更,这个变更包括了增加和减少消费者,比如消费者宕机退出消费组。
- 主题的分区数发生变更,kafka目前只支持增加分区,当增加的时候就会触发重平衡
- 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡
- 消费者宕机,退出消费组,触发再平衡,重新给消费组中的消费者分配分区。
- 由于broker宕机,主题X的分区3宕机,此时分区3没有Leader副本,触发再平衡,消费者4没有对 应的主题分区,则消费者4闲置。
- 主题增加分区,需要主题分区和消费组进行再均衡。
- 由于使用正则表达式订阅主题,当增加的主题匹配正则表达式的时候,也要进行再均衡。
为什么说重平衡为人诟病呢?因为重平衡过程中,消费者无法从kafka消费消息,这对kafka的TPS 影响极大,而如果kafka集内节点较多,比如数百个,那重平衡可能会耗时极多。数分钟到数小时都有 可能,而这段时间kafka基本处于不可用状态。所以在实际环境中,应该尽量避免重平衡发生。
如何避免重平衡?
要说完全避免重平衡,是不可能,因为你无法完全保证消费者不会故障。而消费者故障其实也是最 常见的引发重平衡的地方,所以我们需要保证尽力避免消费者故障。
而其他几种触发重平衡的方式,增加分区,或是增加订阅的主题,抑或是增加消费者,更多的是主动控制。
如果消费者真正挂掉了,就没办法了,但实际中,会有一些情况,kafka错误地认为一个正常的消费者已经挂掉了,我们要的就是避免这样的情况出现。
首先要知道哪些情况会出现错误判断挂掉的情况。 在分布式系统中,通常是通过心跳来维持分布式系统的,kafka也不例外。
- 心跳机制
在分布式系统中,由于网络问题你不清楚没接收到心跳,是因为对方真正挂了还是只是因为负载过重没来得及发生心跳或是网络堵塞。所以一般会约定一个时间,超时即判定对方挂了。
在kafka消费者场景中,参数配置如下:
session.timeout.ms //控制心跳超时时间 默认10000ms
heartbeat.interval.ms //控制心跳发送频率 默认3000ms 频率越高越不容易被误 判,但也会消耗更多资源。
max.poll.interval.ms //控制poll的间隔 默认300000ms
max.poll.records // 默认500条
session.timeout.ms是consumer和kafka server维持一个会话的时间,也就是说consumer和server之间通信的间隔时间最长是这些,超过这个时间的话server就认为consumer不可用,会被从consumer group当中踢掉。因为现在consumer有一个专门的heartbeat后台线程来维持心跳,默认的时间间隔是 heartbeat.interval.ms 默认3000ms,所以这个配置不用担心
max.poll.interval.ms是consumer在两次poll()之间的最大时间间隔,超过这个时间配置的consumer都会被从consumer group 当中踢掉。这样的话,在两次poll()中间的数据处理时间久需要控制了。默认的时间是 300000,也就是5分钟,同时每次拉下来的数据条数受max.poll.records控制,默认最多为500条。
如果我们的数据有些关联数据比较多,可能存在一个批次的数据消费处理时间超过5min。在这种理论基础上,我们将max.poll.interval.ms加大,同时将max.poll.records减小到100。
但是rebalance耗时比较长的情况仍然存在。这个时候考虑是因为定时任务的启动和结束导致的rebalance,但是为何rebalance耗时5分钟仍然是不可理解的。后面在kafka的官方文档中有这个的配置:rebalance.timeout.ms文档上介绍的就是rebalance会等待consumer 发起join-group请求的最大时长,默认是60s,但是这个配置是针对的kafka-connect的,不是我们这里的。
这里给出一个相对较为合理的配置,如下:
session.timout.ms:设置为6s
heartbeat.interval.ms:设置2s
max.poll.interval.ms:推荐为消费者处理消息最长耗时再加1分钟
3. 消费者参数配置
参数 | 说明 |
---|---|
bootstrap.servers | 向Kafka集群建立初始连接用到的host/port列表。 客户端会使用这里列出的所有服务器进行集群其他服务器的发现,而不管 是否指定了哪个服务器用作引导。 这个列表仅影响用来发现集群所有服务器的初始主机。 字符串形式:host1:port1,host2:port2,… 由于这组服务器仅用于建立初始链接,然后发现集群中的所有服务器,因 此没有必要将集群中的所有地址写在这里。 一般最好两台,以防其中一台宕掉。 |
key.deserializer | key的反序列化类,该类需要实现 org.apache.kafka.common.serialization.Deserializer 接口。 |
value.deserializer | 实现了 org.apache.kafka.common .serialization.Deserializer 接口的反序列化器,用于对消息的value进行反序列化。 |
client.id | 当从服务器消费消息的时候向服务器发送的id字符串。在ip/port基础上 提供应用的逻辑名称,记录在服务端的请求日志中,用于追踪请求的源。 |
group.id | 用于唯一标志当前消费者所属的消费组的字符串。 如果消费者使用组管理功能如subscribe(topic)或使用基于Kafka的偏移量 管理策略,该项必须设置。 |
auto.offset.reset | 当Kafka中没有初始偏移量或当前偏移量在服务器中不存在(如,数据被 删除了),该如何处理? earliest:自动重置偏移量到最早的偏移量 latest:自动重置偏移量为最新的偏移量 none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异 常 anything:向消费者抛异常 |
enable.auto.commit | 如果设置为true,消费者会自动周期性地向服务器提交偏移量。 |
auto.commit.interval.ms | 如果设置了 enable.auto.commit 的值为true, 则该值定义了消费者偏移量向Kafka提交的频率。 |
fetch.min.bytes | 服务器对每个拉取消息的请求返回的数据量最小值。 如果数据量达不到这个值,请求等待,以让更多的数据累积, 达到这个值之后响应请求。 默认设置是1个字节,表示只要有一个字节的数据, 就立即响应请求,或者在没有数据的时候请求超时。 将该值设置为大一点儿的数字,会让服务器等待稍微 长一点儿的时间以累积数据。 如此则可以提高服务器的吞吐量,代价是额外的延迟时间。 |
fetch.max.wait.ms | 如果服务器端的数据量达不到 fetch.min.bytes 的话, 服务器端不能立即响应请求。 该时间用于配置服务器端阻塞请求的最大时长。 |
fetch.max.bytes | 服务器给单个拉取请求返回的最大数据量。 消费者批量拉取消息,如果第一个非空消息批次的值比该值大, 消息批也会返回,以让消费者可以接着进行。 即该配置并不是绝对的最大值。 broker可以接收的消息批最大值通过message.max.bytes (broker配置)或 max.message.bytes (主题配置)来指定。 需要注意的是,消费者一般会并发拉取请求。 |
connections.max.idle.ms | 在这个时间之后关闭空闲的连接。 |
check.crcs | 自动计算被消费的消息的CRC32校验值。 可以确保在传输过程中或磁盘存储过程中消息没有被破坏。 它会增加额外的负载,在追求极致性能的场合禁用。 |
exclude.internal.topics | 是否内部主题应该暴露给消费者。如果该条目设置为true, 则只能先订阅再拉取。 |
isolation.level | 控制如何读取事务消息。如果设置了 read_committed ,消费者的poll()方法只会 返回已经提交的事务消息。如果设置了 read_uncommitted (默认值), 消费者的poll方法返回所有的消息,即使是已经取消的事务消息。 非事务消息以上两种情况都返回。消息总是以偏移量的顺序返回。 |
heartbeat.interval.ms | 当使用消费组的时候,该条目指定消费者向消费者协调器 发送心跳的时间间隔。 心跳是为了确保消费者会话的活跃状态, 同时在消费者加入或离开消费组的时候方便进行再平衡。该条目的值必须小于 session.timeout.ms ,也不应该高于 session.timeout.ms 的1/3。可以将其调整得更小,以控制正常重新平衡的预期时间。 |
session.timeout.ms | 当使用Kafka的消费组的时候,消费者周期性地向broker发送心跳 表明自己的存在。如果经过该超时时间还没有收到消费者的心跳, 则broker将消费者从消费组移除,并启动再平衡。 该值必须在broker配置 group.min.session.timeout.ms 和 |
max.poll.records | 一次调用poll()方法返回的记录最大数量。 |
max.poll.interval.ms | 使用消费组的时候调用poll()方法的时间间隔。 该条目指定了消费者调用poll()方法的最大时间间隔。 如果在此时间内消费者没有调用poll()方法, 则broker认为消费者失败,触发再平衡, 将分区分配给消费组中其他消费者。 |
max.partition.fetch.bytes | 对每个分区,服务器返回的最大数量。消费者按批次拉取数据。 如果非空分区的第一个记录大于这个值,批处理依然可以返回, 以保证消费者可以进行下去。broker接收批的大小由 message.max.bytes (broker参数)或max.message.bytes (主题参数)指定。 fetch.max.bytes 用于限制消费者单次请求的数据量。 |
send.buffer.bytes | 用于TCP发送数据时使用的缓冲大小(SO_SNDBUF), -1表示使用OS默认的缓冲区大小。 |
retry.backoff.ms | 在发生失败的时候如果需要重试,则该配置表示客户端 等待多长时间再发起重试。 该时间的存在避免了密集循环。 |
request.timeout.ms | 客户端等待服务端响应的最大时间。如果该时间超时, 则客户端要么重新发起请求,要么如果重试耗尽,请求失败。 |
reconnect.backoff.ms | 重新连接主机的等待时间。避免了重连的密集循环。 该等待时间应用于该客户端到broker的所有连接。 |
reconnect.backoff.max.ms | 重新连接到反复连接失败的broker时要等待的最长时间 (以毫秒为单位)。 如果提供此选项,则对于每个连续的连接失败, 每台主机的退避将成倍增加,直至达到此最大值。 在计算退避增量之后,添加20%的随机抖动以避免连接风暴。 |
receive.buffer.bytes | TCP连接接收数据的缓存(SO_RCVBUF)。 -1表示使用操作系统的默认值。 |
partition.assignment.strategy | 当使用消费组的时候,分区分配策略的类名。 |
metrics.sample.window.ms | 计算指标样本的时间窗口。 |
metrics.recording.level | 指标的最高记录级别。 |
metrics.num.samples | 用于计算指标而维护的样本数量 |
interceptor.classes | 拦截器类的列表。默认没有拦截器 拦截器是消费者的拦截器,该拦截器需要实现org.apache.kafka.clients.consumer .ConsumerInterceptor 接口,拦截器可用于对消费者接收到的消息进行拦截处理。 |
4 消息订阅
Topic,Kafka用于分类管理消息的逻辑单元,类似与MySQL的数据库。
Partition,是Kafka下数据存储的基本单元,这个是物理上的概念。同一个topic的数据,会 被分散的存储到多个partition中,这些partition可以在同一台机器上,也可以是在多台机器 上。优势在于:有利于水平扩展,避免单台机器在磁盘空间和性能上的限制,同时可以通过复 制来增加数据冗余性,提高容灾能力。为了做到均匀分布,通常partition的数量通常是Broker Server数量的整数倍。
Consumer Group,同样是逻辑上的概念,是Kafka实现单播和广播两种消息模型的手段。 保证一个消费组获取到特定主题的全部的消息。在消费组内部,若干个消费者消费主题分区的消息,消费组可以保证一个主题的每个分区只被消费组中的一个消费者消费。
consumer 采用 pull 模式从 broker 中读取数据。
采用 pull 模式,consumer 可自主控制消费消息的速率, 可以自己控制消费方式(批量消费/逐条消费)
订阅主题 consumer.subscribe(“tp_demo_01,tp_demo_02”)
5. 反序列化
Kafka的broker中所有的消息都是字节数组,消费者获取到消息之后,需要先对消息进行反序列化处理,然后才能交给用户程序消费处理。 消费者的反序列化器包括key的和value的反序列化器。
自定义反序列化器需要实现 org.apache.kafka.common.serialization.Deserializer 接口。
消费者从订阅的主题拉取消息:consumer.poll(3_000)
消费示例:
public class UserDeserializer implements Deserializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
@Override
public User deserialize(String topic, byte[] data) {
ByteBuffer buffer = ByteBuffer.allocate(data.length);
buffer.put(data);
buffer.flip();
final int userId = buffer.getInt();
final int usernameLength = buffer.getInt();
String username = new String(data, 8, usernameLength);
return new User(userId, username);
}
@Override
public void close() {
}
}
public class UserConsumer {
public static void main(String[] args) {
Map<String, Object> configs = new HashMap<>();
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 设置自定义的反序列化器
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeserializer.class);
configs.put(ConsumerConfig.GROUP_ID_CONFIG, "user_consumer");
configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer_id");
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
KafkaConsumer<String, User> consumer = new KafkaConsumer<String, User>(configs);
// 订阅主题
consumer.subscribe(Collections.singleton("tp_user_01"));
final ConsumerRecords<String, User> records = consumer.poll(Long.MAX_VALUE);
records.forEach(new Consumer<ConsumerRecord<String, User>>() {
@Override
public void accept(ConsumerRecord<String, User> record) {
System.out.println(record.value());
}
});
// 关闭消费者
consumer.close();
}
}
6. 位移提交
- Consumer需要向Kafka记录自己的位移数据,这个汇报过程称为
- Consumer 需要为分配给它的每个分区提交各自的位移数据
- 位移提交的由Consumer端负责的,Kafka只负责保管。__consumer_offsets
- 位移提交分为自动提交和手动提交、 同步提交和异步提交
6.1 自动提交
开启自动提交:enable.auto.commit=true
Consumer端配置自动提交间隔: auto.commit.interval.ms 默认5s
Kafka会保证在开始调用poll方法时,提交上次poll返回的所有消息, 因此自动提交不会出现消息丢失,但会重复消费
重复消费举例
Consumer 每 5s 提交 offset,假设提交 offset 后的 3s 发生了 Rebalance,因为是后台自动提交,此时还没有真正提交到服务端,Rebalance 之后的所有 Consumer 从上一次提交的 offset 处继续消费,因此 Rebalance 发生前 3s 的消息会被重复消费
代码示例:
public class KafkaConsumerAuto {
/**
* kafka消费者不是线程安全的
*/
private final KafkaConsumer<String, String> consumer;
private ExecutorService executorService;
public KafkaConsumerAuto() {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "group");
// 打开自动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
props.put("auto.commit.interval.ms", "100");
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");
consumer = new KafkaConsumer<String, String>(props);
// 订阅主题
consumer.subscribe(Collections.singleton("tp_demo_02"));
}
public void execute() throws InterruptedException {
executorService = Executors.newFixedThreadPool(2);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(2_000);
if (null != records) {
executorService.submit(new ConsumerThreadAuto(records, consumer));
}
Thread.sleep(1000);
}
}
public void shutdown() {
try {
if (consumer != null) {
consumer.close();
}
if (executorService != null) {
executorService.shutdown();
}
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("关闭线程池超时。。。");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
public class ConsumerThreadAuto implements Runnable {
private ConsumerRecords<String, String> records;
private KafkaConsumer<String, String> consumer;
public ConsumerThreadAuto(ConsumerRecords<String, String> records,
KafkaConsumer<String, String> consumer) {
this.records = records;
this.consumer = consumer;
}
@Override
public void run() {
for(ConsumerRecord<String,String> record : records){
System.out.println("当前线程:" + Thread.currentThread()
+ "\t主题:" + record.topic()
+ "\t偏移量:" + record.offset() + "\t分区:" + record.partition()
+ "\t获取的消息:" + record.value());
}
}
}
6.2 手动同步提交
使用 KafkaConsumer#commitSync(),会提交 KafkaConsumer#poll() 返回的最新 offset,该方法为同步操作,等待直到 offset 被成功提交才返回
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
try {
consumer.commitSync();
} catch (CommitFailedException e) {
handle(e); // 处理提交失败异常 }
}
}
commitSync 在处理完所有消息之后,手动同步提交可以控制offset提交的时机和频率,但是手动同步提交会有以下问题:
- 调用 commitSync 时,Consumer 处于阻塞状态,直到 Broker 返回结果
- 会影响 TPS
可以选择拉长提交间隔,但有以下问题
- 会导致 Consumer 的提交频率下降,
- Consumer 重启后,会有更多的消息被消费
6.3 手动异步提交
KafkaConsumer#commitAsync(), commitAsync出现问题不会自动重试,处理方式如下:
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(2_000);
process(records); // 处理消息
consumer.commitAsync();// 使用异步提交规避阻塞
}
} catch (Exception e) {
handle(e); // 处理异常
} finally {
try {
consumer.commitSync(); // 最后一次提交使用同步阻塞式提交
} finally {
consumer.close();
}
}
7. 消费者位移管理
Kafka中,消费者根据消息的位移顺序消费消息。
消费者的位移由消费者管理,可以存储于zookeeper中,也可以存储于Kafka主题 __consumer_offsets中。
Kafka提供了消费者API,让消费者可以管理自己的位移。
方法 | 说明 |
---|---|
public void assign(Collection partitions) | 给当前消费者手动分配一系列主题分区。 手动分配分区不支持增量分配,如果先前有分配分区,则该操作会覆盖之前的分配。 如果给出的主题分区是空的,则等价于调用unsubscribe方法。 手动分配主题分区的方法不使用消费组管理功能。当消费组成员变了,或者集群或主题的 元数据改变了,不会触发分区分配的再平衡。 手动分区分配assign(Collection)不能和自动分区分配subscribe(Collection, ConsumerRebalanceListener)一起使用。 如果启用了自动提交偏移量,则在新的分区分配替换旧的分区分配之前,会对旧的分区分 配中的消费偏移量进行异步提交。 |
public Set assignment() | 获取给当前消费者分配的分区集合。如果订阅是通过调用assign方法直接分配主题分区, 则返回相同的集合。如果使用了主题订阅,该方法返回当前分配给该消费者的主题分区集 合。如果分区订阅还没开始进行分区分配,或者正在重新分配分区,则会返回none。 |
public Map<String, List> listTopics() | 获取对用户授权的所有主题分区元数据。该方法会对服务器发起远程调用。 |
public List partitionsFor(String topic) | 获取指定主题的分区元数据。如果当前消费者没有关于该主题的元数据,就会对服务器发 明 起远程调用。 |
public Map<TopicPartition, Long> beginningOffsets(Collection partitions) | 对于给定的主题分区,列出它们第一个消息的偏移量。 注意,如果指定的分区不存在,该方法可能会永远阻塞。 该方法不改变分区的当前消费者偏移量。 |
public void seekToEnd(Collection partitions) | 将偏移量移动到每个给定分区的最后一个。 该方法延迟执行,只有当调用过poll方法或position方法之后才可以使用。 如果没有指定分区,则将当前消费者分配的所有分区的消费者偏移量移动到最后。 如果设置了隔离级别为:isolation.level=read_committed,则会将分区的消费偏移量移动 到最后一个稳定的偏移量,即下一个要消费的消息现在还是未提交状态的事务消息。 |
public void seek(TopicPartition partition, long offset) | 将给定主题分区的消费偏移量移动到指定的偏移量,即当前消费者下一条要消费的消息偏 移量。若该方法多次调用,则最后一次的覆盖前面的。 如果在消费中间随意使用,可能会丢失数据。 |
public long position(TopicPartition partition) | 检查指定主题分区的消费偏移量 |
public void seekToBeginning(Collection partitions) | 将给定每个分区的消费者偏移量移动到它们的起始偏移量。该方法懒执行,只有当调用过 poll方法或position方法之后才会执行。如果没有提供分区,则将所有分配给当前消费者的 分区消费偏移量移动到起始偏移量。 |
8. 消费者拦截器
消费者在拉取了分区消息之后,要首先经过反序列化器对key和value进行反序列化处理。
处理完之后,如果消费端设置了拦截器,则需要经过拦截器的处理之后,才能返回给消费者应用程 序进行处理。
消费端定义消息拦截器,需要实现org.apache.kafka.clients.consumer.ConsumerInterceptor<K, V> 接口。
- 一个可插拔接口,允许拦截甚至更改消费者接收到的消息。首要的用例在于将第三方组件引入 消费者应用程序,用于定制的监控、日志处理等。
- 该接口的实现类通过configre方法获取消费者配置的属性,如果消费者配置中没有指定 clientID,还可以获取KafkaConsumer生成的clientId。获取的这个配置是跟其他拦截器共享 的,需要保证不会在各个拦截器之间产生冲突。
- ConsumerInterceptor方法抛出的异常会被捕获、记录,但是不会向下传播。如果用户配置了 错误的key或value类型参数,消费者不会抛出异常,而仅仅是记录下来。
- ConsumerInterceptor回调发生在 org.apache.kafka.clients.consumer.KafkaConsumer#poll(long)方法同一个线程。
public class OneInterceptor implements ConsumerInterceptor<String, String> {
@Override
public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
// poll方法返回结果之前最后要调用的方法
System.out.println("One -- 开始");
// 消息不做处理,直接返回
return records;
}
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
// 消费者提交偏移量的时候,经过该方法
System.out.println("One -- 结束");
}
@Override
public void close() {
// 用于关闭该拦截器用到的资源,如打开的文件,连接的数据库等
}
@Override
public void configure(Map<String, ?> configs) {
// 用于获取消费者的设置参数
configs.forEach((k, v) -> {
System.out.println(k + "\t" + v);
});
}
}
public class TwoInterceptor implements ConsumerInterceptor<String, String> {
@Override
public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
// poll方法返回结果之前最后要调用的方法
System.out.println("Two -- 开始");
// 消息不做处理,直接返回
return records;
}
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
// 消费者提交偏移量的时候,经过该方法
System.out.println("Two -- 结束");
}
@Override
public void close() {
// 用于关闭该拦截器用到的资源,如打开的文件,连接的数据库等
}
@Override
public void configure(Map<String, ?> configs) {
// 用于获取消费者的设置参数
configs.forEach((k, v) -> {
System.out.println(k + "\t" + v);
});
}
}
public class MyConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "mygrp");
// props.setProperty(ConsumerConfig.CLIENT_ID_CONFIG, "myclient");
// 如果在kafka中找不到当前消费者的偏移量,则设置为最旧的
props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 配置拦截器
// One -> Two 接收消息和发送偏移量确认都是这个顺序
props.setProperty(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG,
"com.lagou.kafka.demo.interceptor.OneInterceptor" +
",com.lagou.kafka.demo.interceptor.TwoInterceptor"
);
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
// 订阅主题
consumer.subscribe(Collections.singleton("tp_demo_01"));
while (true) {
final ConsumerRecords<String, String> records = consumer.poll(3_000);
records.forEach(record -> {
System.out.println(record.topic()
+ "\t" + record.partition()
+ "\t" + record.offset()
+ "\t" + record.key()
+ "\t" + record.value());
});
}
// consumer.close();
}
}
9. 消费组管理
9.1 消费者组特性
consumer group是kafka提供的可扩展且具有容错性的消费者机制。有如下三个特性:
- 消费组有一个或多个消费者,消费者可以是一个进程,也可以是一个线程
- group.id是一个字符串,唯一标识一个消费组
- 消费组订阅的主题每个分区只能分配给消费组一个消费者。
9.2 如何进行组内分区分配?
三种分配策略:RangeAssignor和RoundRobinAssignor以及StickyAssignor
9.3 谁来执行再均衡和消费组管理?
Kafka提供了一个角色:Group Coordinator来执行对于消费组的管理。
Group Coordinator——每个消费组分配一个消费组协调器用于组管理和位移管理。当消费组的第 一个消费者启动的时候,它会去和Kafka Broker确定谁是它们组的组协调器。之后该消费组内所有消费者和该组协调器协调通信。
9.4 如何确定coordinator?
- 确定消费组位移信息写入 __consumers_offsets 的哪个分区。具体计算公式:
__consumers_offsets partition# = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount) 注意:groupMetadataTopicPartitionCount 由 offsets.topic.num.partitions 指定,默认是50个分区。 - 该分区leader所在的broker就是组协调器。
9.5 Rebalance Generation
它表示Rebalance之后主题分区到消费组中消费者映射关系的一个版本,主要是用于保护消费组, 隔离无效偏移量提交的。如上一个版本的消费者无法提交位移到新版本的消费组中,因为映射关系变 了,你消费的或许已经不是原来的那个分区了。每次group进行Rebalance之后,Generation号都会加 1,表示消费组和分区的映射关系到了一个新版本,如下图所示: Generation 1时group有3个成员,随 后成员2退出组,消费组协调器触发Rebalance,消费组进入Generation 2,之后成员4加入,再次触发 Rebalance,消费组进入Generation 3.
9.6 协议(protocol)
kafka提供了5个协议来处理与消费组协调相关的问题:
- Heartbeat请求:consumer需要定期给组协调器发送心跳来表明自己还活着
- LeaveGroup请求:主动告诉组协调器我要离开消费组
- SyncGroup请求:消费组Leader把分配方案告诉组内所有成员
- JoinGroup请求:成员请求加入组
- DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息 等。通常该请求是给管理员使用
组协调器在再均衡的时候主要用到了前面4种请求。
9.7 liveness
消费者如何向消费组协调器证明自己还活着? 通过定时向消费组协调器发送Heartbeat请求。如果超过了设定的超时时间,那么协调器认为该消费者已经挂了。一旦协调器认为某个消费者挂了,那么它 就会开启新一轮再均衡,并且在当前其他消费者的心跳响应中添加“REBALANCE_IN_PROGRESS”,告诉 其他消费者:重新分配分区。
9.8 再均衡过程
再均衡分为2步:Join和Sync
- Join, 加入组。所有成员都向消费组协调器发送JoinGroup请求,请求加入消费组。一旦所有成员都发送了JoinGroup请求,协调器从中选择一个消费者担任Leader的角色,并把组成员信 息以及订阅信息发给Leader。
- Sync,Leader开始分配消费方案,即哪个消费者负责消费哪些主题的哪些分区。一旦完成分 配,Leader会将这个方案封装进SyncGroup请求中发给消费组协调器,非Leader也会发 SyncGroup请求,只是内容为空。消费组协调器接收到分配方案之后会把方案塞进SyncGroup 的response中发给各个消费者。
注意:在协调器收集到所有成员请求前,它会把已收到请求放入一个叫purgatory(炼狱)的地方。然 后是分发分配方案的过程,即SyncGroup请求
注意:消费组的分区分配方案在客户端执行。Kafka交给客户端可以有更好的灵活性。Kafka默认提 供三种分配策略:range和round-robin和sticky。可以通过消费者的参数:partition.assignment.strategy 来实现自己分配策略。
9.9 消费组状态机
消费组组协调器根据状态机对消费组做不同的处理:
- Dead:组内已经没有任何成员的最终状态,组的元数据也已经被组协调器移除了。这种状态 响应各种请求都是一个response: UNKNOWN_MEMBER_ID
- Empty:组内无成员,但是位移信息还没有过期。这种状态只能响应JoinGroup请求
- PreparingRebalance:组准备开启新的rebalance,等待成员加入
- AwaitingSync:正在等待leader consumer将分配方案传给各个成员
- Stable:再均衡完成,可以开始消费。