日志格式的演变(二)

日志格式的演变

      对一个成熟的消息中间件而言,消息格式(或者称为“日志格式”)不仅关系功能维度的扩展,还牵涉性能维度的优化。随着 Kafka 的迅猛发展,其消息格式也在不断升级改进,从0.8.x版本开始到现在的2.0.0版本,Kafka 的消息格式也经历了3个版本:v0 版本、v1 版本和v2 版本。

      每个分区由内部的每一条消息组成,如果消息格式设计得不够精炼,那么其功能和性能都会大打折扣。比如有冗余字段,势必会不必要地增加分区的占用空间,进而不仅使存储的开销变大、网络传输的开销变大,也会使 Kafka 的性能下降。

      反观如果缺少字段,比如在最初的 Kafka 消息版本中没有 timestamp 字段,对内部而言,其影响了日志保存、切分策略,对外部而言,其影响了消息审计、端到端延迟、大数据应用等功能的扩展。虽然可以在消息体内部添加一个时间戳,但解析变长的消息体会带来额外的开销,而存储在消息体(参考下图中的 value 字段)前面可以通过指针偏移量获取其值而容易解析,进而减少了开销(可以查看v1版本),虽然相比于没有 timestamp 字段的开销会大一点。

      由此可见,仅在一个字段的一增一减之间就有这么多门道,那么 Kafka 具体是怎么做的呢?这里只针对 Kafka 0.8.x之上(包含)的版本做相应说明,对于之前的版本不做陈述。

v0版本

       Kafka 消息格式的第一个版本通常称为v0版本,在 Kafka 0.10.0之前都采用的这个消息格式(在0.8.x版本之前,Kafka 还使用过一个更古老的消息格式,不过对目前的 Kafka 而言,我们也不需要了解这个版本的消息格式)。如无特殊说明,我们只讨论消息未压缩的情形。

5-3

 

       上图中左边的“RECORD”部分就是 v0 版本的消息格式,大多数人会把图中左边的整体(即包括 offset 和 message size 字段)都看作消息,因为每个 RECORD(v0 和 v1 版)必定对应一个 offset 和 message size。每条消息都有一个 offset 用来标志它在分区中的偏移量,这个 offset 是逻辑值,而非实际物理偏移值,message size 表示消息的大小,这两者在一起被称为日志头部(LOG_OVERHEAD),固定为12B。

      LOG_OVERHEAD 和 RECORD 一起用来描述一条消息,为了配合陈述的语境,在讲述具体消息格式时会偏向于将单纯的RECORD 看作消息,而在其他地方则偏向于将 LOG_OVERHEAD 和 RECORD 的整体看作消息,读者需要留意其中的区别。与消息对应的还有消息集的概念,消息集中包含一条或多条消息,消息集不仅是存储于磁盘及在网络上传输(Produce & Fetch)的基本形式,而且是 Kafka 中压缩的基本单元,详细结构参考上图中的右边部分。

下面具体陈述一下消息格式中的各个字段,从 crc32 开始算起,各个字段的解释如下。

  • crc32(4B):crc32 校验值。校验范围为 magic 至 value 之间。
  • magic(1B):消息格式版本号,此版本的 magic 值为0。
  • attributes(1B):消息的属性。总共占1个字节,低3位表示压缩类型:0表示 NONE、1表示 GZIP、2表示 SNAPPY、3表示 LZ4(LZ4 自 Kafka 0.9.x引入),其余位保留。
  • key length(4B):表示消息的 key 的长度。如果为-1,则表示没有设置 key,即 key = null。
  • key:可选,如果没有 key 则无此字段。
  • value length(4B):实际消息体的长度。如果为-1,则表示消息为空。
  • value:消息体。可以为空,比如墓碑(tombstone)消息。

       v0 版本中一个消息的最小长度(RECORD_OVERHEAD_V0)为crc32 + magic + attributes + key length + value length = 4B + 1B + 1B + 4B + 4B =14B。也就是说,v0 版本中一条消息的最小长度为14B,如果小于这个值,那么这就是一条破损的消息而不被接收。

          这里我们来做一个测试,首先创建一个分区数和副本因子都为1的主题,名称为“msg_format_v0”,然后往msg_format_v0中发送一条key ="key"、value = "value"的消息,之后查看对应的日志(这里采用 Kafka 0.8.2.1的版本):

