Kafka 凭什么支撑百万级 TPS?底层技术全揭秘

一提到大数据传输,Kafka必然会浮现在人们的脑海中。Kafka堪称大数据领域的杀手锏,在业界拥有众多成熟的应用场景,深受主流公司的认可与青睐。这款专为大数据而打造的消息中间件,凭借着百万级TPS(Transactions Per Second,每秒事务处理量)的惊人吞吐量声名远扬,迅速在大数据领域崭露头角,成为备受瞩目的焦点。在整个大数据生态中,从数据采集、传输,再到存储的各个环节,Kafka都扮演着举足轻重的角色,发挥着不可替代的关键作用。

在消息中间件领域,其实早已存在诸多成熟的产品,诸如RabbitMQ、RocketMQ、ActiveMQ、ZeroMQ等。那么,Kafka究竟凭什么能在众多强劲对手的重重包围中脱颖而出,稳稳占据一席之地呢?答案就是它那极为强悍的吞吐量。接下来,就让我们一同深入探寻,揭开Kafka高吞吐量背后的神秘面纱 。

Kafka 如何做到支持百万级 TPS ?

先用一张思维导图直接告诉你答案:

Kafka 支持百万TPS的秘密

顺序读写磁盘

生产者写入数据和消费者读取数据都是 顺序读写 的,先来一张图直观感受一下顺序读写和随机读写的速度:

顺序读写 VS 随机读写

从图中不难发现,传统硬盘或者SSD(固态硬盘)在顺序读写性能方面表现出色,其顺序读写速度甚至超越了内存的随机读写速度。当然,若将其与内存的顺序读写性能相比,仍存在较大差距。

基于这样的性能特性,Kafka选择顺序读写磁盘也就不难理解了。

接下来,以传统机械磁盘为例,为大家详细介绍顺序读写和随机读写的概念。

盘片与盘面

一块硬盘通常包含多块盘片,每块盘片都有上下两个面。其中,能够用于存储数据的有效面被称作盘面。在大多数情况下,盘片的上下两面均为有效面,所以可以得出这样的结论:盘面数 = 盘片数×2 。

磁头

磁头在切换磁道进行数据读写操作时,依靠的是机械设备来完成,这种机械动作通常速度相对较慢;而当磁头切换盘面进行数据读写时,借助的是电子设备,速度往往较快。因此,为了提高磁盘的读写效率,磁头一般会优先将同一柱面的数据读写完毕,再进行寻道操作(因为此时无需频繁切换磁道) 。

传统机械磁盘

磁道

磁道可看作是以磁盘中间轴为圆心的一系列同心圆环形轨迹。在每个盘面上,都分布着多个这样的磁道,并且磁道与磁道之间留有一定间隙,它是磁盘用于存储数据的关键介质。磁道上均匀覆盖着一层特殊的磁介质,当磁头在磁道上进行操作时,能够通过改变磁介质的极性状态,将其转换为相应的数据信号,这个过程便是磁盘的读取操作;而磁盘的写入操作则刚好相反,是将数据信号转化为磁介质的极性变化。

柱面

柱面是由磁盘中不同盘面里半径相同的磁道共同构成的一个概念。从这个定义可以清晰地得出,柱面的总数与某个盘面所拥有的磁道数量是完全相等的。例如,若一个盘面包含100个磁道,那么整个磁盘所具有的柱面总数同样为100个。

扇区

单个磁道是由多个呈弧形的扇区依次连接组成。值得注意的是,盘面上的每一个磁道所拥有的扇区数量是完全一致的。扇区作为磁盘存储数据的最小单元,在常见的磁盘系统中,其大小一般固定为512bytes。这意味着,无论存储何种类型的数据,磁盘在进行读写操作时,都是以512bytes的扇区为基本单位进行的 。

单盘片示意图

在磁盘数据读取场景中,如果系统每次仅读取一个扇区,那数据读取效率将会极其低下。正因如此,“block(块)”的概念应运而生。文件读取的最小单位是block,依据不同的操作系统,一个block通常由多个扇区组合而成。

了解了这些磁盘相关的基础知识后,理解顺序读写和随机读写就变得轻而易举了。

在此,我们先来看一下维基百科对于顺序读写和随机读写的定义:

  • 顺序读写:这是一种依照记录的逻辑顺序执行读、写操作的存取方式,即按照信息在存储器中的实际物理位置所确定的顺序来使用信息。
  • 随机读写:指的是在对存储器中的消息进行读取或写入操作时,所需耗费的时间与该段信息在存储器中的具体位置毫无关联。

具体到磁盘数据读取过程,当读取第一个block时,需要依次历经寻道、旋转延迟、传输这三个步骤,才能完整读取该block的数据。对于下一个block而言,如果它位于磁盘的其他任意位置,那么访问它时同样需要经历寻道、旋转延迟、传输这些过程,才能读取完数据,这种读取方式就被称为随机读写。然而,如果下一个block的起始扇区恰好紧接在刚才访问的block之后,磁头便能即刻抵达该位置,无需额外等待,可直接进行数据传输,这种读取方式则被称作顺序读写

