kafka 通过消费者获取__consumer_offsets topic的元数据内容
工作中遇到一个问题需要获取kafka的元数据信息,诸如topic创建信息,消费者消费topic的信息等。要获取kafka的元数据信息,首先想到找zookeeper,利用zookeeper的watcher机制去监听kafka的元数据节点的创建,进而拿到对应信息。但由于kafka新版本存在两种消费者元数据保存机制,因此我们不能只考虑获取zookeeper上的元数据。
kafka 消费者offset存储方式
在kafka 0.9 版本开始提供了新的consumer及consumer group,位移的管理与保存机制发生了很大的变化,新版本的consumer默认将不再保存位移到zookeeper中,而是__consumer_offsets topic中
- 如果消费者根据java api来消费,,也就是
kafka.javaapi.consumer.ConsumerConnector
,通过配置参数zookeeper.connect
来消费。这种情况,消费者的offset会更新到zookeeper的/kafka/consumers/<group.id>/offsets/<topic>/<partitionId>
,但是zookeeper其实并不适合进行大批量的读写操作,尤其是写操作。 - 因此kafka提供了另一种解决方案:增加__consumeroffsets topic,将offset信息写入这个topic,摆脱对zookeeper的依赖(指保存offset这件事情)。__consumer_offsets中的消息保存了每个consumer group某一时刻提交的offset信息。
如下图,可能有这两种情况ZK:zookeeper,KF:kafka
通过新建一个消费者去获取该topic的信息
既然__consumer_offsets 是一个topic,kafka将consumer的消费信息push到该topic,便想到通过消费者的方式去获取该topic的信息
代码如下:
import kafka.common.OffsetAndMetadata;
import kafka.coordinator.*;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Properties;
/**
* @author: wangjian
* @date: 2018-08-01 15:19
*/
public class KafkaCli {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "cdh003:9092");
//每个消费者分配独立的组号
props.put("group.id", "test_1");
//如果value合法,则自动提交偏移量
props.put("enable.auto.commit", "true");
//设置多久一次更新被消费消息的偏移量
props.put("auto.commit.interval.ms", "1000");
//设置会话响应的时间,超过这个时间kafka可以选择放弃消费或者消费下一条消息
props.put("session.timeout.ms", "30000");
//该参数表示从头开始消费该主题
props.put("auto.offset.reset", "earliest");
//注意反序列化方式为ByteArrayDeserializer
props.put("key.deserializer",
"org.apache.kafka.common.serialization.ByteArrayDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.ByteArrayDeserializer");
KafkaConsumer<byte[], byte[]> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("__consumer_offsets"));
System.out.println("Subscribed to topic " + "__consumer_offsets");
while (true) {
ConsumerRecords<byte[], byte[]> records = consumer.poll(100);
for (ConsumerRecord<byte[], byte[]> record : records) {
// 这里直接将record的全部信息写到System.out打印流中
// GroupMetadataManager.OffsetsMessageFormatter formatter = new GroupMetadataManager.OffsetsMessageFormatter();
// formatter.writeTo(record, System.out);
//对record的key进行解析,注意这里的key有两种OffsetKey和GroupMetaDataKey
//GroupMetaDataKey中只有消费者组ID信息,OffsetKey中还包含了消费的topic信息
BaseKey key = GroupMetadataManager.readMessageKey(ByteBuffer.wrap(record.key()));
if (key instanceof OffsetKey) {
GroupTopicPartition partition = (GroupTopicPartition) key.key();
String topic = partition.topicPartition().topic();
String group = partition.group();
System.out.println("group : " + group + " topic : " + topic);
System.out.println(key.toString());
} else if (key instanceof GroupMetadataKey) {
System.out.println("groupMetadataKey:------------ "+key.key());
}
//对record的value进行解析
OffsetAndMetadata om = GroupMetadataManager.readOffsetMessageValue(ByteBuffer.wrap(record.value()));
//System.out.println(om.toString());
}
}
}
}
key的解析输出信息如下:
value的解析输出信息如下:
但是使用这种方式解析过一会儿会出现如下异常:
Exception in thread "main" org.apache.kafka.common.protocol.types.SchemaException: Error reading field 'metadata': Error reading string of length 25970, only 12 bytes available
at org.apache.kafka.common.protocol.types.Schema.read(Schema.java:72)
at kafka.coordinator.GroupMetadataManager$.readOffsetMessageValue(GroupMetadataManager.scala:958)
at kafka.coordinator.GroupMetadataManager.readOffsetMessageValue(GroupMetadataManager.scala)
at com.wangjian.KafkaCli.main(KafkaCli.java:68)
测试发现这是当key类型为GroupMetadataKey时,去解析OffsetAndMetadata会抛异常,应该用如下方法去解析GroupMetadata:
if (key instanceof GroupMetadataKey) {
System.out.println("groupMetadataKey: " + key.key());
//第一个参数为group id,先将key转换为GroupMetadataKey类型,再调用它的key()方法就可以获得group id
GroupMetadata groupMetadata = GroupMetadataManager.readGroupMessageValue(((GroupMetadataKey) key).key(), ByteBuffer.wrap(record.value()));
System.out.println("GroupMetadata: "+groupMetadata.toString());
}
它的输出如下:
groupMetadataKey: test_1
GroupMetadata: [test_1,Some(consumer),Stable,Map(consumer-1-2b952983-41bd-4bdb-bc65-89ceedd91d26 -> [consumer-1-2b952983-41bd-4bdb-bc65-89ceedd91d26,consumer-1,/172.18.89.153,30000])]
可以看到GroupMetadata中保存了该消费者组中每个消费者的具体信息,包括了消费者所在IP等
使用formatter方式写到System.out输出信息如下:
GroupMetadataManager.OffsetsMessageFormatter formatter = new GroupMetadataManager.OffsetsMessageFormatter();
formatter.writeTo(record, System.out);
这是完整的OffsetMessage信息
同时也可以看到__consumer_offsets topic的每一日志项的格式都是:[Group, Topic, Partition]::[OffsetMetadata[Offset, Metadata], CommitTime, ExpirationTime]
总结:
kafka push 到__consumer_offsets 该topic的元数据信息有两种:
key:OffsetKey —————————> value:OffsetAndMetadata 保存了消费者组各个partition的offset位移信息元数据
key:GroupMetadataKey ————————> value:GroupMetadata 保存了消费者组中各个消费者的信息
在通过java 开发consumer去消费该topic获取元数据时,应注意区分这两种情况,还有反序列化方式