1.为什么压缩:
压缩秉承了用时间换空间的经典trade-off思想,即用CPU的时间去换取磁盘空间或网络I/O传输量,Kafka的压缩算法也是出于这种目的。
2.怎么压缩:
了解Kafka如何压缩消息,首先要清楚Kafka的消息格式,目前kafka有两大类消息格式,社区称之为V1版本和V2版本。
v1版本
字段含义
1.CRC(4B):CRC校验码,占用4个字节,校验magic至value之间字节是否被篡改
2.magic(1B):消息格式版本号,占用1个字节。V0版本是0,V1版本是1,V2版本是2
3.attributes(1B):属性字段,占用1个字节,只使用低3位表示消息的压缩类型, 0表示NONE,表示不启用压缩。1表示GZIP。2表示SNAPPY。3表示LZ4。 且第4个bit被利用起来:0表示timestamp类型为CreateTime,而1表示tImestamp类型为LogAppendTime,其他位保留。
4.timestamp(8B):时间戳
5.key length(4B):表示消息的key的长度。如果为-1,则表示没有设置key,即key=null
6.key:消息key,长度由key length值指定。如果key length值是-1,则无key(没有此字段)
7.value length(4B):表示消息的长度,占用4字节。
8.value:消息value,长度由value length值指定。如果value length值是-1,则无value,消息没有该字段。
上图中的RECORD就是消息v1版本的消息格式,大多数人将offset和message size字段都都看成是消息,因为每个Record(v1版)必定对应一个offset和message size。每条消息都一个offset用来标志它在partition中的偏移量,这个offset是逻辑值,而非实际物理偏移值,message size表示消息的大小,这两者的一起被称之为日志头部(LOG_OVERHEAD),固定为12B。LOG_OVERHEAD和RECORD一起用来描述一条消息。与消息对应的还有消息集的概念,消息集中包含一条或者多条消息,消息集不仅是存储于磁盘以及在网络上传输(Produce & Fetch)的基本形式,而且是kafka中压缩的基本单元,详细结构参考上右图。
v1版本的最小消息长度(RECORD_OVERHEAD_V1)为crc32+magic+timestamp+attributes + key length + value length = 4+1+8+1+4+4=22B,也就是说v1版本中一条消息的最小长度为22B,如果小于这个值,那么这就是一条破损的消息而不被接受。
例子 发送一条key="key",value="value"的消息,那么此条消息会占用 LOG_OVERHEAD + RECORD_OVERHEAD_V1 + 3B的key + 5B的value = 12 + 22 + 3 + 5 = 42B。
V2版本
v2版本中消息集谓之为Record Batch,而不是先前的Message Set了,其内部也包含了一条或者多条消息,消息的格式参见下图中部和右部。在消息压缩的情形下,Record Batch Header部分(参见下图左部,从first offset到records count字段)是不被压缩的,而被压缩的是records字段中的所有内容。
先说一下,Varint是一种使用一个或多个字节序列化整数的方法,会把整数编码为变长字节。对于32位整型数据经过Varint编码后需要15个字节,小的数字使用1个byte,大的数字使用5个bytes。64位整型数据编码后占用110个字节。在实际场景中小数字的使用率远远多于大数字,因此通过Varint编码对于大部分场景都可以起到很好的压缩效果。
Record的关键字段,内部字段大量采用了Varints,这样Kafka可以根据具体的值来确定需要几个字节来保存。v2版本的消息格式去掉了crc字段,另外增加了length(消息总长度)、timestamp delta(时间戳增量)、offset delta(位移增量)和headers信息,并且attributes被弃用了。
字段分析:
1.length:消息总长度。
2.attributes:弃用,但是还是在消息格式中占据1B的大小,以备未来的格式扩展。
3.timestamp delta:时间戳增量。通常一个timestamp需要占用8个字节,如果像这里保存与RecordBatch的起始时间戳的差值的话可以进一步的节省占用的字节数。
4.offset delta:位移增量。保存与RecordBatch起始位移的差值,可以节省占用的字节数。
5.headers:这个字段用来支持应用级别的扩展,而不需要像v0和v1版本一样不得不将一些应用级别的属性值嵌入在消息体里面。Header的格式如上图最有,包含key和value,一个Record里面可以包含0至多个Header。具体可以参考以下KIP-82。
crc移动到了RrcordBatch中
原来在 V1 版本中,每条消息都需要执行 CRC 校验,但有些情况下消息的 CRC 值是会发生变化的。比如在 Broker 端可能会对消息时间戳字段进行更新(例如用户指定的timestamp类型是LogAppendTime而不是CreateTime),那么重新计算之后的 CRC 值也会相应更新;再比如 Broker 端在执行消息格式转换时(主要是为了兼容老版本客户端程序),也会带来 CRC 值的变化。鉴于这些情况,再对每条消息都执行 CRC 校验就有点没必要了,不仅浪费空间还耽误 CPU 时间,因此在 V2 版本中,消息的 CRC 校验工作就被移到了消息集合这一层(RecordBatch)。
v2消息集的修改(彻底)
1.first offset:表示当前RecordBatch的起始位移。
2.length:计算partition leader epoch到headers之间的长度。
3.partition leader epoch:用来确保数据可靠性,详细可以参考KIP-101
4.magic:消息格式的版本号,对于v2版本而言,magic等于2。
5.attributes:消息属性,注意这里占用了两个字节。低3位表示压缩格式,可以参考v0和v1;第4位表示时间戳类型;第5位表示此RecordBatch是否处于事务中,0表示非事务,1表示事务。第6位表示是否是Control消息,0表示非Control消息,而1表示是Control消息,Control消息用来支持事务功能。
6.last offset delta:RecordBatch中最后一个Record的offset与first offset的差值。主要被broker用来确认RecordBatch中Records的组装正确性。
7.first timestamp:RecordBatch中第一条Record的时间戳。
8.max timestamp:RecordBatch中最大的时间戳,一般情况下是指最后一个Record的时间戳,和last offset delta的作用一样,用来确保消息组装的正确性。
9.producer id:用来支持幂等性,详细可以参考KIP-98。
10.producer epoch:和producer id一样,用来支持幂等性。
11.first sequence:和producer id、producer epoch一样,用来支持幂等性。
12.records count:RecordBatch中Record的个数。
例子:v2相比于v1节省空间了吗?
插入一条key="key",value="value"的消息,按照格式验证一下,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部分长度为(1+1+1+1+3+1+5+1=14B),所以Record的length字段值为14,编码为变长整型占1B。最后总消息占用字节数为=61+14+1=76
这么看上去好像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编码极大的节省了空间。
消息压缩
消息压缩是将整个消息集进行压缩而作为内层消息(inner message),内层消息整体作为外层(wrapper message)的value。结构如上图所示。压缩后的外层消息(wrapper message)中的key为null,所以图中没有画出那一部分。
压缩对于offset的操作: 当生产者创建压缩消息的时候,对内部压缩消息设置的offset是从0开始为每个内部消息分配offset,详细可以参考下图右部:
其实每个从生产者发出的消息集中的消息offset都是从0开始的,当然这个offset不能直接存储在日志文件中,对offset进行转换时在服务端进行的,客户端不需要做这个工作。外层消息保存了内层消息中最后一条消息的绝对位移(absolute offset),绝对位移是指相对于整个partition而言的。参考上图,对于未压缩的情形,图右内层消息最后一条的offset理应是1030,但是被压缩之后就变成了5,而这个1030被赋予给了外层的offset。当消费者消费这个消息集的时候,首先解压缩整个消息集,然后找到内层消息中最后一条消息的inner offset,然后根据如下公式找到内层消息中最后一条消息前面的消息的absolute offset(RO表示Relative Offset,IO表示Inner Offset,而AO表示Absolute Offset):
例子:
如上图:如果不压缩的话,那么内层消息应该为(1025,1026,1027,1028,1029,1030),压缩后变为(0,1,2,3,4,5),将1030赋给外层。
那么解压如何如何确定呢,利用(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
举例:找1030的起始偏移量:先解压整个消息集,找到最后一条消息的偏移量1030,找到内层的偏移量,0-5, so RO = 0-5 = -5 AO = 1030 + (-5) = 1025.
v1和v2版本异同
A:相同点:
(1):Kafka的消息层次分为:消息集合(message set,Record Batch)和消息(message,Record);一个消息集合中包含若干条日志项(record item),而日志项才是真正封装消息的地方。
(2):Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体的一条条消息,他总是在消息集合这个层面上进行写入操作。
B:不同点:引入V2的目的主要是针对V1版本的一些弊端做了修正
(1)把消息的公共部分抽取出来放到外层集合里。 如:在V1中每条消息都要执行CRC校验(循环冗余校验),有些情况下消息的CRC值会变,对每条消息都执行CRC校验,不仅浪费空间还耽误CPU时间。 v2中,消息的crc校验工作被移到了消息集合这一层。
(2)保存压缩消息的方法发生了变化: v1把多条消息进行压缩后在保存到外层消息的消息体字段中。 v2 对整个消息集合进行压缩,压缩效果好与前者。
何时压缩?
在 Kafka 中,压缩可能发生在两个地方:生产者端和 Broker 端。生产者程序中配置 compression.type 参数即表示启用指定类型的压缩算法。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 开启GZIP压缩
props.put("compression.type", "gzip");
Producer<String, String> producer = new KafkaProducer<>(props);
这样 Producer 启动后生产的每个消息集合都是经 GZIP 压缩过的,故而能很好地节省网络传输带宽以及 Kafka Broker 端的磁盘占用。
Broker端采用压缩算法的情况
1.Broker 端指定了和 Producer 端不同的压缩算法。这会导致Broker端接收到生产者发来的压缩消息,Broker端重新解压、在压缩。
2.Broker 端发生了消息格式转换。这种转换主要是为了兼容老版本的消费者程序,(v1和v2的差别)。这个过程会涉及消息的解压和重新压缩。这不仅对性能影响很大,还会让Kafka丧失引以为豪的Zero Copy特性。
何时解压
通常情况下解压发生在消费者端。
A:这个流程是Producer发送的压缩消息到Broker,Broker原封不动的保存起来,当Consumer程序请求这部分消息时,Broker原样发出去,当下消息到的Consumer端后,由Consumer自行解压。
B:Consume之所以知道这些消息是用何种压缩算法的,是因为Kafka会将启用了哪种压缩算法封装到消息集合中,当Consumer读取到消息集合时,就知道了。
压缩算法对比
在Kafka2.1.0版本之前,仅支持GZIP,Snappy和LZ4。2.1.0后还支持Zstandard算法(Facebook开源,能够提供超高压缩比)。
A:一个压缩算法的优劣,有两个重要指标:压缩比和压缩/解压缩吞吐量,两者都是越高越好。
B:吞吐量:LZ4>Snappy>zstd和GZIP,压缩比:zstd>LZ4>GZIP>Snappy
启用压缩情况
A:启用压缩的一个条件是Producer端所在机器CPU资源充裕
B:生产环境网络带宽资源有限
C:尽量不要出现消息格式转换的情况。