程序员要追求极致的效能 Kafka掌控这11项要领
来源: 朱小厮的部落格
批量处理
传统讯息中介软件的讯息传送和消费整体上是针对单条的。对于生产者而言,它先发一条讯息,然后broker返回ACK表示已接收,这里产生2次rpc;对于消费者而言,它先请求接受讯息,然后broker返回讯息,最后传送ACK表示已消费,这里产生了3次rpc(有些讯息中介软件会优化一下,broker返回的时候返回多条讯息)。而Kafka采用了批量处理:生产者聚合了一批讯息,然后再做2次rpc将讯息存入broker,这原本是需要很多次的rpc才能完成的操作。假设需要传送1000条讯息,每条讯息大小1KB,那么传统的讯息中介软件需要2000次rpc,而Kafka可能会把这1000条讯息包装成1个1MB的讯息,采用2次rpc就完成了任务。这一改进举措一度被认为是一种“作弊”的行为,然而在微批次理念盛行的今日,其它讯息中介软件也开始纷纷效仿。
客户端优化
这里接着批量处理的概念继续来说,新版生产者客户端摒弃了以往的单执行绪,而采用了双执行绪:主执行绪和Sender执行绪。主执行绪负责将讯息置入客户端快取,Sender执行绪负责从快取中传送讯息,而这个快取会聚合多个讯息为一个批次。有些讯息中介软件会把讯息直接扔到broker。
日志格式
Kafka从0.8版本开始日志格式历经了三次变革:v0、v1、v2。在之前发过的一篇文章《一文看懂Kafka讯息格式的演变》中详细介绍了Kafka日志格式,Kafka的日志格式越来越利于批量讯息的处理,有兴趣的同学可以阅读一下这篇文章以作了解。

日志编码
如果了解了Kafka具体的日志格式(可以参考上图),那么你应该了解日志(Record,或者称之为讯息)本身除了基本的key和value之外,还有一些其它的字段,原本这些附加字段按照固定的大小占用一定的篇幅(参考上图左),而Kafka最新的版本中采用了变成字段Varints和ZigZag编码,有效地降低了这些附加字段的占用大小。日志(讯息)尽可能变小了,那么网络传输的效率也会变高,日志存档的效率也会提升,从而整理的效能也会有所提升。
讯息压缩
Kafka支援多种讯息压缩方式(gzip、snappy、lz4)。对讯息进行压缩可以极大地减少网络传输 量、降低网络 I/O,从而提高整体的效能。讯息压缩是一种使用时间换空间的优化方式,如果对 时延有一定的要求,则不推荐对讯息进行压缩。

建立索引,方便快速定位查询
每个日志分段档案对应了两个索引档案,主要用来提高查询讯息的效率,这也是提升效能的一种方式。(具体的内容在书中的第5章有详细的讲解,公众号里好像忘记发表了,找了一圈没找到)
分割槽
很多人会忽略掉这个因素,其实分割槽也是提升效能的一种非常有效的方式,这种方式所带来的效果会比前面所说的日志编码、讯息压缩等更加的明显。分割槽在其他分散式元件中也有大量涉及,至于为什么分割槽能够提升效能这种基本知识在这里就不在赘述了。不过需要注意,一昧地增加分割槽并不能一直带来效能的提升,有兴趣的同学可以看一下这篇《Kafka主题中的分割槽数越多吞吐量就越高?》。
一致性
绝大多数的资料在讲述Kafka效能优化的举措之时是不会提及一致性的东西的。我们所了解的通用的一致性协议如Paxos、Raft、Gossip等,而Kafka另辟蹊径采用类似PacificA的做法不是“拍大腿”拍出来的,采用这种模型会提升整理的效率。具体的细节后面会整理一篇,类似《在Kafka中使用Raft替换PacificA的可行性分析及优缺点》。

顺序写盘
操作系统可以针对线性读写做深层次的优化,比如预读(read-ahead,提前将一个比较大的磁盘块读入内存) 和后写(write-behind,将很多小的逻辑写操作合并起来组成一个大的物理写操作)技术。Kafka 在设计时采用了档案追加的方式来写入讯息,即只能在日志档案的尾部追加新的消 息,并且也不允许修改已写入的讯息,这种方式属于典型的顺序写盘的操作,所以就算 Kafka 使用磁盘作为储存介质,它所能承载的吞吐量也不容小觑。
页快取
为什么Kafka效能这么高?当遇到这个问题的时候很多人都会想到上面的顺序写盘这一点。其实在顺序斜盘前面还有页快取(PageCache)这一层的优化。
页快取是操作系统实现的一种主要的磁盘快取,以此用来减少对磁盘 I/O 的操作。具体 来说,就是把磁盘中的资料快取到内存中,把对磁盘的访问变为对内存的访问。为了弥补性 能上的差异,现代操作系统越来越“激进地”将内存作为磁盘快取,甚至会非常乐意将所有 可用的内存用作磁盘快取,这样当内存回收时也几乎没有效能损失,所有对于磁盘的读写也 将经由统一的快取。
当一个程序准备读取磁盘上的档案内容时,操作系统会先检视待读取的资料所在的页 (page)是否在页快取(pagecache)中,如果存在(命中)则直接返回资料,从而避免了对物 理磁盘的 I/O 操作;如果没有命中,则操作系统会向磁盘发起读取请求并将读取的资料页存入 页快取,之后再将资料返回给程序。同样,如果一个程序需要将资料写入磁盘,那么操作系统也会检测资料对应的页是否在页快取中,如果不存在,则会先在页快取中新增相应的页,最后将资料写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的 资料写入磁盘,以保持资料的一致性。
对一个程序而言,它会在程序内部快取处理所需的资料,然而这些资料有可能还快取在操 作系统的页快取中,因此同一份资料有可能被快取了两次。并且,除非使用 Direct I/O 的方式, 否则页快取很难被禁止。此外,用过 Java 的人一般都知道两点事实:物件的内存开销非常大, 通常会是真实资料大小的几倍甚至更多,空间使用率低下;Java 的垃圾回收会随着堆内资料的 增多而变得越来越慢。基于这些因素,使用档案系统并依赖于页快取的做法明显要优于维护一 个程序内快取或其他结构,至少我们可以省去了一份程序内部的快取消耗,同时还可以通过结构紧凑的字节码来替代使用物件的方式以节省更多的空间。如此,我们可以在 32GB 的机器上使用 28GB 至 30GB 的内存而不用担心 GC 所带来的效能问题。此外,即使 Kafka 服务重启, 页快取还是会保持有效,然而程序内的快取却需要重建。这样也极大地简化了程式码逻辑,因为 维护页快取和档案之间的一致-由操作系统来负责,这样会比程序内维护更加安全有效。
Kafka 中大量使用了页快取,这是 Kafka 实现高吞吐的重要因素之一。虽然讯息都是先被写入页快取,然后由操作系统负责具体的刷盘任务的。

零拷贝
我在很久之前就之前就发过一篇《什么是Zero Copy》,如果对Zero Copy不了解的同学可以翻阅一下。Kafka使用了Zero Copy技术提升了消费的效率。前面所说的Kafka将讯息先写入页快取,如果消费者在读取讯息的时候如果在页快取中可以命中,那么可以直接从页快取中读取,这样又节省了一次从磁盘到页快取的copy开销。另外对于读写的概念可以进一步了解一下什么是写放大和读放大。
附
一个磁盘IO流程可以参考下图:

end:如果你觉得本文对你有帮助的话,记得关注点赞转发,你的支援就是我更新动力。