相对Producer来说,Consumer使用和设计类似,但更为复杂。因此将Consumer相关知识总结一番。顾名思义,consumer就是读取kafka集群中某些topic消息的应用程序。consumer有两个版本,老版本用Scala语言编写,其api包名为kafka.consumer.*, 分别提供high-level和low-level两种API,其缺点是用户必须自行实现错误处理和故障转移等功能,必须依赖zk记录消费的offset;新版本用Java语言编写,其api包名为org.apache.kafka.clients.consumer.*。本文主要讨论新版consumer的特性,同时对老版本相关特性进行一定讨论。首先,先贴出一段简单的kafka consumer代码
public static void main(String[] args){
String topicName = "test-topic";
String groupId = "test-group";
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092"); //必须指定
props.put("group.id", groupId);
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("auto.offset.reset", "earliest");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer consumer = new KafkaConsumer(props);
consumer.subscribe(Arrays.asList(topicName));
try {
while (true) {
ConsumerRecords records = consumer.poll(1000);
for(ConsumerRecord record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n",record.offset(),record.key(),record.value());
}
}
} finally {
consumer.close();
}
}
新版消费者有消费者组的概念,根据kafka官网的定义,若干个个消费者使用一个消费者组名(group.id)进行标记,topic的每条消息支付发送到每个订阅它的消费者组的一个消费者实例上。这句话展现了消费者组3个特点:
consumer group会将从topic中消费的数据公平的分配到组内的每一个consumer上,且具有实现高伸缩性,高容错性的consumer机制,一旦某个consumer挂掉,consumer group会立即将已崩溃的consumer负责的分区转交给其它consumer负责,从而保证group继续正常工作,不会丢失数据——这个过程就是consumer group的rebalance机制。
consumer group订阅topic列表很简单,下面两个语句都可以实现:
consumer.subscribe(Arrays.asList("topic-1", "topic-2"));
//基于正则表达式订阅
//如果用户不是自动提交offset,则需要实现ConsumerRebalanceListener指定分区分配方案变更时的逻辑
//自动提交则使用下面默认无操实现即可
consumer.subscribe(Pattern.compile("kafka-.*"), new NoOpConsumerRebalanceListener());
为了同时消费多个topic多个分区的消息,旧版本会为每个topic分区创建一个专门的线程去消费,新版本则采用类似于Linux I/O模型的poll或select机制,使用一个线程同时管理多个Socket连接。新版本Consumer从实现上来说是一个双线程的Java进程——用户主线程和后台心跳线程,且consumer不是线程安全的,因此每个consumer实例需要运行在专属线程中,以及显式的同步锁保护。最后要关闭consumer以清除各种Socket资源,并通知coordinator主动离组从而更快地开启新一轮rebalance。poll方法具有以下特性:
//定时返回的消费方式
ConsumerRecords records = consumer.poll(1000);
//获取足够数据返回的消费方式
try{
ConsumerRecords records = consumer.poll(Long.MAX_VALUE);
} catch (WakeupException e) {
//处理异常
}
consumer每次消费数据需要知道最新消费消息的位置,该位置称为位移(offset)。consumer需要定期向Kafka提交自己的位置信息,根据提交的策略不同会有3种不同的消息交付语义保证:
消息的位置信息包括消费者组中每个消费者上次提交位移、当前位置、水位等信息。老版consumer位置信息由zookeeper保存,新版consumer则是增加__consumeroffsets topic,将offset信息写入这个topic,摆脱存储位移对zookeeper的依赖。__consumer_offsets中的消息保存了每个consumer group某一时刻提交的offset信息,并且配置了compact策略,使得它总是能够保存最新的位移信息,既控制了该topic总体的日志容量,也能实现保存最新offset的目的其存储结构如下图
如前所述,位移的提交策略对与提供消息交付语义至关重要。新版consumer默认情况下自动提交,间隔5秒(老版60秒),其缺点是不能细粒度的提交位移,特别是有较强的精确一次处理语义时。使用手动提交需要设置enable.auto.commit=false,并调用commitSync或commitAsync方法。下面是一段典型的手动按照分区级别进行位移提交的示例代码:
try {
while(running) {
ConsumerRecords records = consumer.poll(1000);
for(TopicPartition partition :records.partitions()) {
List> partitionRecords = records.records(partition);
for(ConsumerRecord record : partitionRecords) {
//消息处理
}
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
}
}
} finally {
consumer.close();
}
新版consumer提供了三个设置消费API,分别为:
这里要注意的是,在seek前要使用subscribe()订阅topic或使用assign()分配topic的指定分区,否则就会出现 "java.lang.IllegalStateException: No current assignment for partition xxxxxx "的报错。另外由于subscribe()和assign()是“lazy”的,在seek前需要先"虚调用(dummy call)"poll()。对于subscribe()来说,下面的代码是一种更为有效的方式。
/** 开始消费 **/
Collection topics = Arrays.asList(topic);
consumer.subscribe(topics, new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection collection) {
}
@Override
public void onPartitionsAssigned(Collection collection) {
consumer.seekToEnd(collection);
}
});
consumer group 触发rebalance的条件有3个:
鉴于目前一次rebalance操作开销比较大,生产环境中要避免不必要的rebalance出现,一定要结合自身业务特点调优request.timeout.ms、max.poll.records、max.pll.interval.ms等参数。
新版本consumer提供了3种分区分配策略:range策略、round-robin策略和sticky策略。range策略将单个topic所有分区按照顺序排列,然后根据consumer个数,将其划分成固定大小的分区段后依次分配给每个consumer;round-robin则把所有topic的所有分区按顺序排列,然后轮询式的分配给每个consumer,默认分配策略是range。
为了隔离每次rebalance上的数据,新版consumer设计了rebalance generation用于标识某次rebalance,主要目的是为了保护consumer group,防止老的generation提交无效的offset。
新版Kafakrebalance流程是通过consumer与coordinator之间的一系列"交流"完成的,它提供了5个处理rebalance相关协议:
rebalance流程主要分成两步:加入组和同步更新方案: