03-Kafka消费者

Kafka消费者

Kafka系列文章是基于:深入理解Kafka:核心设计与实践原理一书,结合自己的部分实践和总结。

一、消费者与消费者组

  • 消费者一定属于且只属于一个消费者分组,分组通过配置项group.id指定,其默认值为null。
  • Topic被一个消费者组订阅时,每一条消息只能被其中一个消费者消费,更准确的说应该是该Topic下的每一个分区都只能被该组的一个消费者消费。
  • 消费者组之间互不影响干扰,Topic下的分区和消费者分组内的消费者之间的分配关系在增减消费者或者分区时会重新分配,这个过程称为再均衡。

二、客户端开发

2.1 必要参数

参数含义
bootstrap.serversKafka的Broker地址
key.deserializerkey的反序列化器
value.deserializervalue的反序列化器
group.id消费者分组Id

2.2 主题与分区

2.2.1 订阅主题
  • subscribe包括几个重载方法,可以订阅指定主题,也可以按照正则表达式订阅指定类型的主题。
//1.订阅指定主题
subscribe(Collection<String> topics)

//2.订阅指定类型topic,使用正则表达式,ConsumerRebalanceListener参数是再均衡监听器
subscribe(Pattern pattern, ConsumerRebalanceListener callback);

//3.指定再均衡监听器
subscribe(Collection<String> topics, ConsumerRebalanceListener listener) 
2.2.2 订阅分区
  • assign方法可以订阅Topic的指定分区
//assign方法可以订阅Topic的指定分区,参数TopicPartition只包含2个属性,分别是Topic和对应分区编号
assign(Collection<TopicPartition> partitions)

public final class TopicPartition implements Serializable {

    private int hash = 0;
    private final int partition;
    private final String topic;
}
2.2.3 获取分区信息
  • partitionsFor方法可以获取分区信息
//如果订阅某些分区,需要先知道有哪些分区,通过partitionsFor方法获取分区信息
//KafkaConsumer#partitionsFor
public List<PartitionInfo> partitionsFor(String topic)

//PartitionInfo包含topic的相关分区信息
public class PartitionInfo {

    private final String topic;//主题
    private final int partition;//分区编号
    private final Node leader;//leader副本所在节点
    private final Node[] replicas;//分区的AR,即所有副本集合
    private final Node[] inSyncReplicas;//分区的ISR集合

}
  • 订阅指定分区
//如下代码我们可以通过查询得到的分区信息来订阅某些主题,下面我们订阅分区号为偶数的全部分区
    List<TopicPartition> topicPartitionList = new ArrayList<>();
    List<PartitionInfo> partitionInfoList = consumer.partitionsFor("topic");//获取topic全部分区
        for (PartitionInfo pi : partitionInfoList) {
            if (pi.partition() % 2 == 0) {
            topicPartitionList.add(new TopicPartition(pi.topic(), pi.partition()));//添加偶数分区到集合
        }
    }
consumer.assign(topicPartitionList);//订阅分区号为偶数的分区
2.2.4 取消订阅
  • 订阅状态
    //SubscriptionState.SubscriptionType
    private enum SubscriptionType {
        NONE, AUTO_TOPICS, AUTO_PATTERN, USER_ASSIGNED
    }
PS: 前面提到了多种订阅方式,subscribe(Collection<String> topics)subscribe(Pattern pattern)或者assign(Collection<TopicPartition> partitions),分别是订阅主题,正则匹配订阅和订阅分区,对应着枚举SubscriptionType中的三种状态,这三种状态是互斥的,消费者只能使用其中一种方式,如果没有订阅则为NONE。 
  • 取消订阅;KafkaConsumer#unsubscribe,可以取消前面三种方式所订阅的主题或者分区
/**
 * Unsubscribe from topics currently subscribed with {@link #subscribe(Collection)}. This
 * also clears any partitions directly assigned through {@link #assign(Collection)}.
*/
unsubscribe();

2.3 反序列化

  • 反序列化器的接口是Deserializer,功能是将字节数组转换为Java类型,和序列化器的功能相对应。反序列化器相关配置如下:
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

public static final String KEY_DESERIALIZER_CLASS_CONFIG = "key.deserializer";
public static final String VALUE_DESERIALIZER_CLASS_CONFIG = "value.deserializer";
  • 反序列化器和序列化器有很多相似之处,就不在多写,相关内容可以参考:02-Kafka生产者的序列化器部分

