当前位置: 首页 > Linux

3台廉价机器每秒写入200万条!为什么卡夫卡这么快?

时间:2023-04-06 19:44:31 Linux

作者:冰月,简介:阿里高级工程师,关注分布式系统和高可用架构,方法论和认知升级,践行持续学习。来源:www.cnblogs.com/binyue/p/10308754.html30秒Kafka简评让我们快速回顾一下Kafka是什么以及它是如何工作的一些细节。Kafka是一个分布式消息系统,最初由LinkedIn构建,现在是Apache软件基金会的一部分,并被各种公司使用。Kafka的消息保存或缓存在磁盘上。一般认为在磁盘上读写数据会降低性能,因为寻址会消耗更多的时间,但实际上Kafka的一个特点就是高吞吐量。即使是普通的服务器,Kafka也可以轻松支持每秒百万级的写入请求,超越大部分消息中间件。这一特性也使得Kafka广泛应用于日志处理等海量数据场景。一般配置很简单。生产者向集群发送记录,记录下来,交给消费者。Kafka中一个重要的抽象是主题。生产者向主题发布记录,消费者订阅一个或多个主题。Kafka主题只是分片上的预写日志。生产者将记录附加到这些日志中,消费者订阅更改。每条记录都是一个键/值对。这些键用于将记录分配给特定的日志分区(除非发布者直接指定分区)。这是单个生产者和消费者从两个分区主题读取和写入的简单示例。此图显示了一个生产者进程附加到两个分区的日志,以及一个从同一日志读取的消费者。日志中的每条记录都有一个关联的条目号,我们称之为偏移量。消费者使用偏移量来描述它在每个日志中的位置。这些分区分布在集群中,允许主题比任何一台机器保存更多的数据。请注意,与大多数消息系统不同,日志始终是持久的。收到消息后,它会直接写入文件系统。消息在阅读时不会被删除,而是根据可配置的SLA(比如几天或一周)保留。这允许在数据消费者可能需要重新加载数据的情况下使用。它还可以支持节省空间的发布-订阅,因为无论消费者有多少,都只有一个共享日志;在传统的消息系统中,通常每个消费者都有一个队列,因此添加一个消费者可以使您的数据量增加一倍。这使得Kafka非常适合普通消息系统之外的事物,例如用作Hadoop等离线数据系统的管道。这些离线系统只能作为周期性ETL周期的一部分定期加载,或者可能需要数小时进行维护,在此期间Kafka能够在需要时缓冲TB级未使用的数据。Kafka还将日志复制到多个服务器以实现容错。与其他消息系统相比,副本实现的一个重要架构是复制不需要复杂的配置,只有在非常特殊的情况下才会使用。复制被假定为默认值:我们将未复制的数据视为复制因子恰好为1的特殊情况。当生产者发布包含偏移量的消息时,生产者会收到确认。发布到分区的第一条记录返回偏移量0,第二条记录返回偏移量1,并按顺序增长。消费者从偏移量指定的位置开始消费数据,并通过周期性的提交保存日志中的位置:保存这个偏移量,万一消费者实例崩溃,另一个实例可以从偏移量位置恢复。希望这会有所帮助(如果没有,您可以在此处阅读更完整的kafka介绍)。Kafka的benchmark测试可以参考ApacheKafka的benchmark测试《每秒写入 2 百万(在三台廉价机器上)》:http://ifeve.com/benchmarking...下面我们从数据写入和读取两个方面来分析一下Kafka为什么这么快。向Kafka写数据会将所有接收到的消息写入硬盘,永远不会丢失数据。为了优化写入速度,Kafka使用了顺序写入和MMFile(MemoryMappedFile)两种技术。顺序写磁盘读写的速度取决于你的使用方式,即顺序读写或随机读写。在顺序读写的情况下,磁盘的顺序读写速度与内存的顺序读写速度相等。因为硬盘是机械结构,每次读写都会寻址->写入,而寻址是一个“机械动作”,是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。Kafka为了提高读写硬盘的速度,采用了顺序I/O。而且Linux对磁盘读写有很多优化,包括先读后写,磁盘缓存等,如果在内存中做这些操作,一是Java对象的内存开销很大,二是就是随着堆内存数据的增加,Java的GC时间会变得很长。使用磁盘操作有以下优点:磁盘的顺序读写速度比内存的随机读写要快。JVM的GC效率低,内存占用大。使用磁盘可以避免这个问题。系统冷启动后,磁盘缓存仍然可用。下图展示了Kafka是如何写入数据的。每个Partition其实就是一个文件。Kafka收到消息后,会在文件末尾插入数据(虚拟盒子里的部分):这种方式有一个缺陷——没有办法删除数据,所以Kafka不会删除数据,它会保留所有的数据,每个消费者(Consumer)对于每个Topic都有一个Offset来表示读取了哪条数据。两个消费者:Consumer1有两个offset分别对应Partition0和Partition1(假设每个Topic一个Partition)。Consumer2有一个Offset对应Partition2。这个Offset是客户端SDK保存的,Kafka的Broker完全忽略了这个东西的存在。一般SDK会保存在Zookeeper中,所以需要将Zookeeper的地址提供给Consumer。硬盘如果不删除,肯定是满的,所以Kakfa提供了两种删除数据的策略:基于时间和基于Partition文件大小。具体配置请参考其配置文档。即使按顺序将MemoryMappedFiles写入硬盘,硬盘的访问速度也赶不上内存。所以Kafka的数据并不是实时写入硬盘的。它充分利用现代操作系统的分页存储来使用内存来提高I/O效率。MemoryMappedFiles(以下简称mmap)也译为内存映射文件。在64位操作系统中,一般可以表示一个20G的数据文件。它的工作原理是直接使用操作系统的Page将文件直接映射到物理内存。.映射完成后,你对物理内存的操作会同步到硬盘(操作系统在合适的时候)。通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机的内存),不需要关心内存的大小,因为虚拟内存是给我们的。使用这种方法可以获得大量的I/O改进,消除了从用户空间复制到内核空间的开销。(文件的读取会先把数据放到内核空间的内存中,然后再拷贝到用户空间的内存中)但是也有一个明显的缺陷——不可靠,写入mmap的数据并不是真正写入的时候到硬盘,当程序主动调用Flush时,操作系统才真正将数据写入硬盘。Kafka提供了一个参数producer.type来控制是否主动Flush:如果Kafka写入mmap,会立即flush,然后返回给Producer称为同步(Sync)。如果Kafka写入mmap后立即返回,Producer不会调用Flush,这称为异步(Async)。Kafka在从磁盘读取数据时做了哪些优化?基于Sendfile实现零拷贝传统模式下,当需要传输文件时,具体过程如下:调用Read函数,将文件数据拷贝到内核缓冲区。Read函数返回,将文件数据从内核缓冲区复制到用户缓冲区,调用Write函数将文件数据从用户缓冲区复制到与Socket相关的内核缓冲区。数据从套接字缓冲区复制到相关协议引擎。以上是传统的网络文件传输Read/Write方式。我们可以看到,在这个过程中,文件数据实际上经历了四次拷贝操作:硬盘—>内核buf—>用户buf—>Socket相关Buffer—>协议引擎,Sendfile系统调用提供了减少上述多次拷贝的方法并提高文件传输性能。在内核版本2.1中,引入了Sendfile系统调用以简化网络上和两个本地文件之间的数据传输。Sendfile的引入不仅减少了数据拷贝,也减少了上下文切换。发送文件(套接字,文件,长度);运行过程如下:Sendfile系统调用,将文件数据复制到内核缓冲区。然后从内核缓冲区copy到内核中Socket相关的缓冲区。最后将Socket相关的buffer复制到协议引擎中。与传统的Read/Write方式相比,2.1版本内核中引入的Sendfile减少了从内核缓冲区到User缓冲区,再从User缓冲区到Socket相关缓冲区的文件拷贝。2.4内核版本之后,文件描述符的结果发生了变化,Sendfile实现了更简单的方法,再次减少了一次Copy操作。在Apache、Nginx、Lighttpd等Web服务器中,都有Sendfile相关的配置。使用Sendfile可以大大提高文件传输性能。Kafka将所有消息一一存储在文件中。当消费者需要数据时,Kafka将文件直接发送给消费者。以mmap为文件读写方式,直接发送给Sendfile。BatchCompression在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO,特别是对于需要在广域网上的数据中心之间发送消息的数据管道。数据压缩会消耗少量的CPU资源,但是对于Kafka来说,要考虑网络IO:因为每条消息都进行了压缩,但是压缩率比较低,所以Kafka采用的是批量压缩,即多条消息一起压缩而不是单个消息压缩。Kafka允许递归收集消息。成批消息可以以压缩形式传输,并在日志中保持压缩状态,直到被消费者解压缩。Kafka支持多种压缩协议,包括Gzip和Snappy压缩协议。总结Kafka速度的秘诀就是把所有的消息都变成一批文件,并进行合理的批次压缩,减少网络IO损失,通过mmap提高I/O速度。写数据时,由于最后加了一个Partion,所以速度最优;读取数据时配合Sendfile直接暴力输出。