[root@node1 kafka_2.10-0.8.2.1]# bin/kafka-run-class.sh 
     kafka.tools.DumpLogSegments --files 
     /tmp/kafka-logs/msg_format_v0-0/00000000000000000000.log
Dumping /tmp/kafka-logs-08/msg_format_v0-0/00000000000000000000.log
Starting offset: 0
offset: 0 position: 0 isvalid: true payloadsize: 5 magic: 0 
compresscodec: NoCompressionCodec crc: 592888119 keysize: 3

         日志的大小(即00000000000000000000.log文件的大小)为34B,其值正好等于 LOG_OVERHEAD + RECORD_OVERHEAD_V0 + 3B的 key + 5B的 value = 12B + 14B + 3B + 5B = 34B。

[root@node1 msg_format_v0-0]# ll *.log
-rw-r--r-- 1 root root       34 Apr 26 02:52 00000000000000000000.log

我们再发送一条 key = null,value = "value"的消息,之后查看日志的大小:

[root@node1 msg_format_v0-0]# ll *.log
-rw-r--r-- 1 root root       65 Apr 26 02:56 00000000000000000000.log

        日志大小为65B,减去上一条34B的消息(LOG_OVERHEAD+RECORD),可以得知本条消息的大小为31B,正好等于 LOG_OVERHEAD + RECORD_OVERHEAD_V0 + 5B的value = 12B + 14B+ 5B = 31B。

v1版本

       Kafka 从0.10.0版本开始到0.11.0版本之前所使用的消息格式版本为 v1,比 v0 版本就多了一个 timestamp 字段,表示消息的时间戳。v1 版本的消息结构如下图所示。

5-4

 

       v1 版本的 magic 字段的值为1。v1 版本的 attributes 字段中的低3位和 v0 版本的一样,还是表示压缩类型,而第4个位(bit)也被利用了起来:0表示 timestamp 类型为 CreateTime,而1表示 timestamp 类型为 LogAppendTime,其他位保留。timestamp 类型由 broker 端参数 log.message.timestamp.type 来配置,默认值为 CreateTime,即采用生产者创建消息时的时间戳。如果在创建 ProducerRecord 时没有显式指定消息的时间戳,那么 KafkaProducer 也会在发送这条消息前自动添加上。

下面是 KafkaProducer 中与此对应的一句关键代码:

long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();

        v1 版本的消息的最小长度(RECORD_OVERHEAD_V1)要比v0版本的大8个字节,即22B。如果像 v0 版本介绍的一样发送一条 key = "key"、value = "value" 的消息,那么此条消息在 v1 版本中会占用42B,具体测试步骤参考 v0 版的相关介绍。

消息压缩

        常见的压缩算法是数据量越大压缩效果越好,一条消息通常不会太大,这就导致压缩效果并不是太好。而 Kafka 实现的压缩方式是将多条消息一起进行压缩,这样可以保证较好的压缩效果。在一般情况下,生产者发送的压缩数据在 broker 中也是保持压缩状态进行存储的,消费者从服务端获取的也是压缩的消息,消费者在处理消息之前才会解压消息,这样保持了端到端的压缩。

      Kafka 日志中使用哪种压缩方式是通过参数 compression.type 来配置的,默认值为“producer”,表示保留生产者使用的压缩方式。这个参数还可以配置为“gzip”、“snappy”、“lz4”,分别对应 GZIP、SNAPPY、LZ4 这3种压缩算法。如果参数 compression.type 配置为 “uncompressed”,则表示不压缩。

注意要点:压缩率是压缩后的大小与压缩前的对比。例如:把100MB的文件压缩后是90MB,压缩率为90/100×100%=90%,压缩率越小,压缩效果越好。一般口语化陈述时会误描述为压缩率越高越好,为了避免混淆,本节不引入学术上的压缩率而引入压缩效果,这样容易达成共识。

