gitbook/Kafka核心技术与实战/docs/102132.md
2022-09-03 22:05:03 +08:00

12 KiB
Raw Blame History

10 | 生产者压缩算法面面观

你好,我是胡夕。今天我要和你分享的内容是:生产者压缩算法面面观。

说起压缩compression我相信你一定不会感到陌生。它秉承了用时间去换空间的经典trade-off思想具体来说就是用CPU时间去换磁盘空间或网络I/O传输量希望以较小的CPU开销带来更少的磁盘占用或更少的网络I/O传输。在Kafka中压缩也是用来做这件事的。今天我就来跟你分享一下Kafka中压缩的那些事儿。

怎么压缩?

Kafka是如何压缩消息的呢要弄清楚这个问题就要从Kafka的消息格式说起了。目前Kafka共有两大类消息格式社区分别称之为V1版本和V2版本。V2版本是Kafka 0.11.0.0中正式引入的。

不论是哪个版本Kafka的消息层次都分为两层消息集合message set以及消息message。一个消息集合中包含若干条日志项record item而日志项才是真正封装消息的地方。Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体的一条条消息它总是在消息集合这个层面上进行写入操作。

那么社区引入V2版本的目的是什么呢V2版本主要是针对V1版本的一些弊端做了修正和我们今天讨论的主题相关的修正有哪些呢先介绍一个就是把消息的公共部分抽取出来放到外层消息集合里面这样就不用每条消息都保存这些信息了。

我来举个例子。原来在V1版本中每条消息都需要执行CRC校验但有些情况下消息的CRC值是会发生变化的。比如在Broker端可能会对消息时间戳字段进行更新那么重新计算之后的CRC值也会相应更新再比如Broker端在执行消息格式转换时主要是为了兼容老版本客户端程序也会带来CRC值的变化。鉴于这些情况再对每条消息都执行CRC校验就有点没必要了不仅浪费空间还耽误CPU时间因此在V2版本中消息的CRC校验工作就被移到了消息集合这一层。

V2版本还有一个和压缩息息相关的改进就是保存压缩消息的方法发生了变化。之前V1版本中保存压缩消息的方法是把多条消息进行压缩然后保存到外层消息的消息体字段中而V2版本的做法是对整个消息集合进行压缩。显然后者应该比前者有更好的压缩效果。

我对两个版本分别做了一个简单的测试结果显示在相同条件下不论是否启用压缩V2版本都比V1版本节省磁盘空间。当启用压缩时这种节省空间的效果更加明显就像下面这两张图展示的那样

何时压缩?

在Kafka中压缩可能发生在两个地方生产者端和Broker端。

生产者程序中配置compression.type参数即表示启用指定类型的压缩算法。比如下面这段程序代码展示了如何构建一个开启GZIP的Producer对象

 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);

这里比较关键的代码行是props.put(“compression.type”, “gzip”)它表明该Producer的压缩算法使用的是GZIP。这样Producer启动后生产的每个消息集合都是经GZIP压缩过的故而能很好地节省网络传输带宽以及Kafka Broker端的磁盘占用。

在生产者端启用压缩是很自然的想法那为什么我说在Broker端也可能进行压缩呢其实大部分情况下Broker从Producer端接收到消息后仅仅是原封不动地保存而不会对其进行任何修改但这里的“大部分情况”也是要满足一定条件的。有两种例外情况就可能让Broker重新压缩消息。

情况一Broker端指定了和Producer端不同的压缩算法。

先看一个例子。想象这样一个对话。

Producer说“我要使用GZIP进行压缩。”

Broker说“不好意思我这边接收的消息必须使用Snappy算法进行压缩。”

你看这种情况下Broker接收到GZIP压缩消息后只能解压缩然后使用Snappy重新压缩一遍。如果你翻开Kafka官网你会发现Broker端也有一个参数叫compression.type和上面那个例子中的同名。但是这个参数的默认值是producer这表示Broker端会“尊重”Producer端使用的压缩算法。可一旦你在Broker端设置了不同的compression.type值就一定要小心了因为可能会发生预料之外的压缩/解压缩操作通常表现为Broker端CPU使用率飙升。

情况二Broker端发生了消息格式转换。

所谓的消息格式转换主要是为了兼容老版本的消费者程序。还记得之前说过的V1、V2版本吧在一个生产环境中Kafka集群中同时保存多种版本的消息格式非常常见。为了兼容老版本的格式Broker端会对新版本消息执行向老版本格式的转换。这个过程中会涉及消息的解压缩和重新压缩。一般情况下这种消息格式转换对性能是有很大影响的除了这里的压缩之外它还让Kafka丧失了引以为豪的Zero Copy特性。

所谓“Zero Copy”就是“零拷贝”我在专栏第6期提到过说的是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速的数据传输。因此如果Kafka享受不到这个特性的话性能必然有所损失所以尽量保证消息格式的统一吧这样不仅可以避免不必要的解压缩/重新压缩对提升其他方面的性能也大有裨益。如果有兴趣你可以深入地了解下Zero Copy的原理。

何时解压缩?

有压缩必有解压缩通常来说解压缩发生在消费者程序中也就是说Producer发送压缩消息到Broker后Broker照单全收并原样保存起来。当Consumer程序请求这部分消息时Broker依然原样发送出去当消息到达Consumer端后由Consumer自行解压缩还原成之前的消息。

那么现在问题来了Consumer怎么知道这些消息是用何种压缩算法压缩的呢其实答案就在消息中。Kafka会将启用了哪种压缩算法封装进消息集合中这样当Consumer读取到消息集合时它自然就知道了这些消息使用的是哪种压缩算法。如果用一句话总结一下压缩和解压缩那么我希望你记住这句话Producer端压缩、Broker端保持、Consumer端解压缩。

除了在Consumer端解压缩Broker端也会进行解压缩。注意了这和前面提到消息格式转换时发生的解压缩是不同的场景。每个压缩过的消息集合在Broker端写入时都要发生解压缩操作目的就是为了对消息执行各种验证。我们必须承认这种解压缩对Broker端性能是有一定影响的特别是对CPU的使用率而言。

事实上最近国内京东的小伙伴们刚刚向社区提出了一个bugfix建议去掉因为做消息校验而引入的解压缩。据他们称去掉了解压缩之后Broker端的CPU使用率至少降低了50%。不过有些遗憾的是,目前社区并未采纳这个建议,原因就是这种消息校验是非常重要的,不可盲目去之。毕竟先把事情做对是最重要的,在做对的基础上,再考虑把事情做好做快。针对这个使用场景,你也可以思考一下,是否有一个两全其美的方案,既能避免消息解压缩也能对消息执行校验。

各种压缩算法对比

那么我们来谈谈压缩算法。这可是重头戏!之前说了这么多,我们还是要比较一下各个压缩算法的优劣,这样我们才能有针对性地配置适合我们业务的压缩策略。

在Kafka 2.1.0版本之前Kafka支持3种压缩算法GZIP、Snappy和LZ4。从2.1.0开始Kafka正式支持Zstandard算法简写为zstd。它是Facebook开源的一个压缩算法能够提供超高的压缩比compression ratio

对了看一个压缩算法的优劣有两个重要的指标一个指标是压缩比原先占100份空间的东西经压缩之后变成了占20份空间那么压缩比就是5显然压缩比越高越好另一个指标就是压缩/解压缩吞吐量比如每秒能压缩或解压缩多少MB的数据。同样地吞吐量也是越高越好。

下面这张表是Facebook Zstandard官网提供的一份压缩算法benchmark比较结果

从表中我们可以发现zstd算法有着最高的压缩比而在吞吐量上的表现只能说中规中矩。反观LZ4算法它在吞吐量方面则是毫无疑问的执牛耳者。当然对于表格中数据的权威性我不做过多解读只想用它来说明一下当前各种压缩算法的大致表现。

在实际使用中GZIP、Snappy、LZ4甚至是zstd的表现各有千秋。但对于Kafka而言它们的性能测试结果却出奇得一致即在吞吐量方面LZ4 > Snappy > zstd和GZIP而在压缩比方面zstd > LZ4 > GZIP > Snappy。具体到物理资源使用Snappy算法占用的网络带宽最多zstd最少这是合理的毕竟zstd就是要提供超高的压缩比在CPU使用率方面各个算法表现得差不多只是在压缩时Snappy算法使用的CPU较多一些而在解压缩时GZIP算法则可能使用更多的CPU。

最佳实践

了解了这些算法对比,我们就能根据自身的实际情况有针对性地启用合适的压缩算法。

首先来说压缩。何时启用压缩是比较合适的时机呢?

你现在已经知道Producer端完成的压缩那么启用压缩的一个条件就是Producer程序运行机器上的CPU资源要很充足。如果Producer运行机器本身CPU已经消耗殆尽了那么启用消息压缩无疑是雪上加霜只会适得其反。

除了CPU资源充足这一条件如果你的环境中带宽资源有限那么我也建议你开启压缩。事实上我见过的很多Kafka生产环境都遭遇过带宽被打满的情况。这年头带宽可是比CPU和内存还要珍贵的稀缺资源毕竟万兆网络还不是普通公司的标配因此千兆网络中Kafka集群带宽资源耗尽这件事情就特别容易出现。如果你的客户端机器CPU资源有很多富余我强烈建议你开启zstd压缩这样能极大地节省网络资源消耗。

其次说说解压缩。其实也没什么可说的。一旦启用压缩,解压缩是不可避免的事情。这里只想强调一点:我们对不可抗拒的解压缩无能为力,但至少能规避掉那些意料之外的解压缩。就像我前面说的,因为要兼容老版本而引入的解压缩操作就属于这类。有条件的话尽量保证不要出现消息格式转换的情况。

小结

总结一下今天分享的内容我们主要讨论了Kafka压缩的各个方面包括Kafka是如何对消息进行压缩的、何时进行压缩及解压缩还对比了目前Kafka支持的几个压缩算法最后我给出了工程化的最佳实践。分享这么多内容我就只有一个目的就是希望你能根据自身的实际情况恰当地选择合适的Kafka压缩算法以求实现最大的资源利用率。

开放讨论

最后给出一道作业题请花时间思考一下前面我们提到了Broker要对压缩消息集合执行解压缩操作然后逐条对消息进行校验有人提出了一个方案把这种消息校验移到Producer端来做Broker直接读取校验结果即可这样就可以避免在Broker端执行解压缩操作。你认同这种方案吗

欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。