这张图片由作者创作。
开始原文发表于https://vutr.substack.com ._
如上一篇文章所承诺的,这周我们将继续了解Apache Kafka。我将介绍我对Kafka一些重要设计(如文件系统、零拷贝技术、批处理技术)的研究。
Kafka 使用文件系统功能在继续之前,我们可以先了解一下操作系统(OS)页面缓存的概念。
这张图是作者自己画的。
现代的操作系统通常会借用未使用的内存(RAM)来作为页面缓存。经常访问的磁盘数据会被加载到缓存中,从而避免频繁直接读取磁盘。因此,系统运行速度更快,缩短了磁盘寻道延迟。如果某个程序需要内存,内核会回收用于页面缓存的内存。这样可以确保页面缓存不会影响系统的性能。
Kafka 使用操作系统的文件系统来存储数据,也因此利用了内核的页面缓存。操作系统并不会试图在内存中保留尽可能多的数据,而是在内存不足时将其刷新到磁盘之前,先将其全部加载到页面缓存中,再将其刷新到磁盘。
因此,这种方法有助于Kafka简化代码库,因为操作系统负责处理页面缓存逻辑。此外,这种方法也对Kafka有利,因为Java虚拟机存在一些痛点。
- 存储对象所导致的高内存消耗。
- 当堆内对象数量增加时,垃圾收集器的运行速度会变慢。
通过利用操作系统文件系统而不是将消息缓存在内存中的Java对象,Kafka可以解决上述两个问题。
要顺序访问的模式得知 Kafka 使用文件系统进行数据存储和缓存后,你可能会想,“由于磁盘操作总是比内存慢,这会不会影响 Kafka 的性能表现?”
这里的关键在于访问模式。毫无疑问的是, 在随机访问的情况下,磁盘速度会比RAM慢,而在顺序访问的情况下,磁盘则可能略胜一筹。我们来仔细看看这些模式:
- 随机存取是一种检索或存储数据的方法,其中数据可以按任何顺序随机访问。
- 循序存取是一种检索或存储数据的方法,其中数据按顺序存取。
Kafka 是这样设计的,使写入(生产者写数据)和读取(消费者读数据)按顺序进行。让我们来看看它们是怎么做到这一点的。
来写作者的这张图。
在 Kafka 中,消息是通过主题进行分组的。每个主题会被拆分成多个分区。每个主题的分区对应一个逻辑日志流。物理上,一个日志是由一组大小相近的段文件组成(例如,每个段文件大约为 1GB)。每当生产者向分区发布消息时,代理会将消息追加到最后一个段文件。任何时候只有一个段文件处于活动状态并接受数据写入;一旦段文件达到大小限制就会被关闭,并且 Kafka 会为后续写入打开一个新的段文件。
在Appending at the end of段文件末尾的追加操作,确保了Kafka中的数据写入是顺序的。
《读》这张图是作者自己绘制的。
消费者总是按顺序从特定分区中消费消息。Kafka中存储的消息没有消息ID,每条消息有一个逻辑偏移量。这避免了维护额外索引结构的开销,这些索引结构将消息ID映射到消息的实际位置。Kafka消息的偏移量递增但不一定是连续的;因此,要找到下一个消息的偏移量,需要进行这样的计算:为了获取下一条消息的偏移量,需要把当前偏移量加上当前消息的长度,就像数组数据结构处理随机访问那样。
除了包含实际数据的日志文件外,代理服务器还拥有另外两个索引文件,这些文件有助于更快地定位所需的分段文件。第一个索引将偏移量对应到分段文件及其内部位置,使代理能够快速找到给定偏移量的消息。后者将时间戳对应到消息偏移量;此索引在通过时间戳查找消息时使用。Kafka使用内存映射文件技术来读取这些索引文件,这使得Kafka可以将索引文件读取得就像它们直接位于内存中一样。
该索引将偏移量映射到消息在段日志中的位置。Travis Jeffery(2016 年)撰写的《Kafka 存储内部工作原理》。原文链接
在开始拉取消息时,消费者首先向代理请求开始消费的起始偏移量。然后,代理通过索引文件定位包含请求消息的分段文件,并将数据发送给消费者。消费者接收到消息时,计算下一个消息的偏移量,并在下一次请求中使用它。
零复制使用文件系统也有助于Kafka在幕后实现零拷贝优化。需要注意的是,零拷贝操作并不意味着完全没有数据复制。它只是确保不会进行不必要的复制。这项优化并非专为Kafka首次发明,它只是利用了操作系统现有的这项技术。
我们先来看看原始数据传输的流程,然后再来看看零拷贝技术是怎样的工作方式。
数据传输流程作者自制的图片。
在从磁盘读取文件并通过网络传输的典型流程中,数据会被复制四次,同时用户模式和内核模式之间会发生四次上下文切换。流程如下所示:
- 它从磁盘读取文件内容并存储在操作系统页面缓存(读缓冲区)中。此步骤需要从用户模式切换到内核模式。
- 数据从读缓冲区复制到应用程序缓冲区,同样这也需要模式切换,从用户模式转到内核模式。
- 数据接着被复制到套接字缓冲区。同样,这需要从用户模式切换到内核模式。
- 在将数据发送到套接字缓冲区之后,返回用户模式进行上下文切换。然后将数据从套接字缓冲区复制到网络接口控制器 (NIC)中。
- NIC将数据发送到目标位置。
零拷贝流为了更清楚地说明:
用户模式和内核模式的模式切换:在现代操作系统中,软件运行在用户模式和内核模式。用户模式限制了对系统资源的访问,而内核模式则允许完全访问。当用户应用程序需要内核级别的访问权限,比如访问硬件设备时,它会通过系统调用请求操作系统从用户模式切换到内核模式。这种切换称为模式切换,包括保存当前处理器状态、改变模式和加载新的状态。
网络接口控制器(NIC)管理计算机与网络之间的接口,将数据转换成用于网络传输的信号,并接收传入数据供计算机处理。
套接字缓冲区是一种内存空间,用于内核临时存储网络套接字的传入和传出数据包,管理应用程序与网络之间的数据流动。
作者绘制的图片。
借助零拷贝优化,数据可以直接从页面缓存复制到套接字缓冲区。在Unix系统下,这种方法通过一个sendfile()系统调用来实现。它可以直接从一个file descriptor复制数据到另一个,而不需要在使用read()和write()系统调用时在用户空间进行数据传输。因此,这种优化可以帮助Kafka绕过原始数据传输过程中的第二和第三步。当Kafka利用零拷贝技术时,流程可以概括如下:
- 数据从磁盘复制到页缓存。
- 然后,数据通过 sendfile() 调用直接从页缓存复制到网络接口。
- 网卡将数据发送到目标。
因此,上下文切换次数从四次减少到两次,并且数据复制不再需要复制到Kafka应用中。此外,在第一步中,数据只会被一次性复制到页面缓存里,在需要时复用,而不是每次读取时不再在内存中移动并在用户空间中复制。
重要的是,从生产者发送到磁盘,直到代理将数据转发给消费者,Kafka的数据格式在这一过程中始终保持一致。使用相同的格式让Kafka能高效利用零拷贝技术,并避免解压和再压缩消息。
批量处理由于后台文件系统使用,客户端向代理发送过多的小请求可能会影响Kafka的性能。为解决这一问题,Kafka协议提供了一种消息集抽象,有助于将消息进行分组。这有助于减少网络往返中发送大量单条消息请求的开销。
除了网络性能的好处之外,批处理还帮助代理更高效地写入消息到磁盘;代理不是逐条追加消息,而是批量追加消息到磁盘。这使得Kafka能够进行更大的连续磁盘读写操作。
此外,更重要的是,如果网络带宽成为瓶颈时,Kafka支持通过高效的批量格式压缩消息批次。消息批次可以被分组、压缩并发送给broker。
结尾我们在本文中刚刚讨论了一些 Kafka 的设计决策。首先,Kafka 通过操作系统文件系统进行读写操作。其次,由于使用文件系统并保持物理数据格式的一致性,Kafka 可以利用零拷贝技术,使数据传输更加高效。最后,我们看到批量处理消息如何帮助 Kafka 提升性能。下周我们将继续深入了解 Kafka 的生产者部分。
那麽,下周再見 :)
参考文献[1] Kafka 官方页面
[2] 格温·夏皮拉,托德·帕利诺,拉吉尼·希瓦拉,克里特·佩蒂,Kafka 权威指南:实时数据与大规模流处理 (2021)
_[3] 维基百科-内存映射文件(https://en.wikipedia.org/wiki/Memory-mapped_file)_
_维基百科 — 页面缓存 页面缓存_
安德烈·扎博洛茨基的《Kafka是如何在写入磁盘时保持高性能的?》(2021。)
_【7】Stanislav Kozlovski,_零拷贝入门(https://2minutestreaming.beehiiv.com/p/apache-kafka-zero-copy-operating-system-optimization) 2023
[8] Travis Jeffery, [Kafka 的存储内部工作机制](https://medium.com/the-hoard/how-kafkas-storage-internals-work-3a29b02e026) ((2016 年))
我的通讯稿是一封每周的博客风格电子邮件,在这封邮件中,我记录从比我更聪明的人那里学到的东西。
所以,如果你想和我一起学习和成长,请在这里订阅吧:https://vutr.substack.com!