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

Kafka性能:为什么Kafka这么“快”?

时间:2023-03-12 11:05:12 科技观察

《码哥》的Redis系列文章有一篇讲解Redis的性能优化——《Redis 核心篇:唯快不破的秘密》。从IO、线程、数据结构、编码等方面深入剖析Redis“快”的内在秘密。65弟兄深受启发。在学习Kafka的过程中,发现Kafka也是一个性能非常优秀的中间件,于是找“码哥”聊聊Kafka性能优化的知识,于是“码哥”决定把这篇性能方面的博文作为卡夫卡系列的开始。Kafka系列文章先睹为快,敬请期待:从性能讲解开始我们的Kafka之旅,让我们一起深入了解Kafka“坚不可摧”的内在秘密。不仅可以学习到Kafka性能优化的各种方法,还可以提炼出各种性能优化方法论,同样可以应用到我们自己的项目中,帮助我们写出高性能的项目。关公展秦琼《65:Redis和Kafka是功能完全不同的中间件,有没有可比性?》是的,所以这篇文章不是关于《分布式缓存的选型》,也不是?。我们针对这两个不同领域的项目进行性能优化,看看优秀项目性能优化的通用方法,以及针对不同场景特点的优化方法。很多人学了很多,懂了很多框架,但是遇到实际问题时,往往会觉得自己知识匮乏。这就是没有把所学的知识系统化,没有从具体的实施中抽象出有效的方法论。学习开源项目最重要的一点就是归纳,将不同项目的优秀实现总结成方法论,然后推演到自己的实践中。开篇寄语码哥:理性、客观、审慎是程序员的特点和优势,但很多时候我们也需要感性一点,冲动一点。这个时候,它可以帮助我们更快地做出决定。“悲观者正确,乐观者成功”。我希望每个人都是一个乐观地解决问题的人。《Kafka性能全景图》从高度抽象的角度来看,性能问题无法逃避以下三个方面:网络磁盘复杂度对于Kafka这样的网络分布式队列来说,网络和磁盘是最重要的优化方面。对于上面提出的抽象问题,解决方案高度抽象,也很简单:并发压缩批量缓存算法知道了问题和思路,我们来看看Kafka中的角色,这些角色就是可以优化的点:ProducerBrokerConsumer是的,所有的问题,思路,优化点都列出来了。我们可以尽可能细化,三个方向都可以细化。这样一来,所有的实现一目了然,即使你不看Kafka的实现,我们也可以想到一两个可以优化的点。这就是思考的方式。提出问题>列出问题点>列出优化方法>列出具体切入点>权衡和具体实现。现在,你也可以尝试自己去思考优化的点和方法。不需要十全十美,也不需要纠结好不好,稍微想想就可以了。65哥:不行,我又蠢又懒,你直接告诉我吧,我比较擅长免费嫖。《顺序写》65哥:Redis是纯内存系统,你的kafka也需要读写磁盘,能比吗?我们又得回到在学校学的操作系统课程。65哥,课本还有吗?来吧,翻到关于磁盘的章节,让我们回顾一下磁盘的工作原理。65哥:鬼还留着,书还没上一半就没了。要不是我考试视力好,估计还没毕业。”看经典大图:要完成一次磁盘IO,需要经过三个步骤:寻道、旋转、数据传输。影响磁盘IO性能的因素也发生在上述三个步骤中,所以主要花费的时间是:寻道时间:Tseek是指将读写磁头移动到正确磁道所需要的时间。寻道时间越短,I/O操作越快。目前磁盘的平均寻道时间一般为3-15ms。旋转延迟:旋转是指磁盘旋转将请求数据所在的扇区移动到读写磁盘所需的时间。旋转延迟取决于磁盘的旋转速度,通常表示为磁盘转一圈所需时间的1/2。例如:7200rpm磁盘的平均旋转延迟约为60*1000/7200/2=4.17ms,而15000rpm磁盘的平均旋转延迟为2ms。数据传输时间:Ttransfer是指完成请求数据传输所需的时间,它取决于数据传输速率,其值等于数据大小除以数据传输速率。目前IDE/ATA可以达到133MB/s,SATAII可以达到300MB/s的接口数据传输率,数据传输时间通常比前两部分消耗的时间要短很多。简单计算时可以忽略。因此,如果在写入磁盘时省略寻道和旋转,则可以大大提高磁盘读写性能。Kafka使用顺序写入文件来提高磁盘写入性能。按顺序写入文件基本上减少了磁盘寻道和旋转的次数。磁头不再需要在磁轨上晃动,而是全速前进。Kafka中的每个分区都是有序且不可变的消息序列。新消息不断附加到分区的末尾。在Kafka中,Partition只是一个逻辑概念。Kafka将Partition划分为多个Segment,每个Segment对应一个物理文件,Kafka追加到Segment文件上,也就是顺序写入文件。65师兄:为什么Kafka可以使用append的方式?》这个跟Kafka的本质有关,我们先看看Kafka和Redis,说白了,Kafka是一个Queue,而Redis是一个HashMap。Queue和Map有什么区别?Queue是FIFO,而数据是有序的;HashMap数据是无序的,是随机读写的。Kafka的不可变性和顺序性使得Kafka以追加的方式写文件。实际上很多满足上述特点的数据系统可以使用追加写入来优化磁盘performance.通常有Redis的AOF文件,各种数据库WAL(Writeaheadlog)机制等,所以,如果清楚了解自己业务的特点,就可以有针对性的进行优化。零文案》65哥:哈哈,面试的时候被问到这个问题,可惜答案一般般,唉。什么是零拷贝?从Kafka的角度来看,KafkaConsumer消费的是存储在Broker磁盘上的数据,从读取Broker磁盘到网络传输给Consumer,这期间都涉及到哪些系统交互。KafkaConsumer从Broker消费数据,Broker读取Log,使用sendfile。如果使用传统IO模型,伪代码逻辑如下:readFile(buffer)send(buffer)如图,如果使用传统IO流程,先读网络IO,再写磁盘IO,实际上需要复制四次数据。第一次:读取磁盘文件到操作系统的内核缓冲区;第二次:将内核缓冲区中的数据复制到应用程序的缓冲区中;第三步:将应用程序的buffer中的数据复制到socket网络中,发送给Buffer;第四次:将socketbuffer数据拷贝到网卡,网卡通过网络传输。65哥:啊,操作系统有这么傻吗?抄来抄去。“不是说操作系统傻,操作系统的设计是每个应用程序都有自己的用户内存,用户内存和内核内存是隔离的,这是为了程序和系统安全着想。没关系不过也有零拷贝技术,英文-Zero-Copy,零拷贝就是尽可能的减少上面数据的拷贝次数,从而减少拷贝的CPU开销,减少context的次数用户态和内核态的切换,从而优化数据传输的性能,常见的零拷贝思路有以下三种:DirectI/O:数据直接跨过内核,在用户地址空间和I/O设备之间传输.内核只进行必要的虚拟存储配置等辅助工作;避免在内核空间和用户空间之间复制数据:当应用程序不需要访问数据时,可以避免将数据从内核空间复制到用户空间;copy-on-write:数据a不需要事先复制,而是在需要修改时进行部分复制。Kafka使用mmap和sendfile实现零拷贝。分别对应Java的MappedByteBuffer和FileChannel.transferTo。使用JavaNIO实现零拷贝,如下:FileChannel.transferTo()在这种模型下,上下文切换次数减少为一次。具体来说,transferTo()方法指示块设备通过DMA引擎将数据读入读取缓冲区。然后将该缓冲区复制到另一个内核缓冲区以暂存到套接字。最后,套接字缓冲区通过DMA复制到NIC缓冲区。我们将副本数量从四个减少到三个,其中只有一个副本涉及CPU。我们还将上下文切换的次数从四个减少到两个。这是一个很大的改进,但是还没有零拷贝查询。后者可以在运行Linux内核2.4及更高版本和支持收集操作的网络接口卡时作为进一步优化来实现。如下。按照前面的示例,调用transferTo()方法会导致设备通过DMA引擎将数据读入内核读取缓冲区。但是,当使用gather操作时,读取缓冲区和套接字缓冲区之间没有复制。相反,NIC被赋予一个指向读取缓冲区的指针以及一个偏移量和长度,这由DMA清除。CPU绝对不参与复制缓冲区。关于零拷贝的详细内容可以阅读这篇文章零拷贝(Zero-copy)分析及其应用。当PageCacheproducer产生消息给Broker时,Broker会根据offset使用pwrite()系统调用【对应JavaNIO的FileChannel.write()API】写入数据。这时候数据会先写入到pagecache中。当consumer消费消息时,Broker通过sendfile()系统调用[对应FileChannel.transferTo()API]将数据从pagecache零拷贝传输到broker的Socketbuffer,然后通过网络传输.leader和follower之间的同步和上面consumer消费数据的过程是一样的。pagecache中的数据会随着内核flusher线程的调度和sync()/fsync()的调用被写回磁盘。即使进程崩溃,也不用担心数据丢失。另外,如果consumer要消费的消息不在pagecache中,会从磁盘中读取,顺便预读一些相邻的block放入pagecache中,方便下一次读。因此,如果Kafka生产者的生产率和消费者的消费率相差不大,那么整个生产-消费过程几乎只需要读写brokerpagecache就可以完成,磁盘访问很少。网络模型《65哥:网络,作为Java程序员自然是Netty》没错,Netty是JVM领域一个优秀的网络框架,提供高性能的网络服务。大多数Java程序员一提到网络框架,第一个想到的就是Netty。Dubbo、Avro-RPC等优秀的框架都使用Netty作为底层网络通信框架。Kafka本身将网络模型实现为RPC。底层基于JavaNIO,使用与Netty相同的Reactor线程模型。Reacotr模型主要分为三个角色。Reactor:将IO事件分配给相应的handlers来处理Acceptor:处理客户端连接事件Handler:处理非阻塞任务。在传统的阻塞IO模型中,每个连接都需要一个独立的线程来处理。并发数大时,创建线程数大,占用资源;采用阻塞IO模型。连接建立后,如果当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费。针对传统阻塞IO模型的两个问题,Reactor模型基于池化的思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池;基于IO多路复用模型,多个连接共享同一个阻塞对象,无需等待所有连接。当遍历到新的可以处理的数据时,操作系统会通知程序,线程会跳出阻塞状态,进行业务逻辑处理。Kafka基于Reactor模型实现多路复用和处理线程池。它的设计如下:它包含一个Acceptor线程,用于处理新的连接。Acceptor有N个Processor线程选择和读取socket请求,有N个Handler线程处理请求和响应,即处理业务逻辑。I/O多路复用可以将多个I/O块多路复用到同一个select块中,这样系统就可以在单线程的情况下同时处理多个客户端请求。它最大的优点是系统开销小,不需要创建新的进程或线程,减少了系统资源开销。总结:KafkaBroker的KafkaServer设计是一个优秀的网络架构。想了解Java网络编程或者需要使用该技术的同学不妨阅读一下源码。《码哥》后续的Kafka系列文章也会涉及到对这段源码的解读。批处理和压缩KafkaProducer将消息发送给Broker,而不是一条一条地发送消息。用过Kafka的同学应该都知道,Producer有两个重要的参数:batch.size和linger.ms。这两个参数与Producer批量发送有关。KafkaProducer的执行流程如下图所示:依次通过以下处理器发送消息:Serialize:key和value都根据传入的serializer进行序列化。一个优秀的序列化方法可以提高网络传输的效率。Partition:决定将消息写入topic的哪个分区,默认遵循murmur2算法。还可以将自定义分区程序传递给生产者,以控制应将消息写入哪个分区。压缩:默认情况下,Kafka生产者不启用压缩。压缩不仅可以加快从生产者到代理的传输速度,还可以加快复制过程中的传输速度。压缩有助于提高吞吐量、降低延迟并提高磁盘利用率。Accumulate:顾名思义,Accumulate是一个消息累加器。它内部为每个Partition维护了一个Deque双端队列。队列存储要发送的批处理数据。Accumulate将数据累积到一定数量,或者在一定的有效期内分批发送数据。记录在主题的每个分区的缓冲区中累积。根据生产者批量大小属性对记录进行分组。主题中的每个分区都有一个单独的累加器/缓冲区。GroupSend:在accumulator中记录分区的批次按它们发送到的broker分组。根据batch.size和linger.ms属性将记录分批发送给代理。生产者根据两个条件发送记录。当达到定义的批量大小时或达到定义的延迟时间时。Kafka支持多种压缩算法:lz4、snappy、gzip。Kafka2.1.0正式支持ZStandard-ZStandard是Facebook开源的一种压缩算法,旨在提供超高的压缩比(compressionratio)。有关详细信息,请参阅zstd。Producer、Broker和Consumer使用相同的压缩算法。当Producer向Broker写入数据时,Consumer从Broker读取数据时甚至不需要解压。最后在ConsumerPoll收到消息的时候进行解压,这样可以节省大量的网络和磁盘开销。分区并发Kafka的topic可以分为多个Partition,每个Partition类似于一个队列,保证数据有序。同一个Group下的不同Consumer同时消费Partition。分区实际上是调优Kafka并行度的最小单位。因此,可以说每增加一个Partition,就增加一个消费并发。Kafka有一个优秀的分区分配算法——StickyAssignor,可以保证分区分配尽可能均衡,每次重新分配的结果尽可能与上次分配结果一致。这样整个集群的分区尽量平衡,各个Broker和Consumer的处理不会太偏斜。65哥:那不是分区越多越好吗?当然不是。更多的分区需要打开更多的文件句柄。在Kafka的代理中,每个分区将与文件系统中的一个目录进行比较。在Kafka的数据日志文件目录中,每个日志数据段都会分配两个文件,一个索引文件和一个数据文件。因此,随着分区数量的增加,所需的文件句柄数量急剧增加,必要时需要对操作系统进行调整。文件句柄数。客户端/服务器需要使用的内存越多。客户端producer有一个参数batch.size,默认是16KB。它会为每个分区缓存消息,一旦满了,它会打包并批量发送消息。看样子这是一个可以提升性能的设计。但显然,因为这个参数是分区级别的,如果分区数量增加,这部分缓存需要的内存占用也会增加。高可用分区减少的越多,每个Broker在网络上分配的分区就越多,当一个Broker崩溃时,恢复时间会很长。文件结构Kafka消息以Topic为单位进行分类,每个Topic之间相互独立,互不影响。每个ATopic可以划分为一个或多个分区。每个分区都有一个日志文件,用于记录消息数据。Kafka的每个partitionlog在物理上根据大小被划分为多个Segment。段文件组成:由2部分组成,即索引文件和数据文件。这两个文件一一对应,成对出现。后缀“.index”和“.log”分别代表段索引文件和数据文件。段文件命名规则:partitionglobal的第一个段从0开始,后面的每个段文件的名字都是前一个段文件最后一条消息的偏移值。该值最长可达64位长,19位字符长度,无数字补0。索引采用稀疏索引,使每个索引文件的大??小受到限制。Kafka使用mmap的方式将索引文件直接映射到内存中,这样索引的操作就不需要操作磁盘IO了。mmap的Java实现对应于MappedByteBuffer。65兄笔记:mmap是一种内存映射文件的方法。即把文件或其他对象映射到进程的地址空间,实现文件磁盘地址与进程虚拟地址空间中的虚拟地址的一一映射关系。实现了这样的映射关系后,进程就可以使用指针对这段内存进行读写,系统会自动将脏页回写到对应的文件盘中,即不调用read就完成了对文件的操作,write等系统调用函数。相反,内核空间对这个区域的修改也直接反映到用户空间,这样就可以实现不同进程之间的文件共享。》Kafka充分利用二分法寻找offset对应的消息位置:根据二分法找到小于offset的段的.log和.index,目标offset减去文件名中的offset为得到消息在这个段的偏移量,再次使用二分法在索引文件中找到对应的索引,到日志文件中依次查找,直到找到偏移量对应的消息。综上所述,Kafka是一个优秀的开源项目,其性能优化可谓是淋漓尽致,值得深入研究的项目,无论是思想还是实现,都应该好好看看,认真思考一下。Kafka性能优化:零拷贝网络和磁盘优秀的网络模型,基于JavaNIO高效的文件数据结构设计Parition并行可扩展的数据批量传输数据压缩顺序读写磁盘无锁轻量级偏移本文转载自微信公众号「码哥字节」,可通过以下二维码关注。转载本文请联系码哥字节公众号。