面试官:手写个高性能kafka?
作为MQ,Kafka的性能说第二,恐难有人敢说第一。一台配置较好的服务器,对Kafka做极限性能压测,Kafka单节点的极限处理能力接近2000万条消息/s,吞吐量达600MB/s。
像全异步化的线程模型、高性能的异步网络传输、自定义的私有传输协议和序列化、反序列化等等,这些方法和优化技巧,Kafka都做到了。
性能优化除了这些通用手段,它还有啥葵花宝典般神技呢?
批量消息提升服务端处理能力
批量处理是一种非常有效的提升系统吞吐量的方法。
Kafka内部的消息都是以“批”为单位处理。
一批消息从发送端到接收端,是如何在Kafka中流转的呢?
Producer端
在Kafka的客户端SDK,Kafka的Producer只提供了单条发送的send()方法,并没有提供任何批量发送的接口。原因是,Kafka根本就没有提供单条发送的功能,是的,你没有看错,虽然它提供的API每次只能发送一条消息,但实际上,Kafka的客户端SDK在实现消息发送逻辑的时候,采用了异步批量发送。
当调用send()方法发送一条消息之后,无论你是同步发送还是异步发送,Kafka都不会立即把这消息发出去。猥琐发育一波再打个团战:
- 先把消息缓存在内存
- 然后选择合适时机把缓存的所有消息组成一批,一次性发给Broker
Kafka服务端,即Broker端,又是如何处理这一批批消息的呢?
服务端,Kafka不会把一批消息再还原成多条消息,再一条条处理,这样太慢了。
而是每批消息都会被当做一个“批消息”处理。即在Broker整个处理流程,无论是写入磁盘、从磁盘读出、还是复制到其他副本这些流程中,批消息都不会被解开,一直是作为一条“批消息”来进行处理的。
在消费时,同样是以批为单位传递,Consumer从Broker拉到一批消息后,在客户端把批消息解开,再一条条交给用户代码。
比如说,你在客户端发30条消息,在业务程序看,是发送了30条消息,而对于Kafka的Broker来说,它其实就是处理了1条包含30条消息的“批消息”。显然处理1次请求要比处理30次请求快得多。
构建批消息和解开批消息分别在发送端和消费端的客户端完成,不仅减轻Broker压力,减少了Broker处理请求的次数,提升了总体的处理能力。
这就是Kafka用批量消息提升性能的方法。
相比于网络传输和内存,磁盘IO的速度是比较慢的。对于消息队列的服务端来说,性能的瓶颈主要在磁盘IO这一块。
顺序读写提升磁盘IO性能
磁盘有个特性:顺序读写性能远好于随机读写。
在SSD上,顺序读写的性能要比随机读写快几倍,如果是机械硬盘,这个差距会达到几十倍。
os每次从磁盘读写数据时,需先寻址,即找到数据在磁盘的物理位置,然后再读写数据。
若是机械硬盘,寻址需要较长时间,因为要移动磁头。顺序读写相比随机读写省去大量寻址时间,只要寻址一次,就可连续读写下去,所以性能比随机读写好。
Kafka充分利用磁盘特性。存储设计非常简单,对每个分区,它把从Producer收到的消息,顺序地写入对应log文件,一个文件写满,就开启新文件顺序写。
消费时,也是从某个全局位置开始,即某个log文件的某位置开始,顺序读出消息。
这简单的设计,充分利用顺序读写特性,极大提升Kafka在使用磁盘时的IO性能。
PageCache加速消息读写
PageCache是os在内存中给磁盘的文件建立的缓存。
无论使用什么高级语言,在调用系统API读写文件时,并不会直接去读写磁盘的文件,实际操作的都是PageCache,即文件在内存中缓存的副本。
应用程序在写入文件时,操作系统会先把数据写入到内存中的PageCache,再一批批写到磁盘。
读取文件的时候,也是从PageCache中来读取数据,这时候会出现两种可能情况。
- PageCache中有数据,直接读取,这样就节省了从磁盘上读取数据的时间;另一种情况是,PageCache中没有数据,这时候操作系统会引发一个缺页中断,应用程序的读取线程会被阻塞,操作系统把数据从文件中复制到PageCache中,然后应用程序再从PageCache中继续把数据读出来,这时会真正读一次磁盘上的文件,这个读的过程就会比较慢。
应用程序使用完某块PageCache后,os并不会立刻清除该PageCache,而是尽可能地利用空闲的物理内存保存这些PageCache,除非系统内存不够用,操作系统才会清理部分PageCache。清理的策略一般是LRU或它的变种算法:优先保留最近一段时间最常使用的那些PageCache。
Kafka在读写消息文件的时候,充分利用了PageCache的特性。一般来说,消息刚刚写入到服务端就会被消费,按照LRU的“优先清除最近最少使用的页”这种策略,读取时候,对于这种刚刚写入的PageCache,命中的几率会非常高。
大部分情况下,消费读消息都会命中PageCache,带来的好处有:
- 读取的速度会非常快
- 给写入消息让出磁盘的IO资源,间接也提升了写入的性能
零拷贝
Kafka的服务端在消费过程中,还使用了一种“零拷贝”的操作系统特性来进一步提升消费的性能。
在服务端,处理消费的大致逻辑是这样的:
- 首先,从文件中找到消息数据,读到内存中
- 然后,把消息通过网络发给客户端
数据实际上做了2次或者3次复制:
- 从文件复制数据到PageCache中,如果命中PageCache,这一步可省
- 从PageCache复制到应用程序的内存空间中,也就是我们可以操作的对象所在的内存
- 从应用程序的内存空间复制到Socket的缓冲区,这过程就是我们调用网络应用框架API发送数据的过程
Kafka使用零拷贝技术可把这复制次数减少一次,上面的2、3步骤两次复制合并成一次复制。
直接从PageCache中把数据复制到Socket缓冲区中,这样不仅减少一次数据复制,更重要的是,由于不用把数据复制到用户内存空间,DMA控制器可以直接完成数据复制,不需要CPU参与,速度更快。
下面是这个零拷贝对应的系统调用:
#include <sys/socket.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
如果你遇到这种从文件读出数据后再通过网络发送出去的场景,并且这过程中你不需对这些数据处理,那一定要使用零拷贝方法,有效提升性能。