5-5

 

      以上都是针对消息未压缩的情况,而当消息压缩时是将整个消息集进行压缩作为内层消息(inner message),内层消息整体作为外层(wrapper message)的 value,其结构上图所示。

        压缩后的外层消息(wrapper message)中的 key 为 null,所以上图左半部分没有画出 key 字段,value 字段中保存的是多条压缩消息(inner message,内层消息),其中 Record 表示的是从 crc32 到 value 的消息格式。当生产者创建压缩消息的时候,对内部压缩消息设置的 offset 从0开始为每个内部消息分配 offset,详细可以参考下图右半部分。

5-6

 

      其实每个从生产者发出的消息集中的消息 offset 都是从0开始的,当然这个 offset 不能直接存储在日志文件中,对 offset 的转换是在服务端进行的,客户端不需要做这个工作。外层消息保存了内层消息中最后一条消息的绝对位移(absolute offset),绝对位移是相对于整个分区而言的。

      参考上图,对于未压缩的情形,图右内层消息中最后一条的 offset 理应是1030,但被压缩之后就变成了5,而这个1030被赋予给了外层的 offset。当消费者消费这个消息集的时候,首先解压缩整个消息集,然后找到内层消息中最后一条消息的 inner offset,根据如下公式找到内层消息中最后一条消息前面的消息的 absolute offset(RO 表示 Relative Offset,IO 表示 Inner Offset,而 AO 表示 Absolute Offset):

RO = IO_of_a_message - IO_of_the_last_message 
AO = AO_Of_Last_Inner_Message + RO

      注意这里的 RO 是前面的消息相对最后一条消息的 IO 而言的,所以其值小于等于0,0表示最后一条消息自身。

注意要点:压缩消息,英文是 compress message,Kafka 中还有一个 compact message,常常被人们直译成压缩消息,需要注意两者的区别。compact message 是针对日志清理策略而言的(cleanup.policy = compact),是指日志压缩(Log Compaction)后的消息,这个在后面的章节中会有相关介绍。本节中的压缩消息单指 compress message,即采用 GZIP、LZ4 等压缩工具压缩的消息。

        在讲述 v1 版本的消息时,我们了解到 v1 版本比 v0 版的消息多了一个 timestamp 字段。对于压缩的情形,外层消息的 timestamp 设置为:

  • 如果 timestamp 类型是 CreateTime,那么设置的是内层消息中最大的时间戳。
  • 如果 timestamp 类型是 LogAppendTime,那么设置的是 Kafka 服务器当前的时间戳。 内层消息的 timestamp 设置为:
  • 如果外层消息的 timestamp 类型是 CreateTime,那么设置的是生产者创建消息时的时间戳。
  • 如果外层消息的 timestamp 类型是 LogAppendTime,那么所有内层消息的时间戳都会被忽略。

       对 attributes 字段而言,它的 timestamp 位只在外层消息中设置,内层消息中的 timestamp 类型一直都是 CreateTime。

变长字段

       Kafka 从0.11.0版本开始所使用的消息格式版本为 v2,这个版本的消息相比 v0 和 v1 的版本而言改动很大,同时还参考了 Protocol Buffer 而引入了变长整型(Varints)和 ZigZag 编码。为了更加形象地说明问题,首先我们来了解一下变长整型。

       Varints 是使用一个或多个字节来序列化整数的一种方法。数值越小,其占用的字节数就越少。Varints 中的每个字节都有一个位于最高位的msb位(most significant bit),除最后一个字节外,其余 msb 位都设置为1,最后一个字节的 msb 位为0。这个 msb 位表示其后的字节是否和当前字节一起来表示同一个整数。除 msb 位外,剩余的7位用于存储数据本身,这种表示类型又称为 Base 128。

       通常而言,一个字节8位可以表示256个值,所以称为 Base 256,而这里只能用7位表示,2的7次方即128。Varints 中采用的是小端字节序,即最小的字节放在最前面。

举个例子,比如数字1,它只占一个字节,所以 msb 位为0:

0000 0001

再举一个复杂点的例子,比如数字300:

