Kafka.07 - 性能优化介绍
Kafka 在提高效率方面做了很大努力。Kafka 的一个主要使用场景是处理网站活动日志,吞吐量是非常大的,每个页面都会产生好多次写操作。读方面,假设每个消息只被消费一次,读的量的也是很大的,Kafka 也尽量使读的操作更轻量化。
我们之前讨论了磁盘的性能问题,线性读写的情况下影响磁盘性能问题大约有两个方面:太多的琐碎的 I/O 操作和太多的字节拷贝。I/O 问题发生在客户端和服务端之间,也发生在服务端内部的持久化的操作中。
消息集(message set)
为了避免这些问题,Kafka 建立了“消息集(message set)”的概念,将消息组织到一起,作为处理的单位。可以说,Kafka 的消息层次都分为两层:消息集合(message set)以及消息(message)。一个消息集合中包含若干条日志项(record item),而日志项才是真正封装消息的地方,换言之,一个日志项对应一条消息。
以消息集为单位处理消息,比以单个的消息为单位处理,会提升不少性能。Producer 把消息集一块发送给服务端,而不是一条条的发送;服务端把消息集一次性的追加到日志文件中,这样减少了琐碎的 I/O 操作。consumer 也可以一次性的请求一个消息集。
另外一个性能优化是在字节拷贝方面。在低负载的情况下这不是问题,但是在高负载的情况下它的影响还是很大的。为了避免这个问题,Kafka 使用了标准的二进制消息格式,这个格式可以在 producer, broker 和 consumer 之间共享而无需做任何改动。
zero copy
Broker 维护的消息日志仅仅是一些目录文件,消息集以固定的格式写入到日志文件中,这个格式 producer 和 consumer 是共享的,这使得 Kafka 可以一个很重要的点进行优化:消息在网络上的传递。现代的 unix 操作系统提供了高性能的将数据从页面缓存发送到 socket 的系统函数,在 linux 中,这个函数是 sendfile.
为了更好的理解 sendfile 的好处,我们先来看下一般将数据从文件发送到 socket 的数据流向:
操作系统把数据从文件拷贝内核中的页缓存中
应用程序从页缓存从把数据拷贝自己的内存缓存中
应用程序将数据写入到内核中 socket 缓存中
操作系统把数据从 socket 缓存中拷贝到网卡接口缓存,从这里发送到网络上。
这显然是低效率的,有 4 次拷贝和 2 次系统调用。Sendfile 通过直接将数据从页面缓存发送网卡接口缓存,避免了重复拷贝,大大的优化了性能。
在一个多 consumers 的场景里,数据仅仅被拷贝到页面缓存一次而不是每次消费消息的时候都重复的进行拷贝。这使得消息以近乎网络带宽的速率发送出去。这样在磁盘层面你几乎看不到任何的读操作,因为数据都是从页面缓存中直接发送到网络上去了。
数据压缩
压缩(compression)秉承了用时间去换空间的经典 trade-off 思想,具体来说就是用 CPU 时间去换磁盘空间或网络 I/O 传输量,希望以较小的 CPU 开销带来更少的磁盘占用或更少的网络 I/O 传输。很多时候,性能的瓶颈并非 CPU 或者硬盘而是网络带宽,对于需要在数据中心之间传送大量数据的应用更是如此。当然用户可以在没有 Kafka 支持的情况下各自压缩自己的消息,但是这将导致较低的压缩率,因为相比于将消息单独压缩,将大量文件压缩在一起才能起到最好的压缩效果。
何时压缩
在 Kafka 中,压缩可能发生在两个地方:生产者端和 Broker 端。
如果在生产者程序中配置了 compression.type
参数,即表示启用指定类型的压缩算法。配置压缩后,生产的每个消息集合都是经压缩算法压缩过的,故而能很好地节省网络传输带宽以及 Kafka Broker 端的磁盘占用。
Broker 端何时进行压缩呢?
大部分情况下 Broker 从 Producer 端接收到消息后仅仅是原封不动地保存而不会对其进行任何修改。
有两种例外情况可能让 Broker 重新压缩消息。
Broker 端指定了和 Producer 端不同的压缩算法。Broker 端也有一个参数叫
compression.type
,这个参数的默认值是 producer,这表示 Broker 端会“尊重”Producer 端使用的压缩算法。可一旦在 Broker 端设置了不同的compression.type
值,就一定要小心了,因为可能会发生预料之外的压缩 / 解压缩操作,通常表现为 Broker 端 CPU 使用率飙升。Broker 端发生了消息格式转换。为了解决 Kafka 压缩效率低的问题,Kafka 共有两大类消息格式,社区分别称之为 V1 版本和 V2 版本。V2 版本是 Kafka 0.11.0.0 中正式引入的。在一个生产环境中,Kafka 集群中同时保存多种版本的消息格式非常常见。为了兼容老版本的格式,Broker 端会对新版本消息执行向老版本格式的转换。这个过程中会涉及消息的解压缩和重新压缩。一般情况下这种消息格式转换对性能是有很大影响的,除了这里的压缩之外,它还让 Kafka 丧失了引以为豪的 Zero Copy 特性。
何时解压缩
当 Consumer 收到消息后,会由 Consumer 自行进行解压缩。Kafka 会将启用了哪种压缩算法封装进消息集合中,这样当 Consumer 读取到消息集合时,它自然就知道了这些消息使用的是哪种压缩算法。
除了在 Consumer 端解压缩,Broker 端也会进行解压缩。每个压缩过的消息集合在 Broker 端写入时都要发生解压缩操作,目的就是为了对消息执行各种验证。
端到端压缩
Kafka 采用了端到端的压缩:因为有“消息集合”的概念,客户端的消息可以一起被压缩后送到服务端,并以压缩后的格式写入日志文件,然后再以压缩的格式发送到 consumer,也就是说 Producer 发送压缩消息到 Broker 后,Broker 照单全收并原样保存起来。当 Consumer 程序请求这部分消息时,Broker 依然原样发送出去,当消息到达 Consumer 端后,由 Consumer 自行解压缩还原成之前的消息,所以叫做“端到端的压缩”。
一句话总结:Producer 端压缩、Broker 端保持、Consumer 端解压缩。
各种压缩算法对比
在 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 程序运行机器上的 CPU 资源要很充足。如果 Producer 运行机器本身 CPU 已经消耗殆尽了,那么启用消息压缩无疑是雪上加霜,只会适得其反。
如果带宽资源有限,那么我也建议你开启压缩。这年头,带宽可是比 CPU 和内存还要珍贵的稀缺资源,毕竟万兆网络还不是普通公司的标配,因此千兆网络中 Kafka 集群带宽资源耗尽这件事情就特别容易出现。如果你的客户端机器 CPU 资源有很多富余,我强烈建议你开启 zstd 压缩,这样能极大地节省网络资源消耗。
评论