Kafka 中的消息是以主题topic为基本单位进行归类的,各个主题在逻辑上相互独立。每个主题又可以分为一个或多个分区,分区的数量可以在主题创建的时候指定,也可以在之后修改。每条消息在发送的时候会根据分区规则被追加到指定的分区中,分区中的每条消息都会被分配一个唯一的序列号,也就是通常所说的偏移量(offset),具有4个分区的主题的逻辑结构见下图。

kafka订阅不到消息_kafka

 如果分区规则设置得合理,那么所有的消息可以均匀地分布到不同的分区中,这样就可以实现水平扩展。不考虑多副本的情况,一个分区对应一个日志(Log)。为了防止 Log 过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件,这样也便于消息的维护和清理。

事实上,Log 和 LogSegment 也不是纯粹物理意义上的概念,Log 在物理上只以文件夹的形式存储,而每个 LogSegment 对应于磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以“.txnindex”为后缀的事务索引文件)。下图描绘了主题、分区、副本、Log 以及 LogSegment 之间的关系。

kafka订阅不到消息_kafka订阅不到消息_02

 接触过 Kafka 的老司机一般都知晓 Log 对应了一个命名形式为-的文件夹。举个例子,假设有一个名为“topic-log”的主题,此主题中具有4个分区,那么在实际物理存储上表现为“topic-log-0”、“topic-log-1”、“topic-log-2”、“topic-log-3”这4个文件夹:

kafka订阅不到消息_分布式_03

向 Log 中追加消息时是顺序写入的,只有最后一个 LogSegment 才能执行写入操作,在此之前所有的 LogSegment 都不能写入数据。为了方便描述,我们将最后一个 LogSegment 称为“activeSegment”,即表示当前活跃的日志分段。随着消息的不断写入,当 activeSegment 满足一定的条件时,就需要创建新的 activeSegment,之后追加的消息将写入新的 activeSegment。

为了便于消息的检索,每个 LogSegment 中的日志文件(以“.log”为文件后缀)都有对应的两个索引文件:偏移量索引文件(以“.index”为文件后缀)和时间戳索引文件(以“.timeindex”为文件后缀)。每个 LogSegment 都有一个基准偏移量 baseOffset,用来表示当前 LogSegment 中第一条消息的 offset。偏移量是一个64位的长整型数,日志文件和两个索引文件都是根据基准偏移量(baseOffset)命名的,名称固定为20位数字,没有达到的位数则用0填充。比如第一个 LogSegment 的基准偏移量为0,对应的日志文件为00000000000000000000.log。

举例说明,向主题topic-log中发送一定量的消息,某一时刻topic-log-0目录中的布局如下所示。

kafka订阅不到消息_分布式_04

示例中第2个 LogSegment 对应的基准位移是133,也说明了该 LogSegment 中的第一条消息的偏移量为133,同时可以反映出第一个 LogSegment 中共有133条消息(偏移量从0至132的消息)。

注意每个 LogSegment 中不只包含“.log”、“.index”、“.timeindex”这3种文件,还可能包含“.deleted”、“.cleaned”、“.swap”等临时文件,以及可能的“.snapshot”、“.txnindex”、“leader-epoch-checkpoint”等文件。

从更加宏观的视角上看,Kafka 中的文件不只上面提及的这些文件,比如还有一些检查点文件,当一个 Kafka 服务第一次启动的时候,默认的根目录下就会创建以下5个文件:

kafka订阅不到消息_分布式_05

__consumer_offsets中的,初始情况下这个主题并不存在,当第一次有消费者消费消息时会自动创建这个主题。

kafka订阅不到消息_字段_06

在某一时刻,Kafka 中的文件目录布局如上图所示。每一个根目录都会包含最基本的4个检查点文件(xxx-checkpoint)和 meta.properties 文件。在创建主题的时候,如果当前 broker 中不止配置了一个根目录,那么会挑选分区数最少的那个根目录来完成本次创建任务。
------------

kafka订阅不到消息_kafka订阅不到消息_07

 如上图 观察默认topic 为__consumer_offsets 的消费者消费提交的偏移量值

kafka订阅不到消息_kafka_08

例如进入到 topic 目录。cd __consumer_offsets-45 观察里面的数据。与自定义创建的topic文件结构一致。 

kafka订阅不到消息_kafka_09

扫盲:__consumer_offsets !!!

__consumer_offsets 是 kafka 自行创建的,和普通的 topic 相同。它存在的目的之一就是保存 consumer 提交的位移。

当一个消费组消费partition,需要保存offset记录消费到哪,以前保存在zk中,由于zk的写性能不好,以前的解决方法都是consumer每隔一分钟上报一次。这里zk的性能严重影响了消费的速度,而且很容易出现重复消费。
在0.10版本后,kafka把这个offset的保存,从zk总剥离,保存在一个名叫__consumeroffsets topic的topic中。写进消息的key由groupid、topic、partition组成,value是偏移量offset。topic配置的清理策略是compact。总是保留最新的key,其余删掉。一般情况下,每个key的offset都是缓存在内存中,查询的时候不用遍历partition,如果没有缓存,第一次就会遍历partition建立缓存,然后查询返回。

