Kafka 为何快

Kafka 为何能那么快 | Kafka高效读写数据的缘由

不管 kafka 做为 MQ 也好,做为存储层也罢,无非就是两个功能(好简单的样子),一是 Producer 生产的数据存到 broker,二是 Consumer 从 broker 读取数据。那 Kafka 的快也就体如今读写两个方面了,下面咱们就聊聊 Kafka 快的缘由。html

1. 利用 Partition 实现并行处理

咱们都知道 Kafka 是一个 Pub-Sub 的消息系统,不管是发布仍是订阅,都要指定 Topic。java

Topic 只是一个逻辑的概念。每一个 Topic 都包含一个或多个 Partition,不一样 Partition 可位于不一样节点。linux

一方面,因为不一样 Partition 可位于不一样机器,所以能够充分利用集群优点,实现机器间的并行处理。另外一方面,因为 Partition 在物理上对应一个文件夹,即便多个 Partition 位于同一个节点,也可经过配置让同一节点上的不一样 Partition 置于不一样的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优点。web

能并行处理,速度确定会有提高,多个工人确定比一个工人干的快。面试

能够并行写入不一样的磁盘?那磁盘读写的速度能够控制吗?算法

那就先简单扯扯磁盘/IO 的那些事apache

硬盘性能的制约因素是什么?如何根据磁盘I/O特性来进行系统设计?缓存

硬盘内部主要部件为磁盘盘片、传动手臂、读写磁头和主轴马达。实际数据都是写在盘片上,读写主要是经过传动手臂上的读写磁头来完成。实际运行时,主轴让磁盘盘片转动,而后传动手臂可伸展让读取头在盘片上进行读写操做。磁盘物理结构以下图所示:安全

因为单一盘片容量有限,通常硬盘都有两张以上的盘片,每一个盘片有两面,均可记录信息,因此一张盘片对应着两个磁头。盘片被分为许多扇形的区域,每一个区域叫一个扇区。盘片表面上以盘片中心为圆心,不一样半径的同心圆称为磁道,不一样盘片相同半径的磁道所组成的圆柱称为柱面。磁道与柱面都是表示不一样半径的圆,在许多场合,磁道和柱面能够互换使用。磁盘盘片垂直视角以下图所示:网络

图片来源:commons.wikimedia.org
图片来源:commons.wikimedia.org

影响磁盘的关键因素是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。

机械硬盘的连续读写性能很好,但随机读写性能不好,这主要是由于磁头移动到正确的磁道上须要时间,随机读写时,磁头须要不停的移动,时间都浪费在了磁头寻址上,因此性能不高。衡量磁盘的重要主要指标是IOPS和吞吐量。

在许多的开源框架如 Kafka、HBase 中,都经过追加写的方式来尽量的将随机 I/O 转换为顺序 I/O,以此来下降寻址时间和旋转延时,从而最大限度的提升 IOPS。

感兴趣的同窗能够看看 磁盘I/O那些事

磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。

2. 顺序写磁盘

图片来源:kafka.apache.org
图片来源:kafka.apache.org

Kafka 中每一个分区是一个有序的,不可变的消息序列,新的消息不断追加到 partition 的末尾,这个就是顺序写。

好久好久之前就有人作过基准测试:《每秒写入2百万(在三台廉价机器上)》http://ifeve.com/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines/

因为磁盘有限,不可能保存全部数据,实际上做为消息系统 Kafka 也不必保存全部数据,须要删除旧的数据。又因为顺序写入的缘由,因此 Kafka 采用各类删除策略删除数据的时候,并不是经过使用“读 - 写”模式去修改文件,而是将 Partition 分为多个 Segment,每一个 Segment 对应一个物理文件,经过删除整个文件的方式去删除 Partition 内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操做。

3. 充分利用 Page Cache

引入 Cache 层的目的是为了提升 Linux 操做系统对磁盘访问的性能。Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,若是在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操做,提升了性能。Cache 层也正是磁盘 IOPS 为何能突破 200 的主要缘由之一。

在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另外一个 Buffer Cache,每个 Page Cache 包含若干 Buffer Cache。Page Cache 主要用来做为文件系统上的文件数据的缓存来用,尤为是针对当进程对文件有 read/write 操做的时候。Buffer Cache 则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。

使用 Page Cache 的好处:

  • I/O Scheduler 会将连续的小块写组装成大块的物理写从而提升性能

  • I/O Scheduler 会尝试将一些写操做从新按顺序排好,从而减小磁盘头的移动时间

  • 充分利用全部空闲内存(非 JVM 内存)。若是使用应用层 Cache(即 JVM 堆内存),会增长 GC 负担

  • 读操做可直接在 Page Cache 内进行。若是消费和生产速度至关,甚至不须要经过物理磁盘(直接经过 Page Cache)交换数据

  • 若是进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用

Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,并不保证数据必定彻底写入磁盘。从这一点看,可能会形成机器宕机时,Page Cache 内的数据未写入磁盘从而形成数据丢失。可是这种丢失只发生在机器断电等形成操做系统不工做的场景,而这种场景彻底能够由 Kafka 层面的 Replication 机制去解决。若是为了保证这种状况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会下降性能。也正因如此,Kafka 虽然提供了 flush.messagesflush.ms 两个参数将 Page Cache 中的数据强制 Flush 到磁盘,可是 Kafka 并不建议使用。

4. 零拷贝技术

Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件经过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的总体吞吐量。

操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的权限。

为了不用户进程直接操做内核,保证内核安全,操做系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。

