Kafka消费者
Kafka系列文章是基于:深入理解Kafka:核心设计与实践原理一书,结合自己的部分实践和总结。
一、消费者与消费者组
消费者一定属于且只属于一个消费者分组,分组通过配置项group.id指定,其默认值为null。 Topic被一个消费者组订阅时,每一条消息只能被其中一个消费者消费,更准确的说应该是该Topic下的每一个分区都只能被该组的一个消费者消费。 消费者组之间互不影响干扰,Topic下的分区和消费者分组内的消费者之间的分配关系在增减消费者或者分区时会重新分配,这个过程称为再均衡。
二、客户端开发
2.1 必要参数
参数 含义 bootstrap.servers Kafka的Broker地址 key.deserializer key的反序列化器 value.deserializer value的反序列化器 group.id 消费者分组Id
2.2 主题与分区
2.2.1 订阅主题
subscribe包括几个重载方法,可以订阅指定主题,也可以按照正则表达式订阅指定类型的主题。
subscribe ( Collection< String> topics)
subscribe ( Pattern pattern, ConsumerRebalanceListener callback) ;
subscribe ( Collection< String> topics, ConsumerRebalanceListener listener)
2.2.2 订阅分区
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 获取分区信息
public List< PartitionInfo> partitionsFor ( String topic)
public class PartitionInfo {
private final String topic;
private final int partition;
private final Node leader;
private final Node[ ] replicas;
private final Node[ ] inSyncReplicas;
}
List< TopicPartition> topicPartitionList = new ArrayList < > ( ) ;
List< PartitionInfo> partitionInfoList = consumer. partitionsFor ( "topic" ) ;
for ( PartitionInfo pi : partitionInfoList) {
if ( pi. partition ( ) % 2 == 0 ) {
topicPartitionList. add ( new TopicPartition ( pi. topic ( ) , pi. partition ( ) ) ) ;
}
}
consumer. assign ( topicPartitionList) ;
2.2.4 取消订阅
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 ( ) ;
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 消费模式
PS:消息消费一般分为2种模式,推模式和拉模式,前者是服务端主动将消息推动给消费者,后者是消费者主动向服务端拉取消息
2.4.2 poll
KafkaConsumer#ConsumerRecords< K, V> poll ( long timeout)
ConsumerRecords:消费者拉取到的消息记录集合
ConsumerRecords是消费者拉去到的消息记录集合,因为消费者可能订阅了多个主题,因此ConsumerRecords还提供了很多获取消息的方法
for ( ConsumerRecord< String, String> record : consumerRecords) {
System. out. println ( "Record:" + record. value ( ) ) ;
}
ConsumerRecords#records ( TopicPartition partition) ;
ConsumerRecords#partitions ( ) 可以让我们获取全部的分区信息,然后按照分区的维度获取消息
ConsumerRecords#records ( String topic)
方法2 和3 我们可以先获取全部的分区或者主题信息,然后遍历处理,便于我们对部分的分区或者主题信息进行不同的处理。
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;
private final int serializedValueSize;
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
ConsumerRecord#offset ( ) ;
KafkaConsumer#public OffsetAndMetadata committed ( TopicPartition partition) ;
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是非线程安全的,但是其提供了一个线程安全的方法来停止消费。
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方法让我们可以从特定位移处开始消费。
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方法的:
public Map< TopicPartition, Long> beginningOffsets ( Collection< TopicPartition> partitions) ;
public Map< TopicPartition, Long> endOffsets ( Collection< TopicPartition> partitions) ;
seekToBeginning ( Collection< TopicPartition> partitions) ;
seekToEnd ( Collection< TopicPartition> partitions) ;
public Map< TopicPartition, OffsetAndTimestamp> offsetsForTimes ( Map< TopicPartition, Long> timestampsToSearch) ;
2.8 再均衡
2.8.1 描述
再均衡是一个分区从一个消费者转移到另一个消费者的行为,他是消费者高可用和伸缩性的保证基础,但是该发生期间可能有小段时间消费者组不可用,另外消费者状态也有可能丢失(主要是offset),可能导致重复消费和消息丢失,通常情况应该尽量避免。
2.8.1 ConsumerRebalanceListener
ConsumerRebalanceListener是再均衡监听器,可以让消费者客户端在发生再均衡动作前后执行一些操作。
public interface ConsumerRebalanceListener {
void onPartitionsRevoked ( Collection< TopicPartition> partitions) ;
void onPartitionsAssigned ( Collection< TopicPartition> partitions) ;
}
2.9 拦截器
消费者拦截器和生产者的拦截器类似,拦截器会在poll方法返回之前调用,我们得到的就是拦截器处理完之后的消息结果。ConsumerInterceptor是消费者拦截器接口
public interface ConsumerInterceptor < K, V> extends Configurable {
public ConsumerRecords< K, V> onConsume ( ConsumerRecords< K, V> records) ;
public void onCommit ( Map< TopicPartition, OffsetAndMetadata> offsets) ;
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 重要参数
三、参考