1010 1100 0000 0010

      300的二进制表示原本为0000 0001 0010 1100 = 256+32+8+4=300,那么为什么300的变长表示为上面的这种形式? 首先去掉每个字节的 msb 位,表示如下:

1010 1100 0000 0010 
    -> 010 1100 000 0010

如前所述,Varints 使用的是小端字节序的布局方式,所以这里两个字节的位置需要翻转一下:

010 1100 000 0010
    -> 000 0010 010 1100 (翻转)
    -> 000 0010 ++ 010 1100 
    -> 0000 0001 0010 1100 = 256+32+8+4=300

      Varints 可以用来表示 int32、int64、uint32、uint64、sint32、sint64、bool、enum 等类型。在实际使用过程中,如果当前字段可以表示为负数,那么对 int32/int64 和 sint32/sint64 而言,它们在进行编码时存在较大的区别。比如使用 int64 表示一个负数,那么哪怕是-1,其编码后的长度始终为10个字节(可以通过下面的代码来测试长度),就如同对待一个很大的无符号长整型数一样。为了使编码更加高效,Varints 使用了 ZigZag 的编码方式。

public int sizeOfLong(int v) {
    int bytes = 1;
    while ((v & 0xffffffffffffff80L) != 0L) {
        bytes += 1;
        v >>>= 7;
    }
    return bytes;
}

       ZigZag 编码以一种锯齿形(zig-zags)的方式来回穿梭正负整数,将带符号整数映射为无符号整数,这样可以使绝对值较小的负数仍然享有较小的 Varints 编码值,比如-1编码为1,1编码为2,-2编码为3,如下表所示。

原 值编码后的值
00
-11
12
-23
21474836474294967294
-21474836484294967295

对应的公式为:

(n << 1) ^ (n >> 31)

这是对 sint32 而言的,sint64 对应的公式为:

(n << 1) ^ (n >> 63)

以-1为例,其二进制表现形式为1111 1111 1111 1111 1111 1111 1111 1111(补码)。

(n << 1)	= 1111 1111 1111 1111 1111 1111 1111 1110
(n >> 31) 	= 1111 1111 1111 1111 1111 1111 1111 1111
(n << 1) ^ (n >> 31) = 1

       最终-1的 Varints 编码为0000 0001,这样原本用4个字节表示的-1现在可以用1个字节来表示了。1就显得非常简单了,其二进制表现形式为0000 0000 0000 0000 0000 0000 0000 0001。

(n << 1)   	= 0000 0000 0000 0000 0000 0000 0000 0010
(n >> 31) 	= 0000 0000 0000 0000 0000 0000 0000 0000
(n << 1) ^ (n >> 31) = 2

      最终1的 Varints 编码为0000 0010,也只占用1个字节。

      前面说过 Varints 中的一个字节中只有7位是有效数值位,即只能表示128个数值,转变成绝对值之后其实质上只能表示64个数值。比如对消息体长度而言,其值肯定是大于等于0的正整数,那么一个字节长度的 Varints 最大只能表示63。64的二进制数表示为:

0100 0000

经过 ZigZag 处理后为:

1000 0000 ^ 0000 0000 = 1000 0000

每个字节的低7位是有效数值位,所以1000 0000进一步转变为:

000 0001 000 0000

而 Varints 使用小端字节序,所以需要翻转一下位置:

000 0000 000 0001

设置非最后一个字节的 msb 位为1,最后一个字节的 msb 位为0,最终有:

1000 0000 0000 0001

所以最终64表示为1000 0000 0000 0001,而63却表示为0111 1110。

具体的编码实现如下(针对 int32 类型):

public static void writeVarint(int value, ByteBuffer buffer) {
    int v = (value << 1) ^ (value >> 31);
    while ((v & 0xffffff80) != 0L) {
        byte b = (byte) ((v & 0x7f) | 0x80);
        buffer.put(b);
        v >>>= 7;
    }
    buffer.put((byte) v);
}

对应的解码实现如下(针对 int32 类型):

