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来说,消息的主体部分的格式在网络传输中和磁盘上是一致的,也就是说消息的主体部分可以直接从网络读取的字节buffer中写入到文件(部分情况下),也可以直接从文件中copy到网络,而不需要在程序中再加工,这有利于降低服务器端的开销,以及提高IO速度(比如使用zero-copy的传输)。
Kafka的Producer、Broker和Consumer之间采用的是一套自行设计的基于TCP层的协议。Kafka的这套协议完全是为了Kafka自身的业务需求而定制的,而非要实现一套类似于Protocol Buffer的通用协议。
记录的划分以及消息的格式
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
各个部分的含义是:
名称 | 类型 | 描述 |
---|---|---|
CRC | int32 | 表示这条消息(不包括CRC字段本身,不包括记录的offset和MessageSize部分)的校验码。 |
MagicByte | int8 | 表示消息格式的版本,用来做后向兼容,目前值为0。 |
Attributes | int8 | 表示这条消息的元数据,目前最低两位用来表示压缩格式。 |
Key | bytes | 表示这条消息的Key,可以为null。 |
Value | bytes | 表示这条消息的Value。Kafka支持消息嵌套,也就是把一条消息作为Value放到另外一条消息里面。 |
MessageSet
之所以要强调记录与Message的区别,是为了更好地理解MessageSet的概念。Kafka protocol里对于MessageSet的定义是这样的:
MessageSet => [Offset MessageSize Message]
Offset => int64
MessageSize => int32
各部分的含义:
名称 | 类型 | 描述 |
---|---|---|
Offset | int64 | 它用来作为log中的序列号,Producer在生产消息的时候还不知道具体的值是什么,可以随便填个数字进去 |
MessageSize | int32 | 表示这条Message的大小 |
Message | - | 表示这条Message的具体内容,其格式见上一小节。 |
也就是说MessageSet是由多条记录组成的,而不是消息,而这决定了更重要的性质:Kafka的压缩是以MessageSet为单位的,也就决定了Kafka的消息是可以递归包含的。
具体地说,对于Kafka来说,可以对一个MessageSet做为整体压缩,把压缩后得到的字节数组作为一条Message的value。于是,Message既可以表示未压缩的单条消息,也可以表示压缩后的MessageSet。
Message的压缩
Kafka支持下面几种压缩方式,
压缩方式 | 编码 |
---|---|
不压缩 | 0 |
Gzip | 1 |
Snappy | 2 |
LZ4 | 3 |
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和消息内容有可能在同一个缓存块中。
实际在broker给compressed Message赋予offset时,其逻辑也是赋予其包含的messages中的最大offset。
Validate Message
什么需要验证?
首先,网络传输过程中,数据可能会产生错误,即使是写在磁盘上的消息,也可能会由于磁盘的问题产生错误。因此,broker对接收到的消息需要验证其完整性。这里的消息就是前边协议里定义的Message。
对于消息完整性的检测,是使用CRC32校验,但是并不是对消息的所有部分计算CRC,而是对Message的Crc部分以后的部分,不包括记录的offset和MessageSize部分。把offset和MessageSize加到CRC计算中,可以对完整性有更强的估证,但是坏处在于这两个部分在消息由producer到达broker以后,会被broker重写,因此如果把它们计算在crc里边,就需要在broker端重新计算crc32,这样会带来额外的开销。
除了消息的完整性,还需要对消息的合规性进行检验,主要是检验offset是否是单调增长的,以及MessageSize是超过了最大值。
这里检验时使用的MessageSize就不是Message本身的大小了,而是一个记录的大小,包括offset和MessageSize。
何时需要验证?
在broker把收到的producer request里的MessageSet append到Log之前,以及consumer和follower获取消息之后,都需要进行校验。
这种情况分成两种:
1. broker和consumer把收到的消息append到log之前
2. consumser收到消息后
参考: