当前位置: 首页 > 科技观察

多年资深程序员总结:解密Kafka吞吐量高的原因

时间:2023-03-17 22:40:53 科技观察

众所周知,Kafka的吞吐量高于普通消息队列。它号称速度最快,那么它是如何做到的呢,让我们从以下几个方面来分析一下原因。生产者(写入数据)生产者(producer)负责向Kafka提交数据,我们先来分析这部分。Kafka会将所有接收到的消息写入硬盘,永远不会丢失数据。为了优化写入速度,Kafak使用了顺序写入和MMFile两种技术。顺序写入因为硬盘是机械结构,每次读写都会寻址->写入,而寻址是一个“机械动作”,是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。Kafka为了提高读写硬盘的速度,采用了顺序I/O。上图展示了Kafka是如何写入数据的。每个Partition其实就是一个文件。Kafka收到消息后,会在文件末尾(虚帧部分)插入数据。这种方式有一个缺陷——没有办法删除数据,所以Kafka不会删除数据,它会保留所有的数据,每个消费者(Consumer)对于每个Topic都有一个偏移量,代表读取了多少条数据.在上图中,有两个消费者。Consumer1有两个offset分别对应Partition0和Partition1(假设每个topic都有一个Partition);Consumer2有一个offset对应Partition2。这个offset是客户端SDK保存的,Kafka的Broker完全忽略这个东西的存在;一般情况下,SDK会保存在zookeeper中。(所以需要向Consumer提供zookeeper的地址)。如果不删除硬盘,它就会被填满,所以Kakfa提供了两种删除数据的策略。一种是基于时间,另一种是基于分区文件大小。具体配置请参考其配置文档。即使按顺序将MemoryMappedFiles写入硬盘,硬盘的访问速度也赶不上内存。所以Kafka的数据并不是实时写入硬盘的。它充分利用现代操作系统的分页存储来使用内存来提高I/O效率。MemoryMappedFiles(以下简称mmap)也译为内存映射文件。在64位操作系统中,一般可以表示一个20G的数据文件。它的工作原理是直接使用操作系统的Page将文件直接映射到物理内存。.映射完成后,你对物理内存的操作会同步到硬盘(操作系统在合适的时候)。通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),不需要关心内存的大小,虚拟内存会帮我们覆盖。使用这种方式可以获得很多I/O的提升,省去了从用户空间拷贝到内核空间的开销(调用文件的read会先把数据放在内核空间的内存中,然后再拷贝到内核空间用户空间的内存中等。)还有一个明显的缺陷——不可靠,写入mmap的数据并没有真正写入硬盘,直到程序主动调用操作系统才会真正将数据写入硬盘冲洗。Kafka提供了一个参数——producer.type来控制是否主动flush。如果Kafka写入mmap,则立即flush,然后返回给Producer,称为同步(sync);写入mmap后,不调用flush,立即返回Producer,这叫异步(async)。mmap其实是Linux中的一个函数,用来实现内存映射。多亏JavaNIO,它为我提供了一个mappedbytebuffer类,可以用来实现内存映射(所以才有可能借助Java的光这么快,和Scala一点关系都没有!!)Consumer(读取数据)Kafka用的是磁盘文件,想快点?这是我看到卡夫卡后的第一个问题。ZeroMQ根本没有任何服务器节点,也不使用硬盘。按理说应该比Kafka快。不过在实际测试中,其速度还是被Kafka“打败”了。“硬盘比RAM快”绝对是违反常识的;如果发生这种情况-这是作弊。没错,卡夫卡“作弊”了。顺序写和mmap其实都是为作弊做准备。如何提高WebServer静态文件的速度?仔细想想,如何优化一个WebServer来传递一个静态文件呢?答案是零拷贝。传统模式下,当我们从硬盘读取文件时,先拷贝到内核空间(read是系统调用,放在DMA中,所以用内核空间),然后再拷贝到用户空间(1,2);再从用户空间copy到内核空间(你用的socket是系统调用,所以也有自己的内核空间),最后送到网卡(3、4)。在ZeroCopy中,直接从内核空间(DMA)到内核空间(Socket),再发送网卡。这个技术很常见,在TheC10Kproblem中也有详细介绍。Nginx也用到了这个技术,稍微搜索一下就能找到很多资料。Java的NIO提供了FileChannle,其transferTo和transferFrom方法是ZeroCopy。卡夫卡是怎么玩花样的?你想过吗?Kafka将所有消息一一存储在文件中。当消费者需要数据时,Kafka直接将“文件”发送给消费者。这就是秘密所在。比如:10W条消息组合成10MB的数据量,然后Kafka用类似发送文件的方式扔出去。如果消费者和生产者之间的网络很好(只要网络稍微正常10MB完全不是问题...家里的带宽是100Mbps),10MB可能只需要1s。所以答案是——10WTPS,Kafka每秒处理10W条消息。也许你说:不可能发送整个文件吧?里面有什么不受欢迎的消息吗?是的,卡夫卡作为一个“高级作弊者”,自然要正儿八经地作弊。零拷贝对应sendfile函数(以linux为例),接受out_fd作为输出(一般在timesockethandle)in_fd作为输入文件句柄off_t表示in_fd的偏移量(从哪里开始读)size_t表示读了多少对,kafka使用mmap作为读写文件的方式,是文件句柄,所以直接传给sendfile;offset也很好解决,用户会自己保存这个offset,每次请求的时候发送这个offset。(记得吗?放在zookeeper里面);数据量比较好解决,消费者想要快点,就全丢给消费者吧。如果你这样做,一般情况下,消费者会被直接压死;所以Kafka提供了两种方式——Push,我全丢给你,你死了也不管我的事;拉,好吧,你告诉我你需要多少,我给你多少。总结Kafka速度的秘诀在于它将所有消息转换为单个文件。通过mmap提高I/O速度。写入数据时,在最后添加,速度最优;读取数据时配合sendfile直接暴力输出。阿里的RocketMQ也是这种模式,不过是用Java写的。单纯的测试MQ的速度是没有意义的。Kafka这种“暴力”、“流氓”、“无耻”的做法,早已脱下了MQ的裤衩,更像是一个暴力的“数据传输器”。所以,对一个MQ的评价,只是看英雄的速度。世界上没有人会卡夫卡。我们在设计的时候,不能听信网上的谣言——“Kafka最快,大家都在用,所以我们MQ用的是Kafka,错了”。在这种思想的影响下,你可能根本不关心“失败者”;事实上,这些“失败者”可能是更适合你业务的MQ。