Kafka 是一个高性能的消息队列,在众多消息队列产品中,Kafka 的性能绝对是处于第一梯队的。我曾经在一台配置比较好的服务器上,对 Kafka 作过极限的性能压测,Kafka 单个节点的极限处理能力接近每秒钟 2000 万条消息,吞吐量达到每秒钟 600MB。git
你可能会问,Kafka 是如何作到这么高的性能的?github
以前就曾探讨过:怎么开发一个高性能的网络应用程序。其中提到了像全异步化的线程模型、高性能的异步网络传输、自定义的私有传输协议和序列化、反序列化等等,这些方法和优化技巧,你均可以在 Kafka 的源代码中找到对应的实现。面试
在性能优化方面,除了这些通用的性能优化手段以外,Kafka 还有哪些“独门绝技”呢?算法
咱们知道,批量处理是一种很是有效的提高系统吞吐量的方法。在 Kafka 内部,消息都是以“批”为单位处理的。一批消息从发送端到接收端,是如何在 Kafka 中流转的呢?编程
咱们先来看发送端,也就是 Producer 这一端。缓存
在 Kafka 的客户端 SDK(软件开发工具包)中,Kafka 的 Producer 只提供了单条发送的send() 方法,并无提供任何批量发送的接口。缘由是,Kafka 根本就没有提供单条发送的功能,是的,你没有看错,虽然它提供的 API 每次只能发送一条消息,但实际上,Kafka的客户端 SDK 在实现消息发送逻辑的时候,采用了异步批量发送的机制。性能优化
当你调用 send() 方法发送一条消息以后,不管你是同步发送仍是异步发送,Kafka 都不会当即就把这条消息发送出去。它会先把这条消息,存放在内存中缓存起来,而后选择合适的时机把缓存中的全部消息组成一批,一次性发给 Broker。简单地说,就是攒一波一块儿发。在 Kafka 的服务端,也就是 Broker 这一端,又是如何处理这一批一批的消息呢?服务器
在服务端,Kafka 不会把一批消息再还原成多条消息,再一条一条地处理,这样太慢了。网络
Kafka 这块儿处理的很是聪明,每批消息都会被当作一个“批消息”来处理。也就是说,在Broker 整个处理流程中,不管是写入磁盘、从磁盘读出来、仍是复制到其余副本这些流程中,批消息都不会被解开,一直是做为一条“批消息”来进行处理的。框架
在消费时,消息一样是以批为单位进行传递的,Consumer 从 Broker 拉到一批消息后,在客户端把批消息解开,再一条一条交给用户代码处理。
好比说,你在客户端发送 30 条消息,在业务程序看来,是发送了 30 条消息,而对于Kafka 的 Broker 来讲,它其实就是处理了 1 条包含 30 条消息的“批消息”而已。显然处理 1 次请求要比处理 30 次请求要快得多。
构建批消息和解开批消息分别在发送端和消费端的客户端完成,不只减轻了 Broker 的压力,最重要的是减小了 Broker 处理请求的次数,提高了整体的处理能力。
这就是 Kafka 用批量消息提高性能的方法。
咱们知道,相比于网络传输和内存,磁盘 IO 的速度是比较慢的。对于消息队列的服务端来讲,性能的瓶颈主要在磁盘 IO 这一块。接下来咱们看一下,Kafka 在磁盘 IO 这块儿作了哪些优化。
对于磁盘来讲,它有一个特性,就是顺序读写的性能要远远好于随机读写。在 SSD(固态硬盘)上,顺序读写的性能要比随机读写快几倍,若是是机械硬盘,这个差距会达到几十倍。为何呢?
操做系统每次从磁盘读写数据的时候,须要先寻址,也就是先要找到数据在磁盘上的物理位置,而后再进行数据读写。若是是机械硬盘,这个寻址须要比较长的时间,由于它要移动磁头,这是个机械运动,机械硬盘工做的时候会发出咔咔的声音,就是移动磁头发出的声音。
顺序读写相比随机读写省去了大部分的寻址时间,它只要寻址一次,就能够连续地读写下去,因此说,性能要比随机读写要好不少。
Kafka 就是充分利用了磁盘的这个特性。它的存储设计很是简单,对于每一个分区,它把从Producer 收到的消息,顺序地写入对应的 log 文件中,一个文件写满了,就开启一个新的文件这样顺序写下去。消费的时候,也是从某个全局的位置开始,也就是某一个 log 文件中的某个位置开始,顺序地把消息读出来。
这样一个简单的设计,充分利用了顺序读写这个特性,极大提高了 Kafka 在使用磁盘时的IO 性能。
接下来咱们说一下 Kafka 是如何实现缓存的。
在 Kafka 中,它会利用 PageCache 加速消息读写。PageCache 是现代操做系统都具备的一项基本特性。通俗地说,PageCache 就是操做系统在内存中给磁盘上的文件创建的缓存。不管咱们使用什么语言编写的程序,在调用系统的 API 读写文件的时候,并不会直接去读写磁盘上的文件,应用程序实际操做的都是 PageCache,也就是文件在内存中缓存的副本。
应用程序在写入文件的时候,操做系统会先把数据写入到内存中的 PageCache,而后再一批一批地写到磁盘上。读取文件的时候,也是从 PageCache 中来读取数据,这时候会出现两种可能状况。
一种是 PageCache 中有数据,那就直接读取,这样就节省了从磁盘上读取数据的时间;另外一种状况是,PageCache 中没有数据,这时候操做系统会引起一个缺页中断,应用程序的读取线程会被阻塞,操做系统把数据从文件中复制到 PageCache 中,而后应用程序再从PageCache 中继续把数据读出来,这时会真正读一次磁盘上的文件,这个读的过程就会比较慢。
用户的应用程序在使用完某块 PageCache 后,操做系统并不会马上就清除这个PageCache,而是尽量地利用空闲的物理内存保存这些 PageCache,除非系统内存不够用,操做系统才会清理掉一部分 PageCache。清理的策略通常是 LRU 或它的变种算法,这个算法咱们不展开讲,它保留 PageCache 的逻辑是:优先保留最近一段时间最常使用的那些 PageCache。
Kafka 在读写消息文件的时候,充分利用了 PageCache 的特性。通常来讲,消息刚刚写入到服务端就会被消费,按照 LRU 的“优先清除最近最少使用的页”这种策略,读取的时候,对于这种刚刚写入的 PageCache,命中的概率会很是高。
也就是说,大部分状况下,消费读消息都会命中 PageCache,带来的好处有两个:一个是读取的速度会很是快,另一个是,给写入消息让出磁盘的 IO 资源,间接也提高了写入的性能。
Kafka 的服务端在消费过程当中,还使用了一种“零拷贝”的操做系统特性来进一步提高消费
的性能。
咱们知道,在服务端,处理消费的大体逻辑是这样的:
这个过程当中,数据实际上作了 2 次或者 3 次复制:
Kafka 使用零拷贝技术能够把这个复制次数减小一次,上面的 二、3 步骤两次复制合并成一次复制。直接从 PageCache 中把数据复制到 Socket 缓冲区中,这样不只减小一次数据复制,更重要的是,因为不用把数据复制到用户内存空间,DMA 控制器能够直接完成数据复制,不须要 CPU 参与,速度更快。
下面是这个零拷贝对应的系统调用:
#include <sys/socket.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
若是你遇到这种从文件读出数据后再经过网络发送出去的场景,而且这个过程当中你不须要对这些数据进行处理,那必定要使用这个零拷贝的方法,能够有效地提高性能。
咱们总结了 Kafka 的高性能设计中的几个关键的技术点:
以上这些,就是 Kafka 之因此能作到如此高性能的关键技术点。你能够看到,要真正实现一个高性能的消息队列,是很是不容易的,你须要熟练掌握很是多的编程语言和操做系统的底层技术。这些优化的方法和技术,一样能够用在其余适合的场景和应用程序中。我但愿你能充分理解这几项优化技术的原理,知道它们在什么状况下适用,什么状况下不适用。这样,当你遇到合适场景的时候,再深刻去学习它的细节用法,最终就能把它真正地用到你开发的程序中。