确定consumer group位移信息写入__consumers_offsets的哪个partition,具体计算公式:

__consumers_offsets partition =
           Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)   
//groupMetadataTopicPartitionCount由offsets.topic.num.partitions指定,默认是50个分区。

__consumer_offsets 的每条消息格式大致如图所示:

kafka订阅不到消息_kafka_10

可以想象成一个 KV 格式的消息,key 就是一个三元组:group.id+topic+分区号,而 value 就是 offset 的值。

考虑到一个 kafka 生成环境中可能有很多 consumer 和 consumer group,如果这些 consumer 同时提交位移,则必将加重 __consumer_offsets 的写入负载,因此 kafka 默认为该 topic 创建了50个分区,并且对每个 group.id 做哈希求模运算,从而将负载分散到不同的 __consumer_offsets 分区上。如上面的👆🏻具体计算公式。

一般情况下,当集群中第一次有消费者消费消息时会自动创建 __consumer_offsets,它的副本因子受 offsets.topic.replication.factor 参数的约束,默认值为3(注意:该参数的使用限制在0.11.0.0版本发生变化),分区数可以通过 offsets.topic.num.partitions 参数设置,默认值为50。

OffsetCommitRequest

客户端提交消费位移是使用 OffsetCommitRequest请求实现的,OffsetCommitRequest 的结构如下图所示。

kafka订阅不到消息_分布式_11

 

请求体第一层中的 group_id、generation_id 和 member_id 表示消费者具体信息, retention_time 表示当前提交的消费位移所能保留的时长,不过对于消费者而言这个值置为1。也就是说,按照 broker 端的配置 offsets.retention.minutes 来确定保留时长,默认为10080,即7天,超过这个时间后消费位移的信息就会被删除(使用墓碑消息和日志压缩策略)。
注意:这个参数在2.0.0版本之前的默认值为1440,即1天。

OffsetCommitRequest 中的其余字段大抵也是按照分区的粒度来划分消费位移的,注意还有一个 metadata 字段。metadata 是自定义的元数据信息,如果不指定这个参数,那么就会被设置为空字符串,注意 metadata 的长度不能超过 broker 端参数 offset.metadata.max.bytes 参数所配置的大小,默认值为4096。

Key 和 Value
同消费组的元数据信息一样,最终提交的消费位移也会以消息的形式发送至 __consumer_offsets,与消费位移对应的消息只定义了 key和 value 字段的具体内容,它不依赖于具体版本的消息格式,以此做到与具体的消息格式无关。

下图中展示了消费位移对应的消息内容格式,上面是消息的 key,下面是消息的 value。可以看到 key 和 value 中都包含了 version 字段,这个用来标识具体的 key 和 value 的版本信息,不同的版本对应的内容格式可能并不相同。到当前版本版本(2.x)而言 key 和 value 的 version 值都为1。

kafka订阅不到消息_字段_12

 

Key
key 中除了 version 字段还有 group、 topic 、 partition 字段,分别表示消费组的 groupId、topic 和 partition 编号。虽然 key 中包含了4个字段,但最终确定这条消息所要存储的分区还是根据单独的 group 字段来计算的,这样就可以保证消费位移信息与消费组对应的 GroupCoordinator 处于同一个 broker 节点上,省去了中间轮转的开销,这一点与消费组的元数据信息的存储是一样的。

Value
value 中包含了5个字段,除 version 字段外,其余的 offset、metadata、commit_timestamp、expire_timestamp 字段分别表示消费位移、自定义的元数据信息、位移提交到 Kafka 的时间戳、消费位移被判定为超时的时间戳。其中 offset 和 metadata 与 OffsetCommitRequest 请求体中的offset 和metadata 对应,而 commit_timestamp 和 OffsetCommitRequest 请求体中的 retention_time 也有关联,commit_timestamp 值与 offsets.retention.minutes 参数值之和即为 expire_timestamp (默认情况下)。

OffsetCommitResponse

在处理完消费位移之后,Kafka 返回 OffsetCommitResponse 给客户端,OffsetCommitResponse 的结构如下图所示。

kafka订阅不到消息_偏移量_13

 

查看__consumer_offsets中的消息
可以通过 kafka-console-consumer.sh 脚本来查看 __consumer_offsets 中的内容,不过要设定 --formatter 参数为kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter
注意,该参数在0.11.0版本之前为 kafka.coordinator.GroupMetadataManager$OffsetsMessageFormatter

示例:

./kafka-console-consumer.sh --bootstrap-server 10.163.198.134:9092 --topic __consumer_offsets --partition 35 --from-beginning --formatter 'kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter'