Kafka 支持消息的持久化,并在持久化的同时,保证了消息系统的高效运作,它是怎么做到的呢?让我们来看一看吧!

文件系统就一定慢吗?

Kafka 重度依赖文件系统来储存和缓存消息。人们对于文件系统的印象总是“它很慢”,因此似乎没有人相信世界上还有“快”的持久化。实际上,磁盘既可以很慢,也可以很快,完全取决于你如何使用它。如果设计得足够好,磁盘读写并不会比网络传输慢。

以前,磁盘性能很大程度上是被“寻道时延”给拖累了。在一个测试中,写入同样的数据量,顺序写入速度可以达到 600 MB 每秒,而随机写入速度只有 100 KB 每秒,只能达到前者的几千分之一。这是因为顺序访问是很好预测的,操作系统为它们做了很多优化。比如,现代操作系统都会有 预读read-ahead预写write-ahead 机制。预读就是提前读取一大块数据(局部性原理),预写就是把许多小的逻辑写入汇集成一组,然后一次性写入磁盘。

在 ACM 的一项研究中,他们发现 在某些情况下,顺序磁盘访问甚至比随机内存访问还要快

为了补偿“寻道时延”带来的性能损失,现代操作系统都会倾向于大量使用内存来缓存磁盘数据,甚至可以用上所有的空闲内存。这么的代价很小,只不过是在内存替换时损失一点点的可以忽略不计的性能而已。并且,这个特性也不是随便就可以禁用的,除非使用直接 I/O。就算是一个进程只在自己进程空间内缓存数据,操作系统也会把同样的数据在内存中复制一份,也就是说,所有数据都会在系统中储存两份。

另外,Kafka 运行在 JVM 上。如果你了解 Java 内存占用的话,你一定知道下面这两个事实:

  1. 对象的内存开销非常大,通常是其储存数据的两倍大小,甚至更糟。
  2. 随着堆中数据的增加,Java 的垃圾回收会越来越频繁、越来越慢。

那么,考虑到上述因素,使用文件系统和内核的 页缓存pagecache 才是更好的,至少比内存缓存或者其他结构要好。首先,通过利用所有可用内存,Kafka 几乎让可用的缓存大小翻了倍。其次,通过储存一个紧凑的结构,而不是一个个独立对象,可用的缓存大小几乎又翻了倍。在没有达到垃圾回收阈值的情况下,对于一个 32 GB 内存大小的机器,Kafka 可以利用其中 28-30 GB 的内存。更好的是,这个缓存不会随着服务的重启而消失。反观使用进程中缓存,每次启动的时候都需要在内存中重建,对于 10 GB 大小数据,这个过程可能要花上 10 分钟。还有一个好消息,还记得之前我们提到过的“操作系统会预读数据”吗?这就意味着,每次顺序读取,操作系统都会将一大块有用的数据填充到缓存中。

总结以上事实,Kafka 得出了一个最好的、同时也是最简单的方案:与其自己在内存中维护一个尽可能大的缓存,让操作系统决定什么时候写入磁盘(通常是内存写满了);还不如反其道而行之,每产生一条数据,就把它“逻辑写入”到磁盘上的持久化日志中,不一定真的写入磁盘,也就是说,所有的数据都被传输到了内核的页缓存中。

这是一种“以页缓存为中心”的设计,在 Varnish 写的 这篇文章 中有所介绍。

常数时间足矣!

许多消息系统都是这么设计的:每个消费者都有一个对应的 B 树或是其他的通用随机访问结构,以此来维护消息的元数据。B 树可以说是最多才多艺的数据结构了,有了它,消息系统可以广泛支持事务/非事务的消息语义。不过,它的代价也是相当大的。通常,我们会认为 B 树的操作都是 O(N) 的时间复杂度,但是对于磁盘操作来说就不是这样了。磁盘的每次寻道操作都要花费 10 毫秒时间,并且同一时间,只支持一个寻道操作,并行寻道是不可能的。因此,寥寥几个寻道操作就会导致很大的时间开销。对于存储系统来说,它会混合使用内存操作和磁盘操作(虚拟内存技术),这就导致操作的耗时并不是线性增加的。比如,当数据量增大一倍,缓存大小不变时,操作耗时会增加远不止一倍。

我们应该很容易就会想到日志系统,它只涉及到读取和追加内容到文件末尾这两个操作而已。它的优势就是,所有操作的都是常数时间,并且读与写之间不会冲突,可以并行。这能够带来明显的性能提升,因为操作耗时完全和数据量解耦了。这样一来,硬件要求也可以降低。寻道性能差?没有关系!我们可以花同样的钱去买大容量的低转速 SATA 盘,最终得到差不多的性能。

由于使用文件作为消息存储载体,我们得到了几乎无限的(虚拟)空间,这就意味着我们可以提供大多数消息系统都提供不了的功能。比如,在 Kafka 中,消息并不会在消费之后立即被删除,它们会被保持相对长的时间(比如一个礼拜)。这就给消费者带来了很多灵活性!

后语

以上是 Kafka 在设计持久化机制时的考虑。总结一下,Kafka 在实现持久化时主要考虑了两个因素:

  1. 消息存储在哪里?文件系统!
  • 不主动地在内存中管理缓存,而是被动地让操作系统来做这件事。
  • 利用内核的页缓存pagecache,充分利用机器的物理内存,避免进程启动时的缓存重建
  1. 消息以什么方式存储?追加文件!
  • 扬长避短,从消息系统中得到灵感,只执行顺序写入(追加到文件末尾),使该操作只需要常数时间
  • 无限的(虚拟)空间,可以保留已经消费过的消息

在下一篇文章中,我将介绍 Kafka 在效率方面做了优化,保持关注噢!

参考


本文使用 CC BY-SA 4.0 国际协议 进行许可,欢迎 遵照协议规定 转载。
作者:六开箱
链接:https://lkxed.github.io/posts/kafka-design-persistence/