1. 背景
大数据平台经常以Kafka作为消息中间件,且需要有完整的针对Kafka的管理和监控体系,例如实时查看:
committed-offset:topic在不同消费者组中的消费进度
visible-offset: topic中的可见消息总量,当consumer的隔离级别为read_uncommitted
,visible-offset等于high watermark
;当consumer的隔离级别为read_committed
,visible-offset等于last stable offset
Lag:消费延迟,lag=logEndOffset - committedOffset
log-end-offset:日志末端位移,代表日志文件中下一条待写入消息的offset,这个offset上实际是没有消息的
消费进度的保存机制在不同的Kafka版本中经历了以下几个阶段的变化:
<=0.8:保存在zookeeper中的/consumer路径下
>=0.9:保存在内部主题__consumer_offsets中
因为zookeeper是一个分布式协调工具,不适合大量数据的读和写,因此将消费进度转而保存在内部主题__consumer_offsets中是一个合适的优化,我们只针对该种保存方式进行查询。
2. 方案一:__consumer_offsets
既然主题__consumer_offsets中保存了消费进度消息,我们的第一想法是通过查询__consumer_offsets来获取消费进度。
2.1 消息格式
要想正确地解析__consumer_offsets并获取消费进度,我们首先要了解__consumer_offsets中消息格式。
__consumer_offsets 中保存的记录是普通的Kafka消息,但是消息的格式由Kafka来维护,用户不能干预。__consumer_offsets中保存三类消息,分别是:
- Consumer group组元数据消息
每个消费者组的元信息都保存在这个topic中,这些元数据包括:
需要强调的是,如果使用simple consumer(consumer.assign),因为不存在消费者组,则不会向该内部主题写入消息。
key:key是一个二元组,格式是【version+groupId】,这里的版本表征这类消息的版本号,无实际用途;
value:图中的组元数据
写入时机:消费者组rebalance时写入的
- Consumer group位移消息
__consumer_offsets保存consumer提交到Kafka的位移数据,这是众所周知的。元数据如下:
其中,过期时间是用户指定的过期时间。如果consumer代码在提交位移时没有明确指定过期间隔,故broker端默认设置过期时间为提交时间+offsets.retention.minutes参数值,即提交1天之后自动过期。Kafka会定期扫描_consumer_offsets中的位移消息并删除掉那些过期的位移数据。
key:一个三元组,格式是【groupId + topic + partition】
value:图中的位移元数据
写入时机:消费者提交位移时写入
提交的时候,即使位移数据并没有更新,也会向__consumer_offsets写入一条新消息
- Tombstone消息
第三类消息是tombstone消息或delete mark消息。这类消息只出现在源码中而不暴露给用户。
写入时机:在Kafka后台线程扫描并删除过期位移或者__consumer_offsets分区副本重分配时写入的
2.2 代码
/**
* GroupTopicPartition --> OffsetAndMetadata
* 利用guava cache清除策略进行group的过期清除,利用监听器同时清除集合中的group
*/
private final Cache<GroupTopicPartition, OffsetAndMetadata> groupTopicPartitionOffsetMap = CacheBuilder
.newBuilder()
.maximumSize(100000)
.expireAfterAccess(7,TimeUnit.DAYS)
.removalListener((t) -> groupTopicPartitionOffsetSet.remove(t))
.build();
/**
* group topicPartition set ,element: group、topic、partition
*/
private Set<GroupTopicPartition> groupTopicPartitionOffsetSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
private Map<String,Set<String>> consumerTopicSetMap = new HashMap<>(32);
GroupMetadataManager groupMetaManager = new GroupMetadataManager();
@Override
public void run() {
// 自定义一个消费者组名为“kafkaManager”的消费者,订阅内部topic获取offset信息
Consumer<byte[],byte[]> consumer = createKafkaConsumer();
consumer.subscribe(Collections.singleton("__consumer_offsets"));
while (true){
ConsumerRecords<byte[],byte[]> records = consumer.