接下来,我们再次回到Kafka的话题,深入且详细地介绍Kafka是如何实现顺序读写数据的。

Kafka在写入数据时采用的是顺序写入的方式。这里的每个Partition都可以类比为一个文件,每当Kafka接收到新的数据后,就会把数据追加插入到文件的末尾,图中的虚框部分即代表文件的末尾位置。

顺序写

这种方法有一个问题就是删除数据不方便,所以 Kafka 一般会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个 offset 用来记录读取进度或者叫坐标。

顺序读

Memory Mapped Files(MMAP)

在文章开篇,我们已经了解到硬盘的顺序读写速度基本能够与内存的随机读写速度相抗衡,不过,与内存的顺序读写速度相比,硬盘顺序读写还是显得颇为逊色。那么,对于追求极致效率的Kafka而言,若想进一步提升性能,又该采取何种策略呢?答案是借助现代操作系统的分页存储机制,以此充分发挥内存的优势,大幅提高I/O效率,而这,正是接下来要着重介绍的MMAP技术。

MMAP,即内存映射文件。在64位操作系统环境下,它通常能够映射高达20G的数据文件。其工作原理基于直接利用操作系统的Page(页),实现文件与物理内存之间的直接映射。一旦完成映射,对物理内存所进行的任何操作,都会自动同步到硬盘之上,这一特性极大地优化了数据读写的流程与效率 。

MMAP原理

借助MMAP技术,进程能够以如同读写硬盘般的方式对内存(逻辑内存)进行读写操作,并且无需担忧内存容量的限制,这得益于虚拟内存机制的支持。采用这种方式,I/O性能能够得到显著提升,因为它省去了数据在用户空间与内核空间之间复制的开销,使得数据读写过程更加高效流畅。

然而,MMAP技术也存在一个较为明显的缺陷。当数据写入MMAP时,实际上并未真正被写入硬盘,操作系统会在程序主动调用flush操作时,才会将数据实实在在地写入硬盘。这就意味着在调用flush之前,数据只是暂存在内存中,存在数据丢失的风险。

在Kafka中,为了应对这一情况,提供了一个名为producer.type的参数,用于控制是否主动执行flush操作。若Kafka在将数据写入MMAP后,立即执行flush操作,然后再向Producer返回响应,这种模式被称为同步(sync)模式;而如果Kafka在写入MMAP后,不调用flush操作,便立即向Producer返回响应,这种模式则被称为异步(async)模式。通过这两种模式的设置,Kafka用户可以根据自身业务场景对数据可靠性和写入性能的不同需求,灵活选择合适的写入方式 。

Zero Copy(零拷贝)

Kafka能够在大数据处理领域独占鳌头,离不开其诸多创新性技术,其中零拷贝技术堪称一大“黑科技”。要深入理解零拷贝技术的精妙之处,首先必须了解DMA。

什么是DMA?

我们都清楚,CPU的运行速度与磁盘IO速度相比,简直是天壤之别,若用形象的比喻,两者就如同火箭与乌龟。在常规的IO操作流程中,通常由CPU发出指令,然后只能眼巴巴地等待IO设备完成操作并返回结果,这就导致CPU会有大量时间处于等待IO操作完成的空闲状态。

然而,这种等待在多数情况下并没有太多实际价值。毕竟,我们对I/O设备的操作,大多只是将内存中的数据传输到I/O设备,或者反之。例如,在进行大文件复制时,如果所有数据都必须经过CPU中转,那无疑是对时间的极大浪费。

正是基于这样的背景,DMA技术应运而生。DMA,即直接内存访问(Direct Memory Access),它的出现极大地减少了CPU的等待时间,让数据传输更加高效。

Kafka零拷贝原理

在不使用零拷贝技术的情况下,当消费者(consumer)从Kafka消费数据时,Kafka需要从磁盘读取数据,然后再将数据发送到网络中。在这个过程中,数据总共会经历四次传输。其中,有两次传输是借助DMA完成的,而另外两次传输,则是在CPU的控制下进行的。

四次传输过程

第一次传输

数据从硬盘开始它的旅程,被搬运至操作系统内核的缓冲区。这一传输过程借助直接内存访问(DMA)技术完成,DMA就像一位高效的搬运工,无需CPU全程参与,自主完成数据从硬盘到内核缓冲区的转移,大大减轻了CPU的负担,让CPU得以从繁琐的数据搬运工作中解放出来,去处理其他更为关键的任务 。

第二次传输

数据抵达内核缓冲区后,紧接着进行第二次转移,被复制到应用程序分配的内存之中。这次搬运工作由CPU亲自上阵,CPU按照既定的指令,将数据从内核缓冲区精准无误地复制到指定的用户内存区域,确保数据转移的准确性和安全性 。

