这是《吃透 MQ 系列》的Kafka的第四篇文章。如果你错过了前三篇文章,可以通过下面的链接快速浏览一下:第1篇:揭开Kafka的神秘面纱第2篇:Kafka架构设计中任督二脉第3部分:Kafka存储选择的奥秘中第三篇,深入剖析了Kafka选择“日志文件”作为存储方案的来龙去脉,以及“磁盘顺序写入+稀疏索引”背后精妙的设计思路。而Kafka在单机上可以做到每秒几十万的吞吐量,其性能优化手段远不止于此。Kafka的高性能设计可以说是全方位的。从Prodcuer,到Broker,再到Consumer,Kafka努力优化每一个细节,最终成就了如此极致的性能。在这篇文章中,我想先带大家建立一个高性能设计的思维模式,然后去探索Kafka的高性能设计方案,最后让大家更系统地掌握所有的知识点,理解它的设计理念。1.如何理解高性能设计?我们暂时把Kafka放在一边,试着理解高性能设计的本质。有高并发开发经验的同学早就熟悉线程池、多级缓存、IO多路复用、零拷贝等技术概念,但是这些技术手段的本质是什么?这其实是一个系统性的问题,至少需要深入到操作系统层面,从CPU和存储入手,了解底层的实现机制,然后自下而上,层层解密渗透。但站在更高的角度,我认为:高性能设计其实是一成不变的,必须从“计算和IO”两个维度出发,考虑可能的优化点。“计算”维度的性能优化方式有哪些?有两种方法:1.让更多的核参与计算:比如用多线程代替单线程,用集群代替单机等2.减少计算量:比如替换globalscan用索引、用同步代替异步、通过限流减少请求处理量、采用更高效的数据结构和算法等。我们来看看“IO”维度的性能优化方式?可以借助Linux系统的IO栈图来辅助思考。图1:Linux系统的IO栈图。可以看出整个IO架构是分层的。我们可以从应用程序、操作系统、磁盘等各个层面来考虑性能优化,而所有这些方法几乎都围绕着以下两个方面展开:1.加快IO速度:比如使用磁盘顺序写代替随机写,使用NIO代替BIO,使用性能更好的SSD代替机械硬盘等。2.减少IO次数或IO数据量:如使用系统缓存或外部缓存,减少IO复制次数,批量读写,通过零拷贝技术压缩数据等。以上内容可以理解为高性能设计的“道”。当然,不是几百字就能解释清楚的。我比较感兴趣的是引玉,换个角度看高并发,给大家一个方向。指导。当你抓住了“计算和IO”这两个最本质的东西,然后以这两点为根,那么在这两个维度上探索有哪些性能优化的方法呢?他们的原则是什么?它可以层层剥开高性能设计的神秘面纱,形成可靠的知识体系。这种分析方法可以用来研究Kafka,以及大家熟知的Redis、ES等高性能应用系统。2.Kafka高性能设计全景图有了高性能设计的思维模式之后,我们再回到Kafka本身进行分析。上文提到,Kafka的性能优化手段非常丰富,精巧的设计至少有10种。虽然我们可以从计算和IO两个维度来联想这些方法,但是要完全记住它们似乎并不容易。事物。这就引出了另一个话题:我们应该选择什么样的上下文来连接这些优化方法?上一篇已经分析过:无论Kafka、RocketMQ还是其他消息队列,本质都是“一发一存一消费”。我们可以沿着这条主线进行结构排序。基于这样的想法,形成了下面这张Kafka高性能设计的全景图。我按照生产消息、存储消息、消费消息三个模块,对Kafka最具代表性的12种性能优化方法进行了分类。图2:Kafka高性能设计全景图借助这张全景图,我来一一分析每种方法背后的一般原理,并尝试解读Kafka的设计理念。3.生产消息的性能优化方法让我们从生产消息开始。下面是Producer端使用的四种优化方式。1、批量发送消息Kafka作为一个消息队列,显然是一个IO密集型应用。它面临的挑战不仅是磁盘IO(Broker端需要持久化消息),还有网络IO(Producer到Broker,Broker到Consumer,都需要通过网络传输消息)。上一篇文章指出:磁盘顺序IO的速度其实很快,不亚于内存的随机读写。这样网络IO就成为了Kafka的性能瓶颈。基于这样的背景,Kafka采用了批量发送消息的方式。通过将多条消息按照分区分组,然后每次发送一组消息,大大降低了网络传输的开销。看似很普通的方法,实际上却大大提高了Kafka的吞吐量,其精妙之处远非如此。以下优化方法与它密切相关。2.消息压缩消息压缩的目的是进一步降低网络传输带宽。对于压缩算法,通常是:数据量越大,压缩效果越好。因为前期是批量发送,Kafka的消息压缩机制才能真正发挥威力(压缩的本质在于多条消息的重复)。与压缩单个消息相比,同时压缩多个消息可以大大减少数据量,从而更大程度地提高网络传输速率。有一篇文章比较了Kafka支持的三种压缩算法:gzip、snappy和lz4的性能。测试20000条消息后,结果如下:图3:压缩效果对比,来源:https://www.jianshu.com/p/d69e27749b00总体来说,gzip压缩效果最好,但压缩时间较长产生。与lz4相比,性能最好。事实上,压缩消息不仅减少了网络IO,还大大减少了磁盘IO。因为在Broker中批量消息持久化到磁盘时,仍然保持压缩状态,最后在Consumer端进行解压操作。这种端到端的压缩设计其实非常巧妙,大大提高了写入磁盘的效率。3、Kafka消息中Key和Value的高效序列化,均支持自定义类型,只需要提供相应的序列化器和反序列化器即可。因此,用户可以根据实际情况选择快速紧凑的序列化方式(如ProtoBuf、Avro)来减少实际网络传输量和磁盘存储量,进一步提高吞吐量。4、内存池多路复用前面提到Producer是批量发送消息的,所以消息会先写入Producer的内存中进行缓冲,直到多条消息组成一个Batch,Batch才会通过网络发送给Broker。Batch发送出去后,显然这部分数据在Producer端还会在JVM内存中。由于没有引用,它可以被JVM回收。但是大家都知道JVMGC的时候肯定有一个StopTheWorld的过程。即使使用最先进的垃圾收集器,也难免会造成工作线程的短暂停顿。影响。在此背景下,引入了Kafka优秀的内存池机制。本质上和连接池、线程池是一样的。就是为了提高重用,减少频繁的创建和发布。具体是如何实施的?其实很简单:Producer会占用一个固定大小的内存块,比如64MB,然后把64MB分成M个小内存块(比如一个小内存块的大小是16KB)。当需要创建新的batch时,直接从内存池中取出一个16KB的内存块,然后不断往里面写入消息,但是最大写入量是16KB,然后把batch发送给Broker,这时内存块可以返回到缓冲池继续重复使用,完全不涉及垃圾回收。最后,整个流程如下图所示:图4:Kafka发送端的流程了解了Producer端的四种高性能设计之后,你肯定会有一个疑问:传统的数据库或者消息中间件都在试图让客户端更轻服务器被设计成重量级的,客户端只是作为应用程序与服务器的接口。但是Kafka反其道而行之,采用了独特的设计思路。在将消息发送给Broker之前,客户端需要做很多工作,比如:消息分区路由、校验和计算、压缩消息等,这样很好的分散了Broker的计算压力。可见,没有最好的设计,只有最合适的设计,这就是建筑的本源。4.写在最后Kafka在创建面向性能的解决方案方面做得非常出色。它有很多值得深入研究和学习的设计理念。考虑到篇幅问题,我把Kafka的高性能设计分成了两篇。下一篇文章将继续讲解剩下的8种高性能设计方法及其背后的设计思想。看到这里,希望大家能够建立起高性能的设计思维模式和学习方法。这些技能还可以帮助您了解其他高性能中间件。
