java客户端连接操作kafka-消费者

1,165 阅读6分钟

重要概念

  • consumer: 消费者,负责订阅topic,并拉取消息
  • consumer Group: 消费组,每个消费者都会对应一个消费组;消费组里面会有消费者,被投递到topic里面的消息只会被每个消费组里面的一个消费组消费

image.png

换句话说,每个topic里面的每个分区,会被分配到消费组里面的某个消费者上;而发送消息最终是落到某个分区上的,所以会被某个消费组消费。
所以,不难猜测,我们可以通过增加消费组下面消费者的数量,来横向扩张消费能力;当然这是在分区数>消费者数的情况下;如果小于了,多余的消费者分不到分区,再扩张也没意义

  • 消费方式:kafka是基于发布-订阅的,是从topic中拉取消息的,而非接收

重要参数

  • bootstrap.servers
    用来指定kafka集群
  • group.id
    消费组名称,一定要设置;一般是要有标识性的,比如我们有多个服务共同消费一个tpic,可以使用对应服务名称来作为消费组名称,这样在监控kafka消费情况时,出现消费阻塞问题,可以快速确定
  • client.id
    消费组客户端id,不指定的话会系统自动生成
  • enable.auto.commit
    指定kafka消费是否自动提交;默认为true,开启后会自动定期提交位移,周期时间由客户端参数 auto.commit.interval.ms 配置,默认值为5秒
  • auto.offset.reset
    在 Kafka 中每当消费者查找不到所记录的消费位移时(比如使用新的消费组消费时,或者新的消费者进入时),就会根据消费者客户端参数 auto.offset.reset 的配置来决定从何处开始进行消费,这个参数的默认值为“latest”,表示从分区末尾开始消费消息。
  • ackMode
    顺便提下spring-ackmode,一般在Spring中搭配着使用
public enum AckMode {
        // 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
        RECORD,
        // 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
        BATCH,
        // 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
        TIME,
        // 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
        COUNT,
        // TIME | COUNT 有一个条件满足时提交
        COUNT_TIME,
        // 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
        MANUAL,
        // 手动调用Acknowledgment.acknowledge()后立即提交
        MANUAL_IMMEDIATE,
    }

简单案例

public static void main(String[] args) {
    Properties properties = new Properties();
    properties.put("key.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");
    properties.put("value.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");
    properties.put("bootstrap.servers", brokerList);
    properties.put("group.id", groupId);
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
    TopicPartition tp = new TopicPartition(topic, 0);
    consumer.assign(Arrays.asList(tp));
    //当前消费到的位移
    long lastConsumedOffset = -1;
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(1000);
        if (records.isEmpty()) {
            break;
        }
        List<ConsumerRecord<String, String>> partitionRecords
                = records.records(tp);
        lastConsumedOffset = partitionRecords
                .get(partitionRecords.size() - 1).offset();
        consumer.commitSync();//同步提交消费位移
    }
    System.out.println("comsumed offset is " + lastConsumedOffset);
    OffsetAndMetadata offsetAndMetadata = consumer.committed(tp);
    System.out.println("commited offset is " + offsetAndMetadata.offset());
    long posititon = consumer.position(tp);
    System.out.println("the offset of the next record is " + posititon);
}

常用api

  • subscribe()订阅主题
public void subscribe(Collection<String> topics, ConsumerRebalanceListener listener) 
public void subscribe(Collection<String> topics) 
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener) 
public void subscribe(Pattern pattern)

这是几个重载方法,可以看到支持多个topic,以及正则表达式;不过在实际使用中,正常合理的业务设计下,一般都是一个comsumer消费一个topic即可

  • assign()订阅分区
consumer.assign(Arrays.asList(new TopicPartition("topic-demo", 0)));
public List<PartitionInfo> partitionsFor(String topic)

可通过该方法直接指定订阅某分区;想要获取该topic的具体分区信息可以通过partitionsFor来获取

  • poll拉取消息
public ConsumerRecords<K, V> poll(final Duration timeout)
public List<ConsumerRecord<K, V>> records(TopicPartition partition)
public class ConsumerRecord<K, V> { 
    private final String topic; 
    private final int partition; 
    private final long offset; 
    private final long timestamp; 
    private final TimestampType timestampType; 
    private final int serializedKeySize; 
    private final int serializedValueSize; 
    private final Headers headers; 
    private final K key; 
    private final V value; 
    private volatile Long checksum; //省略若干方法
}

通过poll+records方法获取多条消息;在java客户端 一般是轮训去处理,可以指定timeout;有兴趣的去了解下spring-kafka实现,本质类似

  • commitSync()位移提交 有些重要的消息为了保证消费后的业务逻辑能保证执行,成功;需要关闭自动提交功能,在业务处理完成后再手动提交
  • seek()从指定位移开始消费 这个参数一般是要搭配,获取offset等方法使用,使用场景较少 有兴趣的了解下

扩展点

  • 再均衡器 当消费组里面添加或者删除消费者时,可能会触发分区的再均衡;这个时候可能因为存在为提交的位移,导致消息被重复消息;这个时候可以利用listener来在发生再均衡前主动提及位移
ConsumerRebalanceListener
1.  void onPartitionsRevoked(Collection partitions) 
这个方法会在再均衡开始之前和消费者停止读取消息之后被调用。
可以通过这个回调方法来处理消费位移的提交,以此来避免一些不必要的重复消费现象的发生。
参数 partitions 表示再均衡前所分配到的分区。
2.  void onPartitionsAssigned(Collection partitions) 
这个方法会在重新分配分区之后和消费者开始读取消费之前被调用。
参数 partitions 表示再均衡后所分配到的分区。
  • 拦截器
ConsumerInterceptor
public ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> records);
KafkaConsumer 会在 poll() 方法返回之前调用拦截器的 onConsume() 方法来对消息进行相应的定制化操作,
比如修改返回的消息内容、按照某种规则过滤消息(可能会减少 poll() 方法返回的消息的个数)。
如果 onConsume() 方法中抛出异常,那么会被捕获并记录到日志中,但是异常不会再向上传递。

public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets);
KafkaConsumer 会在提交完消费位移之后调用拦截器的 onCommit() 方法,可以使用这个方法来记录跟踪所提交的位移信息,
比如当消费者使用 commitSync 的无参方法时,我们不知道提交的消费位移的具体细节,而使用拦截器的 onCommit() 方法却可以做到这一点。

public void close()。

多线程消费

  • 方式一 为每个线程实例化一个 KafkaConsumer 对象,即对应一个KafkaConsumer实例;所有的消费现场属于一个消费组;显然:这个并发度是受限于分区数的(前面提到过,一个分区最多分给一个消费者,所以分区数>=消费者数)
    实现:new 多个KafkaConsumer即可
  • 方式二 打破原本的一个分区最多分给一个消费者,多个消费线程同时消费同一个分区,这个通过 assign()、seek() 等方法实现;不过这个实现起来比较复杂,风险大 不建议使用
  • 方式三 第一种,每一个KafkaConsumer实例相当于一个TCP连接,是比较耗费资源的;在考虑使用多线程的时候 一般都是为了提高消费能力,而需要这么做的原因无非是消费者代码里面牵扯了许多其他逻辑,导致消费速度慢,可以在消费处理这里使用多线程去做。(这个时候需要考虑下消费的顺序性)
    实现:在业务代码处,线程池去处理

后话

本文只是介绍了kafka消费者客户端的一些常用api方法、参数等,后续底层的一些原理,以及spring中对于kafka的实现再另外的章节介绍