Kafka的消息格式

插个Kafka在zookeeper中存储结构图

新版本已经有所变化 在这里插入图片描述

插个kafka old|new API的对比和常用shell命令

kafka: old API和new API,不可跨查询。 基于zookeeper和broker。
Old Producer API/Old Consumer API New Producer API/New Consumer API
下面 8.96.143.20:24002 都可以用 8.96.143.20:24002,8.96.143.21:24002,8.96.143.22:24002 代替
8.96.143.20:21007 都可以用 8.96.143.20:21007,8.96.143.21:21007,8.96.143.22:21007 代替

topic 查看:
kafka-topics.sh --list --zookeeper 8.96.143.20:24002/kafka
kafka-topics.sh --describe --zookeeper 8.96.143.20:24002/kafka --topic newtopic
kafka-topics.sh --delete --zookeeper 8.96.143.20:24002/kafka --topic newtopic
kafka-topics.sh --create --zookeeper 8.96.143.20:24002/kafka --partitions 6 --replication-factor 2 --topic newtopic

group查看:
kafka-consumer-groups.sh --zookeeper 8.96.143.20:24002,8.96.143.21:24002,8.96.143.22:24002/kafka --list
kafka-consumer-groups.sh --list --bootstrap-server 8.96.143.20:21007,8.96.143.21:21007,8.96.143.22:21007 --new-consumer --command-config config/consumer.properties

group offset查看:
kafka-consumer-groups.sh --zookeeper 8.96.143.20:24002/kafka --group example-group1 --describe
kafka-consumer-groups.sh --describe --bootstrap-server 8.96.143.20:21007 --new-consumer --group example-group1 --command-config config/consumer.properties

consumer消费topic:
kafka-console-consumer.sh --zookeeper 8.96.143.20:24002/kafka --topic newtopic --from-beginning 环境中好像命令执行不成功
kafka-console-consumer.sh --topic newtopic --bootstrap-server 8.96.143.20:21007 --new-consumer --consumer.config config/consumer.properties

producer 生产消息:
kafka-console-producer.sh --broker-list 8.96.143.20:21005 --topic newtopic --old-producer -sync 这种FIM好像不行了
kafka-console-producer.sh --broker-list 8.96.143.20:21007 --topic newtopic --producer.config config/producer.properties
生产的消息不一定插入 8.96.143.20 的leader 分区。依然是根据均衡算法在集群中选择分区。

配置一个8.96.143.20 broker节点能获取所有节点信息. broker节点可以获取集群信息

默认topic:
__default_metrics 一些元数据
__consumer_offsets (存放group offset,用于new API,offset存放在broker topic上)

参考博客:
KAFKA OFFSET的存储问题
Kafka基础
Kafka知识

Kafka的消息格式

转自https://www.cnblogs.com/devos/p/5100611.html
参考https://www.cnblogs.com/qwangxiao/p/9043491.html

Commit Log

Kafka储存消息的文件被它叫做log,按照Kafka文档的说法是:

Each partition is an ordered, immutable sequence of messages that is continually appended to—a commit log

这反应出来的Kafka的行为是:消息被不断地append到文件末尾,而且消息是不可变的。

这种行为源于Kafka想要实现的功能:高吞吐量,多副本,消息持久化。这种简单的log形式的文件结构能够更好地实现这些功能,不过也会在其它方面有所欠缺,比如检索消息的能力。

而Kafka的行为也决定了它的消息格式。对于Kafka来说,消息的主体部分的格式在网络传输中和磁盘上是一致的,也就是说消息的主体部分可以直接从网络读取的字节buffer中写入到文件(部分情况下),也可以直接从文件中copy到网络,而不需要在程序中再加工,这有利于降低服务器端的开销,以及提高IO速度(比如使用zero-copy的传输)。

这也就决定了Kafka的消息格式必须是适于被直接append到文件中的。当然啥都可以append到文件后面,问题在于怎么从文件中拆分出来一条条记录。

记录的划分以及消息的格式

对于日志来说,一条记录以"\n"结尾,或者通过其它特定的分隔符分隔,这样就可以从文件中拆分出一条一条的记录,不过这种格式更适用于文本,对于Kafka来说,需要的是二进制的格式。所以,Kafka使用了另一种经典的格式:在消息前面固定长度的几个字节记录下这条消息的大小(以byte记),所以Kafka的记录格式变成了:

Offset MessageSize Message

消息被以这样格式append到文件里,在读的时候通过MessageSize可以确定一条消息的边界。