第三次传输

在应用程序完成相关处理后,数据又开启新的征程,从分配的内存转移到操作系统的Socket缓冲区。此过程同样由CPU主导,CPU依据网络通信协议和程序的需求,有条不紊地将数据搬运至Socket缓冲区,为后续的数据网络传输做好准备 。

第四次传输

数据在Socket缓冲区稍作停留后,迎来最后一次转移,被送往网卡的缓冲区。这次传输再次由DMA负责,DMA凭借其高效的数据传输能力,迅速将数据从Socket缓冲区搬运至网卡缓冲区,至此,数据完成了从硬盘到网卡的整个传输过程,为通过网络发送出去做好了最后的准备 。

实际上在kafka中只进行了两次数据传输,如下图:

两次传输,零拷贝技术

第一次传输

借助直接内存访问(DMA)技术,数据从硬盘出发,无需CPU过多干预,直接被读取到操作系统内核的读缓冲区中。DMA就像一位专业且高效的快递员,精准地将数据从硬盘这个“发货地”送达内核读缓冲区这个“中转站”,整个过程高效且流畅,极大地减轻了CPU的工作负担,让CPU能够将更多的精力投入到其他关键任务的处理中。

第二次传输

基于Socket的描述符所提供的详细信息,数据直接从内核读缓冲区写入到网卡的缓冲区。这一过程同样巧妙地避开了CPU的直接参与,而是在系统底层机制的协调下,实现了数据的高效转移。Socket描述符就像是一份详细的地图,指引着数据沿着最便捷的路径,从读缓冲区快速抵达网卡缓冲区,为数据的网络传输做好最后的准备。

对比传统的数据传输方式,在零拷贝技术的加持下,同一份数据的传输次数从原本的四次锐减至两次。更为关键的是,整个传输过程不再依赖CPU进行数据搬运,所有的数据转移任务都由DMA出色完成。在内存层面,数据无需进行额外的复制操作,这便是“零拷贝(Zero-Copy)”技术的核心所在。

零拷贝技术的优势极为显著,无论传输数据量是大是小,在传输相同数据时,采用零拷贝技术能够将传输时间缩短65%。这一惊人的效率提升,大幅提高了机器传输数据的吞吐量。也正因如此,零拷贝技术成为了Kafka能够支持百万TPS(每秒事务处理量)的重要基石,助力Kafka在大数据处理领域始终保持领先地位,高效地应对海量数据的传输与处理需求 。

Batch Data(数据批量处理)

当消费者(consumer)需要从Kafka获取数据时,可能我们下意识会认为是消费者每次请求一条,Kafka就响应发送一条,消费者再次请求,Kafka再发送下一条。但实际上,Kafka采用了更为巧妙的策略。

Kafka将所有消息存储在一个个文件当中。当消费者发起数据请求时,Kafka并不会逐条发送消息,而是直接把包含相关消息的文件发送给消费者。举例来说,假设有100万条消息被整合在一个文件里,数据量可能达到10M。倘若消费者与Kafka之间的网络状况良好,那么这10MB的数据大约1秒钟就能完成发送,换算下来,相当于Kafka每秒处理了100万次事务(TPS),也就是每秒处理了100万条消息。

看到这里,你或许会心生疑惑:消费者有时仅仅需要一条消息,而Kafka却把整个文件都发送过来了,那文件里剩余的消息该如何处理呢?其实,这里就不得不提到消费者的offset机制。消费者可以通过offset来精准记录自身的消费进度,这样即便收到包含大量消息的文件,也能依据offset准确获取到自己需要的那条消息,同时也不会对后续消费造成干扰。

此外,以文件形式发送数据还有一个显著优势,那就是可以对文件进行批量压缩。通过批量压缩,能够有效减少数据在网络传输过程中的体积,进而降低网络IO损耗,提升数据传输效率,让Kafka在数据处理与传输方面的表现更加出色 。

总结

最后,让我们再度深入剖析Kafka能够支撑百万级TPS的核心秘诀:

  1. 顺序写入,极致速度:Kafka采用顺序写入策略,在Partition的末尾直接追加数据,这种方式避免了随机写入带来的寻址开销,从而达成了磁盘I/O速度的最大化。
  2. MMAP技术,无缝衔接:借助MMAP(内存映射)技术,Kafka巧妙地将磁盘文件与内存进行映射,使得对内存的操作就如同直接操作磁盘文件,极大地提升了数据读写效率。
  3. DMA零拷贝,高效传输:利用DMA(直接内存访问)技术实现零拷贝,Kafka有效减少了数据在内存与磁盘之间的传输次数,避免了不必要的数据拷贝,显著提升了数据传输效率。
  4. 批量处理,减少损耗:在读取数据时,Kafka结合sendfile函数直接将数据高效输出,并通过批量压缩的方式,将所有消息整合成一个批量文件,从而合理地减少了网络I/O的损耗,提升了整体性能。