2.4 消费

2.4.1 消费模式
  • Kafka消费者的消费是基于拉模式(pull)
PS:消息消费一般分为2种模式,推模式和拉模式,前者是服务端主动将消息推动给消费者,后者是消费者主动向服务端拉取消息
2.4.2 poll
  • poll是消费者拉去消息的方法。
//通过超时参数指定,在指定时间内没有拉去消息则返回空,timeout为0表示立即返回
KafkaConsumer#ConsumerRecords<K, V> poll(long timeout)
  • ConsumerRecords:消费者拉取到的消息记录集合
ConsumerRecords是消费者拉去到的消息记录集合,因为消费者可能订阅了多个主题,因此ConsumerRecords还提供了很多获取消息的方法

//1.直接迭代消息
for (ConsumerRecord<String, String> record : consumerRecords) {
    System.out.println("Record:" + record.value());
}

//2.获取指定分区消息,可以让我们按照分区的维度处理消息
ConsumerRecords#records(TopicPartition partition);
ConsumerRecords#partitions()可以让我们获取全部的分区信息,然后按照分区的维度获取消息


//3.获取指定主题的消息,可以让我们按照主题的维度处理消息
ConsumerRecords#records(String topic)

方法23我们可以先获取全部的分区或者主题信息,然后遍历处理,便于我们对部分的分区或者主题信息进行不同的处理。
  • ConsumerRecord:每条消息记录,和ProducerRecord对应但是前者信息更加丰富,
public class ConsumerRecord<K, V> {
    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
    public static final int NULL_SIZE = -1;
    public static final int NULL_CHECKSUM = -1;

    private final String topic;//消息所属主题
    private final int partition;//消息对应分区
    private final long offset;//消息偏移量
    private final long timestamp;//时间戳
    private final TimestampType timestampType;//时间戳类型
    private final int serializedKeySize;//序列化之后key的大小
    private final int serializedValueSize;//序列化之后value大小
    private final Headers headers;
    private final K key;
    private final V value;
    private volatile Long checksum;//校验和
}

2.5 提交偏移量

2.5.1 关于偏移量
  • 假设某个分区有消息若干,假设消费者消费了offset为5的消息并提交偏移量,那么有以下几个概念
lastConsumedOffset:当前消费到的位置,为5
committed offset:消费者已经提交过的消费位移,为6 (如果消费了offset为5的消息再提交,那么提交的不是5而是6)
position:下一次拉去消息的位置,为6
  • 相关Api:
//获取指定消息的offset,获取消费到的最后一条消息的offset就是lastConsumedOffset
ConsumerRecord#offset();
//提交分区的消费位移,提交后通过OffsetAndMetadata.offset()获取committed offset
KafkaConsumer#public OffsetAndMetadata committed(TopicPartition partition);
//获取position,即获取下一次拉去消息的位置
KafkaConsumer#public long position(TopicPartition partition);
2.5.2 自动提交
  • 提交时机:Kafka有自动提交机制并且默认开启。但是在启用自动提交的时候有重复消费和消息丢失的可能,比如批量获取5条,如果消费完再提交那么中途故障,则前部分消息会重复消费;如果拉取到就提交,那么中途故障会导致后半部分消息丢失。
//客户端默认是自动提交,通过下面配置开启
enable.auto.commit:true/false , 默认为true
//在开启了自动提交的前提下,提交周期是
auto.commit.interval.ms,

properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
public static final String ENABLE_AUTO_COMMIT_CONFIG = "enable.auto.commit";
2.5.3 手动提交
  • 自动提交编码更加简单,但是对与业务场景较为复杂并且要求严格控制消息重复消费和消息丢失的时候,手动提交更受控制,手动提交的前提是auto.commit.interval.ms为fasle,手动提交又分为同步和异步的方式。
2.5.3.1 同步
  • KafkaConsumer#commitSync(): 该方法提交的频率和拉取消息的频率一致
  • KafkaConsumer#commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets): 用于提交指定分区的offsets,可以按照分区的粒度消费再提交。
PS: 同步提交的方式会阻塞直到提交成功为止,因此会对消费者线程有一定的阻塞
2.5.3.2 异步
  • 异步提交不会阻塞消费者线程,可能提交未成功就拉取到新的一批消息,对比同步提交性能有一定的提示
KafkaConsumer#commitAsync()
KafkaConsumer#commitAsync(OffsetCommitCallback callback) 
KafkaConsumer#commitAsync(final Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)