需要注意的是,在Kafka的文档以及源码中,消息(Message)并不包括它的offset。Kafka的log是由一条一条的记录构成的,Kafka并没有给这种记录起个专门的名字,但是需要记住的是这个“记录”并不等于"Message"。Offset MessageSize Message加在一起,构成一条记录。而在Kafka Protocol中,Message具体的格式为

Message => Crc MagicByte Attributes Key Value
   Crc => int32
   MagicByte => int8
   Attributes => int8
   Key => bytes
   Value => bytes

各个部分的含义是

Field

Description

Attributes

This byte holds metadata attributes about the message. The lowest 2 bits contain the compression codec used for the message. The other bits should be set to 0.

Crc

The CRC is the CRC32 of the remainder of the message bytes. This is used to check the integrity of the message on the broker and consumer.

Key

The key is an optional message key that was used for partition assignment. The key can be null.

MagicByte

This is a version id used to allow backwards compatible evolution of the message binary format. The current value is 0.

Offset

This is the offset used in kafka as the log sequence number. When the producer is sending messages it doesn't actually know the offset and can fill in any value here it likes.

Value

The value is the actual message contents as an opaque byte array. Kafka supports recursive messages in which case this may itself contain a message set. The message can be null.

 

MessageSet

之所以要强调记录与Message的区别,是为了更好地理解MessageSet的概念。Kafka protocol里对于MessageSet的定义是这样的

MessageSet => [Offset MessageSize Message]
   Offset => int64
   MessageSize => int32

也就是说MessageSet是由多条记录组成的,而不是消息,这就决定了一个MessageSet实际上不需要借助其它信息就可以从它对应的字节流中切分出消息,而这决定了更重要的性质:Kafka的压缩是以MessageSet为单位的。而以MessageSet为单位压缩,决定了对于压缩后的MessageSet,不需要在它的外部记录这个MessageSet的结构,也就决定了Kafka的消息是可以递归包含的,也就是前边"value"字段的说明“Kafka supports recursive messages in which case this may itself contain a message set"。

具体地说,对于Kafka来说,可以对一个MessageSet做为整体压缩,把压缩后得到的字节数组作为一条Message的value。于是,Message既可以表示未压缩的单条消息,也可以表示压缩后的MessageSet。

压缩后的消息的读取

就看Message头部的Attributes里的压缩格式标识。说到这个,得说下递归包含的事情,理论上,一个压缩的的MessageSet里的一个Message可能会是另一个压缩后的MessageSet,或者包含更深层的MessageSet。但是实际上,Kafka中的一个Message最多只含有一个MessageSet。从Message中读取MessageSet的逻辑,可以在ByteBufferMessageSet的internalIterator方法中找到:

        if(isShallow) { //是否要进行深层迭代
          new MessageAndOffset(newMessage, offset)
        } else { //如果要深层迭代的话
          newMessage.compressionCodec match {
            case NoCompressionCodec =>
              innerIter = null
              new MessageAndOffset(newMessage, offset) //如果这个Message没有压缩,就直接把它作为一个Message返回
            case _ =>
              innerIter = ByteBufferMessageSet.deepIterator(newMessage) //如果这个Message采用了压缩,就对它进行深层迭代
              if(!innerIter.hasNext)
                innerIter = null
              makeNext()
          }
        }

而ByteBufferMessageSet的deepIterator方法就是对这个Message的value进行解压,然后从中按照Offset MessageSize Message的格式读取一条条记录,对于这次读取的Message,就不再进行深层迭代了。下面是deepIterator的makeNext方法,它被不断调用以生成迭代器的元素

      override def makeNext(): MessageAndOffset = {
        try {
          // read the offset
          val offset = compressed.readLong()
          // read record size
          val size = compressed.readInt()

          if (size < Message.MinHeaderSize)
            throw new InvalidMessageException("Message found with corrupt size (" + size + ") in deep iterator")

          // read the record into an intermediate record buffer
          // and hence has to do extra copy
          val bufferArray = new Array[Byte](size)
          compressed.readFully(bufferArray, 0, size)
          val buffer = ByteBuffer.wrap(bufferArray)

          val newMessage = new Message(buffer)

          // the decompressed message should not be a wrapper message since we do not allow nested compression
          new MessageAndOffset(newMessage, offset)
        } catch {
          case eofe: EOFException =>
            compressed.close()
            allDone()
          case ioe: IOException =>
            throw new KafkaException(ioe)
        }
      }

KAFKA-1718

至于一个MessageSet中不能包含多个压缩后的Message(压缩后的Message也就是以压缩后的MessageSet作为value的Message),Kafka Protocol中是这么说的

The outer MessageSet should contain only one compressed "Message" (see KAFKA-1718 for details).

KAFKA-1718就是在Protocol里添加这么一个特殊说明的原因。事情是这样的:

报各这个问题的人是Go语言client的作者,他发现自己发的Message明显没有过大,但是发生了MessageSizeTooLargeException。后来跟其它人讨论,发现是因为broker端在调用Log.append时,会把传送给这个方法的MessageSet解压开,然后再组合成一个压缩后的MessageSet(ByteBufferMessageSet)。而Go语言的客户端发送的MessageSet中包含了多个压缩后的Message,这样即使发送时的Message不会超过message.max.bytes的限制,但是broker端再次生成的Message就超过了这个限制。所以,Kafka Protocol对这种情况做了特殊说明:The outer MessageSet should contain only one compressed "Message"。

Compressed Message的offset

即然可以把压缩后的MessageSet作为Message的value,那么这个Message的offset该如何设置呢?

这个offset的值只有两种可能:1, 被压缩的MessageSet里Message的最大offset; 2, 被压缩的MessageSet里Message的最小offset.

这两种取值没有功能的不同,只有效率的不同。

由于FetchRequest协议中的offset是要求broker提供大于等于这个offset的消息,因此broker会检查log,找到符合条件的,然后传输出去。那么由于FetchRequest中的offset位置的消息可位于一个compressed message中,所以broker需要确定一个compressed Message是否需要被包含在respone中。

  • 如果compressed Message的offset是它包含的MessageSet的最小offset。那么,我们对于这个Message是否应包含在response中,无法给出"是”或"否“的回答。比如FetchRequest中指明的开始读取的offset是14,而一个compressed Message的offset是13,那么这个Message中可能包含offset为14的消息,也可能不包含。
  • 如果compressed Message的offset是它包含的MessageSet的最大offset,那么,可以根据这个offset确定这个Message“不应该”包含在response中。比如FetchRequest中指明的开始读取的offset是14,那么如果一个compressed Message的offset是13,那它就不该被包含在response中。而当我们顺序排除这种不符合条件的Message,就可以找到第一个应该被包含在response中的Message(压缩或者未压缩), 从它开始读取。

在第一种情况下(最小offset),我们尽管可以通过连续的两个Message确定第一个Message的offset范围,但是这样在读取时需要在读取第二个Message的offset之后跳回到第一个Message,  这通常会使得最近一次读(也就读第二个offset)的文件系统的缓存失效。而且逻辑比第二种情况更复杂。在第二种情况下,broker只需要找到第一个其offset大于或等于目标offset的Message,从它可以读取即可,而且也通常能利用到文件系统缓存,因为offset和消息内容有可能在同一个缓存块中。

在处理FetchRequest时,broker的逻辑也正是如此。对FetchRequest的处理会调用到Log#read(startOffset: Long, maxLength: Int, maxOffset: Option[Long] = None)方法,然后调用到LogSegment的read方法,它的之后的调用有很多,所有不贴代码了,它的注释说明了读取的逻辑

* Read a message set from this segment beginning with the first offset >= startOffset. The message set will include
* no more than maxSize bytes and will end before maxOffset if a maxOffset is specified

即,返回的MessageSet的第一条Message的offset >= startOffset。

而在broker给compressed Message赋予offset时,其逻辑也是赋予其包含的messages中的最大offset。这段逻辑在ByteBufferMessageSet的create方法中:

      messageWriter.write(codec = compressionCodec) { outputStream =>
        val output = new DataOutputStream(CompressionFactory(compressionCodec, outputStream)) //创建压缩流
        try {
          for (message <- messages) {
            offset = offsetCounter.getAndIncrement //offsetCounter是一个AtomicLong,使用它的当前值作为这条Message的offset,然后+1作为下一条消息的offset
            output.writeLong(offset)//写入这条日志记录的offset
            output.writeInt(message.size)//写入这条日志记录的大小
            output.write(message.buffer.array, message.buffer.arrayOffset, message.buffer.limit) //写入这条记录的Message
          }
        } finally {
          output.close()
        }
      }
      val buffer = ByteBuffer.allocate(messageWriter.size + MessageSet.LogOverhead)
      writeMessage(buffer, messageWriter, offset)//以最后一个Message的offset作为这个compressed Message的offset
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值