赞
踩
应用程序使用 KafkaConsumer 向 kafka 订阅消息,并从订阅主题上接收消息。
如果有一个场景,生产者向主题写入消息的速度超过单个消费者的速度,应用程序会更不上消费生成的速度。显然此刻需要对消费者进行横向伸缩。因此我们可以使用多个消费者从同个主题读取数据,对消息进行分流。
Kafka 消费者从属于一个消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。
假设主题有4个分区,我们只创建一个消费者群组,而这个群组只有唯一的消费者,因此消费者C1将收到主题T1的所有分区的消息。
如果G1里新增一个消费者C2,那么每个消费者将从两个分区接收消息。
如果群组G1有四个消费者,那么每个消费者可以分配到一个分区。
当然,如果我们往群组里添加了更多的消费者,超过主题的分区数量,那么有一部分消费者会 闲置 ,不会接收到任何消息。
因为,Kafka消费者有时经常会做一些 高延迟 的操作,比如将接收的数据写到HDFS或数据库,或者使用数据进行比较耗时的计算。
因此往群组里增加消费者是 横向伸缩 消费能力的主要方式。
除了增加消费者横向伸缩单个应用程序外,我们还可以增加消费者群组,虽然两个群组获取相同主题的消息,但是G1与G2之间是互不影响的,重要的一点是横向伸缩并不会对性能造成负面影响。
一个新的消费者加入群组,那么它读取的是原本其他消费者读取的消息。
一个消费者因为被关闭或者发生崩溃时,退出群组,原本它读取的消息将被群组里其他的消费者读取。
当一个消费者的分区所有权转移到另外一个消费者的时候,这种行为被称为 再均衡。它为群组带来的高可用和伸缩性。
但正常情况下,不希望这样的行为发生,因为在 再均衡 期间,消费者无法读取消息,造成整个群组一小段消息无法被消费(不可用)。另外,当分区被重新分配给另外一个消费者时,消费者当前的读取状态会丢失,有可能还需要去刷新缓存。
消费者会向被指派为 群组协调器的broker (不同群组有着不同的协调器) 发送心跳来维持它们和群组的从属关系及它们对分区的所有权的关系(消费者是否崩溃活跃)。
只要消费者正常的时间间隔发送心跳,就被认为是活跃的,即说明它还在读取分区中的消息。一般会在轮询消息和提交偏移量发送心跳(新版本可独立发送心跳)。
如果消费者停止发送心跳时间足够长,会话就会过期,群组协调器的broker就会认为它已经死亡,就会触发一次再均衡。
相比较创建生产者,消费者仍然是三个参数:bootstrap.servers,key.deserializer,value.deserializer
//给消费者配置信息 Properties properties = new Properties(); //链接集群 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092"); //开启自动提交 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true); //自动提交Offset信息时间间隔 properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000"); //Key,Value 反序列化 properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); //配置消费者组 properties.put(ConsumerConfig.GROUP_ID_CONFIG,"bigdata"); //重置消费者的offset //1.未初始化 2.消息丢失 3.earliest:从最早的一条消息开始offset properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); //创建消费者 KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);
创建好消费者之后,下一步就是订阅主题了。
subscribe()方法接受了一个主题列表作为参数。
consumer.subscribe(Collections.singletonList("BigData"));
此时我们创建了一个只包含单个元素的列表,主题叫做"BigData"
我们也可以在调用subscribe()方法时传入一个正则表达式,因此可以匹配到多个主题,如果有人创建了新的主题并且匹配,则消费者就会触发一次 再均衡,消费者就会读取新的主题。
通常是在Kafka和其他系统之间复制数据时,使用正则表达式来订阅多个主题。
如果我们要订阅所有与 user 相关的主题,就可以这样做:
consumer.subscribe("user.*");
消息轮询是消费者API的核心,通过一个简单的轮询向服务器请求数据。
一旦消费者订阅了主题,
轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据。
while (true){
//获取数据
ConsumerRecords<String,String> consumerRecords = consumer.poll(100);
//订阅主题
for (ConsumerRecord<String,String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.key() + "---" + consumerRecord.value());
}
}
poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间(消费者缓冲区已经没有可用的数据就会发生阻塞)。如果该参数设为0,消费者如果没有消息可消费则立即返回。
poll()方法返回一个记录列表。每条记录都包含着记录所属主题的信息、记录所在分区的信息、记录在分区里的偏移量,以及记录里的键值对。
轮询不只是获取数据那么简单。
在第一次调用新消费者的poll()方法时,它会负责查找 GroupCoordinator,然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。
在同一群组里,我们无法让一个线程运行着多个消费者,也无法让多个线程安全地共享一个消费者。
按照规则,一个消费者使用一个线程,最好是把消费者的逻辑封装在自己的对象里,使用Java的ExecutorSerivce启动多个线程。
fetch.min.bytes
fetch.max.wait.ms
max.partition.fetch.bytes
session.timeout.ms
auto.offset.reset
enable.auto.commit
partition.assignment.strategy
分区会分配给群组里的消费者。
PartitionAssignor(群主)根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者。Kafka有两个默认的分配策略。
Range
假定C1和C2同时订阅了T1和T2,而且每个主题都有3个分区。那么消费者C1有可能分配到这两个主题的分区0和分区1,而C2分配到这两个主题的分区2。
RoundRobin
此时C1将分到T1的分区0和分区2以及T2的分区1,C2将分配到主题T1的分区1以及T2的分区0和分区2。
max.poll.records
receive.buffer.bytes 和 send.buffer.bytes
Kafka不像其他的JMS队列那样需要得到消费者的确认,这是Kafka的一个独特之处。相反消费者可以使用Kafka来跟踪数据在分区里的位置(偏移量)。
我们把更新分区当前位置的操作叫做 提交 。
通常消费者都会往一个叫作 _consumer_offset 的特殊主题发送消息,而这个消息包含着每个分区的偏移量。如果此时消费者发生崩溃或者新的消费者加入群组,就会触发再均衡。因此消费者为了能够继续原来的工作,消费者就需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。
如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理。
如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于这两个偏移量之间的消息将会被丢失。
最简单的方式就是让消费者自动提交偏移量,把 enable.auto.commit = true ,那么没过5s,消费者就会自动把poll()所接收的最大偏移量提交上去。
当然也有缺陷,
如果我们默认5s提交时间间隔,在最近一次提交之后的3s发生了再平衡,再平衡后,消费者就会从最后一次提交的偏移量开始读取消息,但这时候的偏移量就已经落后了3s,因此这3s内到达的消息就会被重复处理。
而这种情况是无法避免的。
为了能够控制偏移量提交的时间来消除丢失消息的可能性,并在发生再均衡时减少重复消息的数量。
消费者API就提供了偏移量的方式,我们可以在必要的情况下提交当前的偏移量,而不是基于时间间隔提交。
因此 auto.commit.offset = false ,让应用程序决定何时提交偏移量。
使用 commitSync() 提交偏移量最简单也是最可靠的。
while (true){ //获取数据 ConsumerRecords<String,String> consumerRecords = consumer.poll(100); //订阅主题 for (ConsumerRecord<String,String> consumerRecord : consumerRecords) { System.out.println(consumerRecord.key() + "---" + consumerRecord.value()); //if (Integer.parseInt(consumerRecord.value()) > 20){ // } } try{ //处理完一批记录立即提交 consumer.commitSync(); }catch (CommitFailedException e){ //提交失败 e.printStackTrace(); } }
commitSync()将会提交有poll()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitSync() ,否则还是会有丢失消息或重复消费的风险。
同步提交有一个不足之处,在broker对提交请求做出回应之前,应用程序会一直阻塞,而这样会限制应用程序的吞吐量。
这时候就可以使用 异步提交API 。
我们只管发送提交请求,无需等待broker的响应。
consumer.commitAsync();
异步提交的好处是,在成功提交或者碰到了无法恢复的错误,commitSync()会一直重试,而commitAsync()不会,当然这也是一个坏处,也容易出现重复消费。
commitAsync()也支持回调,在broker做出响应时会执行回调。回调经常被用于记录提交错误。
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
e.printStackTrace();
}
});
发送提交请求然后继续做其他事情,如果提交失败,错误信息和偏移量会被记录下来。
不过如果你要用它来进行重试,一定注意提交的顺序。
我们可以使用一个单调递增的序列号来维护异步提交的顺序。
在每次提交偏移量之后或者在回调里提交偏移量时递增序列号。
在重试前,
先检查回调的序列号和即将提交的偏移量是否相等,
如果相等,说明没有新的提交,那么可以安全地进行重试。
如果序列号比较大,说明已经有一个新的提交已经发送出去了,应该停止重试。
为了这是发生在关闭消费者或再均衡之前的最后一次提交,就能确保能够提交成功。
因此,在消费者关闭前一般会使用 commitSync() 和 commitAsync()。
try { while (true){ //获取数据 ConsumerRecords<String,String> consumerRecords = consumer.poll(100); //订阅主题 for (ConsumerRecord<String,String> consumerRecord : consumerRecords) { System.out.println(consumerRecord.key() + "---" + consumerRecord.value()); } //1 consumer.commitAsync(); } }catch (Exception e) { e.printStackTrace(); }finally { try { //2 consumer.commitSync(); }finally { consumer.close(); } }
1:如果一切正常,我们使用commitAsync()方法来提交,这样的速度更快,而且即使这次提交失败,下次提交很可能成功。
2:如果直接关闭消费者,就没有所谓的"下一次提交"了。使用commitSync()方法会一直重试,直至提交成功或者发生无法恢复的错误。
AsWeAllKnown,提交偏移量的频率与处理消息批次的频率是一样的。
如果想更频繁的提交偏移量可以通过提交特定的偏移量来实现。
像生产者的序列化器一样,仍然推荐使用 Avro 来进行序列化和反序列化消息。
我会另外写一篇如何将 Avro 和 Kafka 集成的文章。
有时候,我们的场景很简单,
我们只需要一个消费者从一个主题的所有分区或者特定分区读取数据。
这时候就不再需要消费群组和再均衡了,
只需要把主题或者分区分配给消费者,然后开始读取消息并提交偏移量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。