PS: 异步提交中的offsets和同步提交意义类似,不过它还支持一个callback的回调方式,提交成功之后会调用OffsetCommitCallback的onComplete方法,
DefaultOffsetCommitCallback是源码中一个实现类,只打印了日志。

2.6 控制和关闭消费

2.6.1 消费控制
  • 消费控制可以让我们在特定的情况下暂停或者再次恢复,对应方法如下:
//暂停某些分区的消费
KafkaConsumer#pause(Collection<TopicPartition> partitions); 
//恢复某些分区的消费
KafkaConsumer#resume(Collection<TopicPartition> partitions);
//获取被暂停消费的分区的集合
KafkaConsumer#pause(Collection<TopicPartition> partitions); 
2.6.2 退出消费
  • KafkaConsumer是非线程安全的,但是其提供了一个线程安全的方法来停止消费。
//退出消费,注意通过finally保证资源释放
KafkaConsumer#wakeup()

2.7 指定位移消费

2.7.1 auto.offset.reset
  • auto.offset.reset: 当Kafka中找不到消费者保存的偏移量时(数据丢失,或者首次加入消费),会根据该配置决定从何处开始消费。
auto.offset.reset:latest/earliest, 默认值latest,表示从分区尾部开始消费,即从下一条将要写入的消息处开始,earliest表示从头开始消费(offset为0开始)
  • auto.offset.reset触发时机:该配置在设置之后,并不是每次启动消费者都会触发该配置的效果,只有在找不到offset,或者位移越界的时候才会触发,比如seek到一个找不到的offset。
PS: 因为Kafka会定时清理消息,因此不代表最大offset之前的offset对应的消息都还保存着,如果找不到则是位移越界,会触发auto.offset.reset配置的效果(从前或者从后开始消费)
2.7.2 seek
  • 如果我们不想像前面那样从头或者从尾开始消费,而需要一种更加精确的控制,seek方法让我们可以从特定位移处开始消费。
//seek方法设置在指定分区的指定位置开始消费
KafkaConsumer#seek(TopicPartition partition, long offset);

//获取消费者分配的分区
KafkaConsumer#Set<TopicPartition> assignment()

PS:seek只能对消费者分配到的分区设置,而分配分区的逻辑在pool中执行,因此先执行pool,然后获取分配到的分区,再执行seek。
另外,如果消费者启动之后能够找到offset,那么auto.offset.reset机制不会触发,此时可以手动seek到指定位置开始消费
  • seek提供了更加灵活的消费方式,支持向前跳跃和向后回溯。配合seek机制,我们可以将Kafka的offset保存在另一个地方,比如数据库,然后消费的时候读取并seek到指定位置,再配合均衡监听器会更加灵活。
2.7.3 其他
  • 我们通过seek来消费指定的偏移处的消息,有时候我们期望从保存的消息的最小offset处(未必是0,offset是0的消息有可能已经被清除了),或者最大offset处,或者以前某个时刻的消息处开始消费,下面几个方法可以辅助seek方法的:
//1.获取分区起始位置,
public Map<TopicPartition, Long> beginningOffsets(Collection<TopicPartition> partitions);

//2.获取分区末尾,从末尾处开始消费。
public Map<TopicPartition, Long> endOffsets(Collection<TopicPartition> partitions);

//3.直接从分区起始开始消费
seekToBeginning(Collection<TopicPartition> partitions);

//4.直接从分区末尾开始消费
seekToEnd(Collection<TopicPartition> partitions);

//5.从指定时间处消息开始消费
//该方法会返回大于或者等于Map中对应分区的时间戳之后的第一条消息的offset
public Map<TopicPartition, OffsetAndTimestamp> offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch);

2.8 再均衡

2.8.1 描述
  • 再均衡是一个分区从一个消费者转移到另一个消费者的行为,他是消费者高可用和伸缩性的保证基础,但是该发生期间可能有小段时间消费者组不可用,另外消费者状态也有可能丢失(主要是offset),可能导致重复消费和消息丢失,通常情况应该尽量避免。
2.8.1 ConsumerRebalanceListener
  • ConsumerRebalanceListener是再均衡监听器,可以让消费者客户端在发生再均衡动作前后执行一些操作。
public interface ConsumerRebalanceListener {