public static int readVarint(ByteBuffer buffer) {
    int value = 0;
    int i = 0;
    int b;
    while (((b = buffer.get()) & 0x80) != 0) {
        value |= (b & 0x7f) << i;
        i += 7;
        if (i > 28)
            throw illegalVarintException(value);
    }
    value |= b << i;
    return (value >>> 1) ^ -(value & 1);
}

       回顾 Kafka v0 和 v1 版本的消息格式,如果消息本身没有 key,那么 key length 字段为-1,int 类型的需要4个字节来保存,而如果采用 Varints 来编码则只需要1个字节。根据 Varints 的规则可以推导出0~63之间的数字占1个字节,64~8191之间的数字占2个字节,8192~1048575之间的数字占3个字节。而 Kafka broker 端配置 message.max.bytes 的默认大小为1000012(Varints 编码占3个字节),如果消息格式中与长度有关的字段采用 Varints 的编码,那么绝大多数情况下都会节省空间,而 v2 版本的消息格式也正是这样做的。

      不过需要注意的是,Varints 并非一直会节省空间,一个 int32 最长会占用5个字节(大于默认的4个字节),一个 int64 最长会占用10个字节(大于默认的8个字节)。下面的代码展示了如何计算一个 int32 占用的字节个数:

public static int sizeOfVarint(int value) {
    int v = (value << 1) ^ (value >> 31);
    int bytes = 1;
    while ((v & 0xffffff80) != 0L) {
        bytes += 1;
        v >>>= 7;
    }
    return bytes;
}

有关 int32/int64 的更多实现细节可以参考 org.apache.kafka.common.utils.ByteUtils。

v2版本

      v2 版本中消息集称为 Record Batch,而不是先前的 Message Set,其内部也包含了一条或多条消息,消息的格式参见下图的中部和右部。在消息压缩的情形下,Record Batch Header 部分(参见下图左部,从 first offset 到 records count 字段)是不被压缩的,而被压缩的是 records 字段中的所有内容。生产者客户端中的 ProducerBatch 对应这里的 RecordBatch,而 ProducerRecord 对应这里的 Record。

5-7

 

      先讲述消息格式 Record 的关键字段,可以看到内部字段大量采用了 Varints,这样 Kafka 可以根据具体的值来确定需要几个字节来保存。v2 版本的消息格式去掉了 crc 字段,另外增加了 length(消息总长度)、timestamp delta(时间戳增量)、offset delta(位移增量)和 headers 信息,并且 attributes 字段被弃用了,笔者对此做如下分析(key、key length、value、value length 字段同 v0 和 v1 版本的一样,这里不再赘述)。

  • length:消息总长度。
  • attributes:弃用,但还是在消息格式中占据1B的大小,以备未来的格式扩展。
  • timestamp delta:时间戳增量。通常一个 timestamp 需要占用8个字节,如果像这里一样保存与 RecordBatch 的起始时间戳的差值,则可以进一步节省占用的字节数。
  • offset delta:位移增量。保存与 RecordBatch 起始位移的差值,可以节省占用的字节数。
  • headers:这个字段用来支持应用级别的扩展,而不需要像 v0 和 v1 版本一样不得不将一些应用级别的属性值嵌入消息体。Header 的格式如图中最右部分所示,包含 key 和 value,一个 Record 里面可以包含0至多个 Header。

          对于 v1 版本的消息,如果用户指定的 timestamp 类型是 LogAppendTime 而不是 CreateTime,那么消息从生产者进入 broker 后,timestamp 字段会被更新,此时消息的 crc 值将被重新计算,而此值在生产者中已经被计算过一次。再者,broker 端在进行消息格式转换时(比如 v1 版转成 v0 版的消息格式)也会重新计算 crc 的值。在这些类似的情况下,消息从生产者到消费者之间流动时,crc 的值是变动的,需要计算两次 crc 的值,所以这个字段的设计在 v0 和 v1 版本中显得比较“鸡肋”。在 v2 版本中将 crc 的字段从 Record 中转移到了 RecordBatch 中。

        v2 版本对消息集(RecordBatch)做了彻底的修改,参考上图最左部分,除了刚刚提及的 crc 字段,还多了如下字段。

  • first offset:表示当前 RecordBatch 的起始位移。
  • length:计算从 partition leader epoch 字段开始到末尾的长度。
  • partition leader epoch:分区 leader 纪元,可以看作分区 leader 的版本号或更新次数,详细内容请参考16节。
  • magic:消息格式的版本号,对 v2 版本而言,magic 等于2。
  • attributes:消息属性,注意这里占用了两个字节。低3位表示压缩格式,可以参考 v0 和 v1;第4位表示时间戳类型;第5位表示此 RecordBatch 是否处于事务中,0表示非事务,1表示事务。第6位表示是否是控制消息(ControlBatch),0表示非控制消息,而1表示是控制消息,控制消息用来支持事务功能,详细内容请参考14节。
  • last offset delta:RecordBatch 中最后一个 Record 的 offset 与 first offset 的差值。主要被 broker 用来确保 RecordBatch 中 Record 组装的正确性。
  • first timestamp:RecordBatch 中第一条 Record 的时间戳。
  • max timestamp:RecordBatch 中最大的时间戳,一般情况下是指最后一个 Record 的时间戳,和 last offset delta 的作用一样,用来确保消息组装的正确性。
  • producer id:PID,用来支持幂等和事务,详细内容请参考14节。
  • producer epoch:和 producer id 一样,用来支持幂等和事务,详细内容请参考14节。
  • first sequence:和 producer id、producer epoch 一样,用来支持幂等和事务,详细内容请参考14节。
  • records count:RecordBatch 中R ecord 的个数。

      为了验证这个格式的正确性,我们往某个分区中一次性发送6条 key 为“key”、value 为“value”的消息,相应的日志内容如下:

0000 0000 0000 0000 0000 0090 0000 0000
0207 3fbb 9a00 0000 0000 0500 0001 6363
9e4c cc00 0001 6363 9e4e 7bff ffff ffff
ffff ffff ffff ffff ff00 0000 061c 0000
0006 6b65 790a 7661 6c75 6500 1e00 d406
0206 6b65 790a 7661 6c75 6500 1e00 d806
0406 6b65 790a 7661 6c75 6500 1e00 da06
0606 6b65 790a 7661 6c75 6500 1e00 dc06
0806 6b65 790a 7661 6c75 6500 1e00 de06
0a06 6b65 790a 7661 6c75 6500

        可以看到全部是以16进制数来表示的,未免晦涩难懂,下面对照上图来详细讲解每个字节所表示的具体含义,具体参考如下:

0000 0000 0000 0000             first offset = 0           RecordBatch
0000 0090			length = 144
0000 0000			partition leader epoch = 0
02				magic = 2
07 3fbb 9a			crc
00 00				attributes
00 0000 05			last offset delta = 5
00 0001 6363 9e4c cc            first timestamp = ‭1526384708812‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬
00 0001 6363 9e4e 7b            max timestamp = ‭1526384709243‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬‬
ff ffff ffff ffff ff            producer id = -1
ff ff				producer epoch = -1
ff ffff ff			first sequence = 0
00 0000 06			records count = 6     
----------------------------------------------------------------------------
1c				length = readVaint(0x1c) = 14		第1个Record
00				attributes
00				timestamp delta = readVaint(0x00) = 0
00				offset delta = readVaint(0x00) = 0
06				key length = readVaint(0x06) = 3
6b65 79				key = "key" 查ASCII码表可知:'k'->0x6b 'e'->0x65 'y'->0x79
0a				value length = readVaint(0x0a) = 5
7661 6c75 65		        value = "value" 查ASCII码表(略)
00				headers count = readVaint(0x00) = 0
----------------------------------------------------------------------------
1e				length = readVaint(0x1e) = 15		第2个Record
00				attributes
d406				timestamp delta = readVaint(d406) = 426
02				offset delta = readVaint(0x02) = 1
06				key length = readVaint(0x06) = 3
6b65 79				key = "key"
0a				value length = readVaint(0x0a) = 5
7661 6c75 65		        value = "value"
00				headers count = readVaint(0x00) = 0
----------------------------------------------------------------------------
1e00 d806 0406 6b65 790a 7661 6c75 6500         第3个Record
1e00 da06 0606 6b65 790a 7661 6c75 6500         第4个Record
1e00 dc06 0806 6b65 790a 7661 6c75 6500         第5个Record
1e00 de06 0a06 6b65 790a 7661 6c75 6500         第6个Record

        这里我们再来做一个测试,在2.0.0版本的 Kafka 中创建一个分区数和副本因子数都为1的主题,名称为“msg_format_v2”。然后同样插入一条 key = "key"、value = "value"的消息,日志结果如下:

[root@node1 kafka_2.11-2.0.0]# bin/kafka-dump-log.sh --files 
     /tmp/kafka-logs/msg_format_v2-0/00000000000000000000.log 
Dumping /tmp/kafka-logs/msg_format_v2-0/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 0 CreateTime: 1538049867325 isvalid: true size: 76 magic: 2 compresscodec: NONE crc: 1494132791

[root@node1 kafka_2.11-2.0.0]# bin/kafka-dump-log.sh --files 
     /tmp/kafka-logs/msg_format_v2-0/00000000000000000000.log --print-data-log
Dumping /tmp/kafka-logs/msg_format_v2-0/00000000000000000000.log
Starting offset: 0
offset: 0 position: 0 CreateTime: 1538049867325 isvalid: true keysize: 3 valuesize: 5 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: key payload: value

      可以看到示例中 size 字段为76,我们根据 v2 版本的日志格式来验证一下,Record Batch Header 部分共61B。Record 部分中的 attributes 占1B;timestamp delta 的值为0,占1B;offset delta 的值为0,占1B;key length 的值为3,占1B;key占3B;value length 的值为5,占1B,value 占5B;headers count 的值为0,占1B;无 headers。Record 部分的总长度 = 1B+1B+1B+1B+3B+1B+5B+1B = 14B,所以 Record 的 length 字段的值为14,编码变为长整型数之后占1B。最后推导出这条消息的占用字节数 = 61B+14B+1B = 76B,符合测试结果。同样再发一条 key = null、value = "value"的消息,可以计算出这条消息占73B。

      这么看上去 v2 版本的消息好像要比之前版本的消息所占用的空间大得多,的确对单条消息而言是这样的,如果我们连续向主题 msg_format_v2 中再发送10条 value 长度为6、key 为 null 的消息,可以得到:

baseOffset: 2 lastOffset: 11 baseSequence: -1 lastSequence: -1 producerId: -1
 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 149
 CreateTime: 1524712213771 isvalid: true size: 191 magic: 2 compresscodec: NONE
 crc: 820363253

      本来应该占用740B大小的空间,实际上只占用了191B,在 v0 版本中这10条消息需要占用320B的空间大小,而 v1 版本则需要占用400B的空间大小,这样看来 v2 版本又节省了很多空间,因为它将多个消息(Record)打包存放到单个 RecordBatch 中,又通过 Varints 编码极大地节省了空间。有兴趣的读者可以自行测试一下在大批量消息的情况下,v2 版本和其他版本消息占用大小的对比,比如往主题 msg_format_v0 和 msg_format_v2 中各自发送100万条1KB的消息。

       v2 版本的消息不仅提供了更多的功能,比如事务、幂等性等,某些情况下还减少了消息的空间占用,总体性能提升很大。

         细心的读者可能注意到前面在演示如何查看日志内容时,既使用了 kafka-run-class.sh kafka.tools.DumpLogSegments 的方式,又使用了 kafka-dump-log.sh 的方式。而 kafka-dump-log.sh 脚本的内容为:

exec $(dirname $0)/kafka-run-class.sh kafka.tools.DumpLogSegments "$@"

         两种方式在本质上没有什么区别,只不过在 Kafka 2.0.0之前并没有 kafka-dump-log.sh 脚本,所以只能使用 kafka-run-class.sh kafka.tools.DumpLogSegments的形式,而从 Kafka 2.0.0开始,可以直接使用 kafka-dump-log.sh 脚本来避免书写错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值