Apache Kafka是一个高性能的消息处理引擎,在众多消息处理产品中,kafka的性能绝对是处于第一梯队的,相信你在工作中或多或少都有使用过,但是你知道kafka为什么这么快吗?接下来,我们就从kafka的设计的多个维度来探秘一下,kafka的一些"独门绝技"?
批量消息
客户端虽然是逐条发送消息,但是kafka的producer会把发送的数据先"攒一波",作为一个"批消息"发送给broker。而当broker收到这批消息后,并不会把这个批消息拆解开逐个进行处理,而是作为一条批消息进行处理,在broker中,无论是把消息写入磁盘,从磁盘中读出来,还是在把消息复制到其他follower分区副本上,都是以批消息作为单位的。而消费端消费的时候,也是读取一批消息,并在消费端对着一批消息进行拆解,然后逐个处理。
也就是说,消息在kafka服务端一直都是批消息,批消息的组装和拆解是在客户端(发送端和消费端)进行的,这样不仅减轻了broker端的压力,而且减少了broker处理请求次数,提升了kafka总体的消息处理效率。
批消息发送流程示意图:
不过攒一波发送消息的机制,会带来一定的消息延迟。就是说,我们使用producer发送了一条消息,但是这条消息并不会立即发送到broker中,而是需要满足一定的"条件",才会发送。如果没有满足这些条件的话,producer是不会发送到broker中,自然在consumer也不会收到这条消息,对于consumer来说,就是没有发送过消息一样。
上面说的条件,主要有两个: 1.发送给某个分区,积压的消息数据占用的空间达到一定规模后,统一发送,这个规模由参数 batch.size决定,参数默认值16kb。
2.producer调用send方法发送消息时,消息会延时发送,延时的最大时间间隔由参数 linger.ms 决定,改参数的默认大小时0ms,也就是立即发送。
在发送消息时,只要满足上面任何一个条件,消息才会被发送出去。其实这也是架构设计上的一种trade off:通过牺牲实时性,来提高吞吐率。
顺序读写
对于磁盘来说,它有一个特性:顺序读写的性能要远远好于随机读写,尤其是机械硬盘。因为每次随机访问前,硬盘都要先进行寻址操作,也就是找到需要访问数据的起始位置,然后从这个位置开始读取数据,而对于机械硬盘的,这个寻址操作是比较消耗性能的。
而顺序读写可以省去大部分的寻址时间,它只需要寻址一次,就可以连写读写下去。所以顺序读写相比随机读写性能要好很多。除此之外,在计算机领域有一个著名的原理,叫做"局部性原理",局部性原理分为:空间局部性和时间局部性。如果一个内存位置被重复的引用,那就是有了时间局部性,如果一个内存位置被引用了,很快这个位置的附近位置也被引用了,这就有了空间局部性,
kafka就是充分利用了磁盘的这个特性。在kafka中,每个分区的数据都对应一个log文件,对于这个分区的数据,都会顺序的写入到这个log文件,当这个文件写满之后,就会开启一个新的文件继续顺序写下去。而读取某个分区中的消息,也是从指定的log文件中,顺序的读取。
不过,细心的老铁,应该会发现其中的问题:因为每个分区的数据,都有自己独立的log文件存储,读取这个分区的数据可以使用顺序读,但是如果分区比较多的话,读取多个分区中的数据,不也是会产生随机读写吗?是的,对于这种情况,kafka就无能为力了,所以在使用kafka的时候,kafka社区会建议,在一个kafka集群中,不要创建过多topic和分区。
而对于这种情况,国内的RocketMQ可以很好的解决这个问题,因为在RocketMQ中,消息的存储是以Broker为单位的,也就是一个Broke上的的所有分区的数据都写入到一个log中,这样可以减少一定的随机读写。如果一个Broker上有上千个主题分区时,RocketMQ的读写性能就会略好于Kafka。
PageCache
PageCache是操作系统在内存中给磁盘文件建立的缓存,是系统级别的缓存,和语言无关。当写数据的时候,数据会先写到PageCache中,操作系统会定时的将PageCach中的数据写到磁盘中,这种攒一波的操作,主要就是为了减少和磁盘IO交互的次数。
读数据的时候,也是先会从PageCache中的查询,如果PageCache中没有要查询的数据时,操作系统会触发一个缺页中断,中断的响应程序会从磁盘中读取数据,加载到PageCache中,然后在返回给客户端。
PageCache作为一种缓存,会占用内存空间,而内存空间是有限的,当空间不足的时候,需要进行内存置换,置换的策略一般是LRU或者是其的变种算法,就是优先保留最近一段时间最常用的那些PageCache。
PageCache工作原理如下图:
ZeroCopy
对于所有的消息队列服务端程序来说,做的大部分事情就是通过网络来接受数据,然后,为了保证消息的可靠性,会将消息存储到磁盘中,最后再从磁盘中读取数据,通过网络将消息发送出去。简单来说,消息队列的绝大部分工作都是在做磁盘和网络之间的数据交互。
而将磁盘中的数据通过网络发送出去的过程大概如下: 1.cpu在用户态,调用read函数(系统调用)。
2.cpu从用户态切换到内核态,通过磁盘驱动程序,将磁盘中的文件内容copy到内核缓冲区(read buffer),这个copy过程无需cpu参与,然后将内核缓冲区中数据copy到用户缓冲区,此copy需要cpu参与。
3.cpu从内核态切换到用户态,将用户缓冲区中数据copy到内核缓冲区(socket buffer),此过程需要cpu参与,然后cpu在切换为用户态,处理其他用户程序。
4.socket中的数据通过网卡驱动程序,经过网卡发送出去。
在这个过程中,需要cpu进行3次状态转换,也就是我们常说的上下文切换。同时数据需要先从内核缓冲区复制到用户缓冲区,然后在从用户缓冲区复制到内核缓冲区,而且这两个过程需要cpu的参与。可看出将磁盘中的数据发送出去,是一个比较重的IO操作,也是比较消耗性能的地方,如果能减少这个过程cpu的上下文切换的次数和数据在多个缓冲区间复制的次数,可以提高每次将磁盘数据通过网络交互的效率,而消息队里每天要处理的数据是海量,总体来说,提升的效率也是很客观的。
而Zero Copy技术,可以对上述过程进行优化,来减少此过程中,cpu的参与和上下文切换。
不过这里要先说明一下,Zero Copy和Page Cache相似,都是操作系统提供的一项能力,和具体语言无关,操作系统通过系统调用的方式,对上层的编程语言暴露接口。例如java语言中,NIO的 FileChannel.transferTo(long position, long count, WriteableByteChannel target) 方法的实现就是使用操作系统提供的 ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 来实现的。
说了这么多,使用的Zero Copy后,上述过程会变成什么样呢?
1.cpu在用户态,调用read函数(系统调用)。 2.cpu从用户态切换到内核态,通过磁盘驱动程序,将磁盘中的文件内容copy到内核缓冲区(read buffer),这个copy过程无需cpu参与。 3.网卡程序直接从read buffer中读取数据通过网卡发送出去。
这里把上下文的切换次数从4次减少到2次,同时也把cpu的参与的copy的次数降低到了0。
总结
其实以上这些技术和原理,并不是kafka特有的,只不过kafka把他们充分利用了起来,而且使用的很好,可以作为这些技术的最佳实践了。那么我们自己的在工作中,也可以利用这里技术和特性,具体这么用,kafka的源码就是最好的例子。
在这里小编突然有一个感悟,现在很多第三方框架越来也多,每个框架都有自己的亮点,在某些方面性能很高,使用起来很方便等。但是稍微了解一下这个框架或者工作的特性原理后,就会发现:使用的计算机领域的原理和知识基本上都是在上学期间学习过的。知识还是那些知识,只不过用的姿势发生了变化。所以,与其学各种各样的框架,不如把基础知识学好,这样无论学习何种框架我们都可以明白它的大致工作原理,然后学习一下,这些框架是如何在细节上实践这些原理的,这样学习起来也会变得很轻松。
|