    /**
     * 该方法会在再均衡发生之前,消费者停止读取消息之后调用,可以在该方法内部处理消费位移的提交,避免重复消费,参数是再均衡之前所分配的分区  
     * 在该方法内部可以查询所分配的每个分区的偏移量,便于处理
     * @param partitions The list of partitions that were assigned to the consumer on the last rebalance
     */
    void onPartitionsRevoked(Collection<TopicPartition> partitions);

    /**
     * 该方法会在再均衡发生之后,消费者开始消费之前调用,参数是再均衡之后所分配的分区
     * @param partitions The list of partitions that are now assigned to the consumer (may include partitions previously
     *            assigned to the consumer)
     */
    void onPartitionsAssigned(Collection<TopicPartition> partitions);
}

2.9 拦截器

  • 消费者拦截器和生产者的拦截器类似,拦截器会在poll方法返回之前调用,我们得到的就是拦截器处理完之后的消息结果。ConsumerInterceptor是消费者拦截器接口
public interface ConsumerInterceptor<K, V> extends Configurable {

    /**
     * 对消息进行处理
     * @param records records to be consumed by the client or records returned by the previous interceptors in the list.
     * @return records that are either modified by the interceptor or same as records passed to this method.
     */
    public ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> records);

    /**
     * 提交offset完毕之后,会调用拦截器的onCommit方法
     * This is called when offsets get committed.
     * <p>
     * Any exception thrown by this method will be ignored by the caller.
     *
     * @param offsets A map of offsets by partition with associated metadata
     */
    public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets);

    /**
     * 关闭
     * This is called when interceptor is closed
     */
    public void close();
}
  • 配置
    properties.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyConsumerInterceptor.class.getName());
    public static final String INTERCEPTOR_CLASSES_CONFIG = "interceptor.classes";

2.10 多线程实现消费者

  • KafkaProducer是线程安全的,但是KafakConsumer是非线程安全的,KafkaConsumer中执行公用方法之前都需要acquire(),执行完之后release(),只有wakeup()是线程安全的。不过即使KafkaConsumer不是线程安全的,但是我们是可以使用多线程消费来加快消费速度的。
2.10.1 线程封闭
  • 是一种常见的多线程实现方式。该方式下每一个线程实例化一个KafkaConsumer,每一个线程消费一个或者多个分区,不过该模式下最多只能扩展到和分区数相同个数的消费者线程,当线程数 > 分区数,那么部分线程实际上不能消费到消息。
2.10.2 多线程消费单个分区
  • 这种方式可以在线程封闭的方式基础上扩展单个分区的消费能力,可以多个线程消费一个分区,通过assign和seek来实现,不过该模式下相关的偏移量等处理也比较复杂。一般而言分区是消费线程的最小划分单位,该方案并不推荐。
2.10.3 poll和处理分离
  • 第一种方式是推荐的方式,但如果该模式下消息的处理过程比较慢,即使poll很快那么处理也会成为瓶颈,另外如果有N个分区创建N个线程,那么就有N个TCP连接,这个开销是比较大的。
  • 在本方案中使用单个KafkaConsumer拉取消息,然后使用并行方式处理消息(比较典型的比如线程池),不过该方式下也会带来offset处理上的困难,如果没有遇到瓶颈,分区数量也不多,推荐使用方式1。

2.11 重要参数

参数作用
max.poll.interval.ms默认30000ms,超过该时长没有poll,消费者组认为该消费者异常,将进行再均衡
auto.offset.reset默认latest
enable.auto.commit默认true
auto.commit.interval.msenable.auto.commit为true时自动提交间隔,默认5000ms
fetch.min.bytes消费者一次请求时最小的消息字节数,默认1
fetch.max.bytes消费者一次请求时最大的消息字节数
fetch.wait.max.ms如果没有足够的数据server阻塞等待足够的数据的最大阻塞时间,默认100ms
max.partition.fetch.bytes消费者一次请求时,每个分区返回的最大消息字节数
max.poll.records消费者一次拉取最大消息条数
connections.max.idle.ms连接闲置后关闭时间
receive.buffer.bytesSocket接收缓冲区大小
send.buffer.bytesSocket发送缓冲区大小
request.timeout.ms生产者等待请求的最大时长
metadata.max.age.ms元数据过期时间,默认5min
reconnect.backoffms默认50ms,重试连接的等待时间
retry.backoff.ms默认100ms,消息发送失败后重试的间隔时间
isolation.level事物隔离级别

三、参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值