__consumer_offsets 中的注册消息(Group Metadata)和位移消息(Offset Commit)。
位移主题有两类消息:消费者组注册消息(Group Metadata)和消费者组的已提交位移消息(Offset Commit)。
注册消息
所谓的注册消息,就是指消费者组向位移主题写入注册类的消息。所有成员都加入组后:Coordinator 向位移主题写入注册消息,只是该消息不含分区消费分配方案;Leader 成员发送方案给 Coordinator 后:当 Leader 成员将分区消费分配方案发给 Coordinator 后,Coordinator 写入携带分配方案的注册消息。
所有成员都加入组后:Coordinator 向位移主题写入注册消息,只是该消息不含分区消费分配方案;
Leader 成员发送方案给 Coordinator 后:当 Leader 成员将分区消费分配方案发给 Coordinator 后,Coordinator 写入携带分配方案的注册消息。.
注册消息的 Key
case class GroupMetadataKey(version: Short, key: String) extends BaseKey {
override def toString: String = key
}
该类的 key 字段是一个字符串类型,保存的是消费者组的名称。注册消息的 Key 就是消费者组名。
GroupMetadataManager 对象有个 groupMetadataKey 方法,负责将注册消息的 Key 转换成字节数组,用于后面构造注册消息。
def groupMetadataKey(group: String): Array[Byte] = {
val key = new Struct(CURRENT_GROUP_KEY_SCHEMA)
key.set(GROUP_KEY_GROUP_FIELD, group)
// 构造一个ByteBuffer对象,容纳version和key数据
val byteBuffer = ByteBuffer.allocate(2 /* version */ + key.sizeOf)
byteBuffer.putShort(CURRENT_GROUP_KEY_SCHEMA_VERSION)
// 依次向 Buffer 写入 Short 型的消息格式版本以及消费者组名
key.writeTo(byteBuffer)
byteBuffer.array()
}
消息体 Value
groupMetadataValue 方法将消费者组重要的元数据写入到字节数组。
def groupMetadataValue(groupMetadata: GroupMetadata, // 消费者组元数据对象
assignment: Map[String, Array[Byte]], // 分区消费分配方案
apiVersion: ApiVersion): Array[Byte] = { // Kafka API版本号
// 确定消息格式版本以及格式结构
val (version, value) = {
if (apiVersion < KAFKA_0_10_1_IV0)
(0.toShort, new Struct(GROUP_METADATA_VALUE_SCHEMA_V0))
else if (apiVersion < KAFKA_2_1_IV0)
(1.toShort, new Struct(GROUP_METADATA_VALUE_SCHEMA_V1))
else if (apiVersion < KAFKA_2_3_IV0)
(2.toShort, new Struct(GROUP_METADATA_VALUE_SCHEMA_V2))
else
(3.toShort, new Struct(GROUP_METADATA_VALUE_SCHEMA_V3))
}
// 依次写入消费者组主要的元数据信息
// 包括协议类型、Generation ID、分区分配策略和Leader成员ID
value.set(PROTOCOL_TYPE_KEY, groupMetadata.protocolType.getOrElse(""))
value.set(GENERATION_KEY, groupMetadata.generationId)
value.set(PROTOCOL_KEY, groupMetadata.protocolOrNull)
value.set(LEADER_KEY, groupMetadata.leaderOrNull)
if (version >= 2)
value.set(CURRENT_STATE_TIMESTAMP_KEY, groupMetadata.currentStateTimestampOrDefault)
// 写入各个成员的元数据信息
// 包括成员ID、client.id、主机名以及会话超时时间
val memberArray = groupMetadata.allMemberMetadata.map { memberMetadata =>
val memberStruct = value.instance(MEMBERS_KEY)
memberStruct.set(MEMBER_ID_KEY, memberMetadata.memberId)
memberStruct.set(CLIENT_ID_KEY, memberMetadata.clientId)
memberStruct.set(CLIENT_HOST_KEY, memberMetadata.clientHost)
memberStruct.set(SESSION_TIMEOUT_KEY, memberMetadata.sessionTimeoutMs)
// 写入Rebalance超时时间
if (version > 0)
memberStruct.set(REBALANCE_TIMEOUT_KEY, memberMetadata.rebalanceTimeoutMs)
// 写入用于静态消费者组管理的Group Instance ID
if (version >= 3)
memberStruct.set(GROUP_INSTANCE_ID_KEY, memberMetadata.groupInstanceId.orNull)
// The group is non-empty, so the current protocol must be defined
// 必须定义分区分配策略,否则抛出异常
val protocol = groupMetadata.protocolOrNull
if (protocol == null)
throw new IllegalStateException("Attempted to write non-empty group metadata with no defined protocol")
// 写入成员消费订阅信息
val metadata = memberMetadata.metadata(protocol)
memberStruct.set(SUBSCRIPTION_KEY, ByteBuffer.wrap(metadata))
val memberAssignment = assignment(memberMetadata.memberId)
assert(memberAssignment != null)
// 写入成员消费分配信息
memberStruct.set(ASSIGNMENT_KEY, ByteBuffer.wrap(memberAssignment))
memberStruct
}
value.set(MEMBERS_KEY, memberArray.toArray)
// 向Buffer依次写入版本信息和以上写入的元数据信息
val byteBuffer = ByteBuffer.allocate(2 /* version */ + value.sizeOf)
byteBuffer.putShort(version)
value.writeTo(byteBuffer)
// 返回Buffer底层的字节数组
byteBuffer.array()
}
已提交位移消息
提交位移消息的 Key 和 Value 构成。Key 是一个 GroupTopicPartition 类型,也就是 < 消费者组名,主题,分区号 > 三元组。
提交位移消息的 Key 定义
case class OffsetKey(version: Short, key: GroupTopicPartition) extends BaseKey {
override def toString: String = key.toString
}
offsetCommitKey 方法负责将这个三元组转换成字节数组,用于后续构造提交位移消息。
def offsetCommitKey(group: String, // 消费者组名
topicPartition: TopicPartition): Array[Byte] = { // 主题 + 分区号
// 创建结构体,依次写入消费者组名、主题和分区号
val key = new Struct(CURRENT_OFFSET_KEY_SCHEMA)
key.set(OFFSET_KEY_GROUP_FIELD, group)
key.set(OFFSET_KEY_TOPIC_FIELD, topicPartition.topic)
key.set(OFFSET_KEY_PARTITION_FIELD, topicPartition.partition)
// 构造ByteBuffer,写入格式版本和结构体
val byteBuffer = ByteBuffer.allocate(2 /* version */ + key.sizeOf)
byteBuffer.putShort(CURRENT_OFFSET_KEY_SCHEMA_VERSION)
key.writeTo(byteBuffer)
// 返回字节数组
byteBuffer.array()
}
提交位移消息的 value
offsetCommitValue 方法决定了 Value 中都有哪些元素
def offsetCommitValue(offsetAndMetadata: OffsetAndMetadata,
apiVersion: ApiVersion): Array[Byte] = {
// generate commit value according to schema version
// 确定消息格式版本以及创建对应的结构体对象
val (version, value) = {
if (apiVersion < KAFKA_2_1_IV0 || offsetAndMetadata.expireTimestamp.nonEmpty) {
val value = new Struct(OFFSET_COMMIT_VALUE_SCHEMA_V1)
value.set(OFFSET_VALUE_OFFSET_FIELD_V1, offsetAndMetadata.offset)
value.set(OFFSET_VALUE_METADATA_FIELD_V1, offsetAndMetadata.metadata)
value.set(OFFSET_VALUE_COMMIT_TIMESTAMP_FIELD_V1, offsetAndMetadata.commitTimestamp)
// version 1 has a non empty expireTimestamp field
value.set(OFFSET_VALUE_EXPIRE_TIMESTAMP_FIELD_V1,
offsetAndMetadata.expireTimestamp.getOrElse(OffsetCommitRequest.DEFAULT_TIMESTAMP))
(1, value)
} else if (apiVersion < KAFKA_2_1_IV1) {
val value = new Struct(OFFSET_COMMIT_VALUE_SCHEMA_V2)
// 依次写入位移值、Leader Epoch值、自定义元数据以及时间戳
value.set(OFFSET_VALUE_OFFSET_FIELD_V2, offsetAndMetadata.offset)
value.set(OFFSET_VALUE_METADATA_FIELD_V2, offsetAndMetadata.metadata)
value.set(OFFSET_VALUE_COMMIT_TIMESTAMP_FIELD_V2, offsetAndMetadata.commitTimestamp)
(2, value)
} else {
val value = new Struct(OFFSET_COMMIT_VALUE_SCHEMA_V3)
value.set(OFFSET_VALUE_OFFSET_FIELD_V3, offsetAndMetadata.offset)
value.set(OFFSET_VALUE_LEADER_EPOCH_FIELD_V3,
offsetAndMetadata.leaderEpoch.orElse(RecordBatch.NO_PARTITION_LEADER_EPOCH))
value.set(OFFSET_VALUE_METADATA_FIELD_V3, offsetAndMetadata.metadata)
value.set(OFFSET_VALUE_COMMIT_TIMESTAMP_FIELD_V3, offsetAndMetadata.commitTimestamp)
(3, value)
}
}
// 构建ByteBuffer,写入消息格式版本和结构体
val byteBuffer = ByteBuffer.allocate(2 /* version */ + value.sizeOf)
byteBuffer.putShort(version.toShort)
value.writeTo(byteBuffer)
// 返回ByteBuffer底层字节数组
byteBuffer.array()
}
Tombstone 消息
Kafka 源码中还存在一类消息,那就是 Tombstone 消息。它就是 Value 为 null 的消息。这个消息的主要作用,是让 Kafka 识别哪些 Key 对应的消息是可以被删除的,有了它,Kafka 就能保证,内部位移主题不会持续增加磁盘占用空间。
- 一旦注册消息中出现了 Tombstone 消息,就表示 Kafka 可以将该消费者组元数据从位移主题中删除;
- 一旦提交位移消息中出现了 Tombstone,就表示 Kafka 能够将该消费者组在某主题分区上的位移提交数据删除。