传统的 Linux 系统中,标准的 I/O 接口(例如read,write)都是基于数据拷贝操做的,即 I/O 操做会致使数据在内核地址空间的缓冲区和用户地址空间的缓冲区之间进行拷贝,因此标准 I/O 也被称做缓存 I/O。这样作的好处是,若是所请求的数据已经存放在内核的高速缓冲存储器中,那么就能够减小实际的 I/O 操做,但坏处就是数据拷贝的过程,会致使 CPU 开销。

咱们把 Kafka 的生产和消费简化成以下两个过程来看

  1. 网络数据持久化到磁盘 (Producer 到 Broker)
  2. 磁盘文件经过网络发送(Broker 到 Consumer)
4.1 网络数据持久化到磁盘 (Producer 到 Broker)

传统模式下,数据从网络传输到文件须要 4 次数据拷贝、4 次上下文切换和两次系统调用。

data = socket.read()// 读取网络数据 
File file = new File() 
file.write(data)// 持久化到磁盘 
file.flush()

这一过程实际上发生了四次数据拷贝:

  1. 首先经过 DMA copy 将网络数据拷贝到内核态 Socket Buffer
  2. 而后应用程序将内核态 Buffer 数据读入用户态(CPU copy)
  3. 接着用户程序将用户态 Buffer 再拷贝到内核态(CPU copy)
  4. 最后经过 DMA copy 将数据拷贝到磁盘文件

DMA(Direct Memory Access):直接存储器访问。DMA 是一种无需 CPU 的参与,让外设和系统内存之间进行双向数据传输的硬件机制。使用 DMA 可使系统 CPU 从实际的 I/O 数据传输过程当中摆脱出来,从而大大提升系统的吞吐率。

同时,还伴随着四次上下文切换,以下图所示

数据落盘一般都是非实时的,kafka 生产者数据持久化也是如此。Kafka 的数据并非实时的写入硬盘,它充分利用了现代操做系统分页存储来利用内存提升 I/O 效率,就是上一节提到的 Page Cache。

对于 kafka 来讲,Producer 生产的数据存到 broker,这个过程读取到 socket buffer 的网络数据,其实能够直接在内核空间完成落盘。并无必要将 socket buffer 的网络数据,读取到应用进程缓冲区;在这里应用进程缓冲区其实就是 broker,broker 收到生产者的数据,就是为了持久化。

在此特殊场景下:接收来自 socket buffer 的网络数据,应用进程不须要中间处理、直接进行持久化时。可使用 mmap 内存文件映射。

Memory Mapped Files:简称 mmap,也有叫 MMFile 的,使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工做原理是直接利用操做系统的 Page 来实现文件到物理内存的直接映射。完成映射以后你对物理内存的操做会被同步到硬盘上。

使用这种方式能够获取很大的 I/O 提高,省去了用户空间到内核空间复制的开销。

mmap 也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并无被真正的写到硬盘,操做系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘。Kafka 提供了一个参数——producer.type 来控制是否是主动flush;若是 Kafka 写入到 mmap 以后就当即 flush 而后再返回 Producer 叫同步(sync);写入 mmap 以后当即返回 Producer 不调用 flush 就叫异步(async),默认是 sync。

零拷贝(Zero-copy)技术指在计算机执行操做时,CPU 不须要先将数据从一个内存区域复制到另外一个内存区域,从而能够减小上下文切换以及 CPU 的拷贝时间。

它的做用是在数据报从网络设备到用户程序空间传递的过程当中,减小数据拷贝次数,减小系统调用,实现 CPU 的零参与,完全消除 CPU 在这方面的负载。

目前零拷贝技术主要有三种类型

  • 直接I/O:数据直接跨过内核,在用户地址空间与I/O设备之间传递,内核只是进行必要的虚拟存储配置等辅助工做;
  • 避免内核和用户空间之间的数据拷贝:当应用程序不须要对数据进行访问时,则能够避免将数据从内核空间拷贝到用户空间
  • mmap
  • sendfile
  • splice && tee
  • sockmap
  • copy on write:写时拷贝技术,数据不须要提早拷贝,而是当须要修改的时候再进行部分拷贝。
4.2 磁盘文件经过网络发送(Broker 到 Consumer)

传统方式实现:先读取磁盘、再用 socket 发送,实际也是进过四次 copy

buffer = File.read 
Socket.send(buffer)

这一过程能够类比上边的生产消息:

  1. 首先经过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝)
  2. 而后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝)
  3. 接着用户程序经过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝)
  4. 最后经过 DMA 拷贝将数据拷贝到 NIC Buffer

Linux 2.4+ 内核经过 sendfile 系统调用,提供了零拷贝。数据经过 DMA 拷贝到内核态 Buffer 后,直接经过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减小数据拷贝外,由于整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,所以大大提升了性能。

Kafka 在这里采用的方案是经过 NIO 的 transferTo/transferFrom 调用操做系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝

5. 批处理

在不少状况下,系统的瓶颈不是 CPU 或磁盘,而是网络IO。

所以,除了操做系统提供的低级批处理以外,Kafka 的客户端和 broker 还会在经过网络发送数据以前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提升了带宽利用率。

6. 数据压缩

Producer 可将数据压缩后发送给 broker,从而减小网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩通常都是和批处理配套使用来做为优化手段的。

小总结 | 下次面试官问我 kafka 为何快,我就这么说

  • partition 并行处理
  • 顺序写磁盘,充分利用磁盘特性
  • 利用了现代操做系统分页存储 Page Cache 来利用内存提升 I/O 效率
  • 采用了零拷贝技术
  • Producer 生产的数据持久化到 broker,采用 mmap 文件映射,实现顺序的快速写入
  • Customer 从 broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,转到 NIO buffer进行网络发送,减小 CPU 消耗