Swap,Cache 与 mmap

众所周知,Milvus 需要把数据全部加载到内存中后才能执行查询。这会对 Milvus 中的查询节点有很高的内存需求,但对于离线场景,用户对查询性能不敏感,可以接受性能降级。这里常规的解决方案是做一个 buffer pool,查询时按需地的将数据加载到内存中。但是这个方案改造力度很大,对于现阶段的 Milvus 来说不合适,这里就不展开了。一个简单而直接的方案是使用 mmap。然而,在最近进行这个改造的时候,我遇到了不少问题,并学到了很多东西。在此记录一下。

Cache 机制

Swap Space

OS 会有一个 swap space,在内存不足的时候,会将部分 page swap out 到磁盘上,也就是 swap space。swap space 的大小是可配置的,一般会根据机器的 RAM 大小去做对应的调整,网上可以找到一些合适的建议值。

既然内存不够用,那为什么不直接简单粗暴的增大 swap space,完全让 OS 去决定换出哪些 page 呢?换出哪些 page 是应用无法控制的,如果换出了处于 critical path 上的 page,会对性能造成很大的影响。

Page Cache

当 OS 访问文件时,它会将读取的 page 的相邻 page 也读入 page cache 中,通常是当前页及前 15 个 page,以及后 16 个 page。Page cache 有类似于 swap 的换进换出机制,但与 swap 不同的是,由于 OS 知道这个 page 来自哪个文件,因此会直接将这样的 page 换出到文件上。如果是脏页,则会执行一次磁盘 I/O 写回,否则只需将 page 从物理内存中简单地移除。

综上,可以看出 swap space 是一种特殊的 page cache,它管理的是匿名的 page(即不与文件关联的 page)。匿名的 page 会被换出到 swap space,而其他 page 则会被换出到其关联的文件。

Mmap

mmap 可以分为两种类型:file-backed map 和 anonymous map。file-backed map 的典型代码通常如下:

void* map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset)

而 anonymous map 通常是这样的:

void* map = mmap(NULL, size, PROT_READ, MAP_SHARED | MAP_ANONYMOUS, -1, 0)

anonymous map 分配出来的内存就是上面所说的 anonymous page。在内存不足的时候,它会被换出到 swap space。然而,这里和 malloc 依然存在差异。malloc 会在 heap 中分配内存,而 anonymous map 只是建立映射,不会有实际的物理内存分配。在实际实现中,malloc 在分配大块内存时会调用 mmap 创建 anonymous map。

数据库系统中的 mmap 应用

文件 IO

考虑读取一个文件,最简单的做法就是先 open,然后 read。这个流程中,从磁盘读取数据到内存中的 buffer,是有一次额外的 copy 的。也就是:

filepage cacheuser bufferfilepage cacheuser buffer\text{file} \to \text{page cache} \to \text{user buffer}file→page cache→user buffer

同样的,写流程是:

user bufferkernel bufferfile\text{user buffer} \to \text{kernel buffer} \to \text{file}

而对于 file-backed mmap,当访问时,是直接访问到 page cache,写的时候换出也是直接从 page cache 到 file,因此可以省掉一次 copy。基于这一点,一些 DB 会用 mmap 来做加速。

Buffer Pool

DB 管理 page 通常会有两种方案,一种是自己实现一个 buffer pool,另一种就是直接对数据文件做 mmap,相当于直接复用 OS 本身的 cache 机制。

为什么我们不再需要 mmap

现在认为使用 mmap 来替代 buffer pool 是一个糟糕的方案。可以参见这篇论文。同样的,根据这篇论文,目前使用 mmap 来加速文件 IO 也是一个糟糕的方案。

对于文件 IO,现代操作系统提供了许多异步 IO 机制,例如 io_uring。而 mmap 在 page fault 时是同步阻塞的,性能会比异步 IO 差很多。同时,在读文件时也可以提供一些 hint 来 bypass page cache,从而实现和 mmap 相当的提升。

对于 buffer pool,由于 mmap 的 page cache 完全由操作系统控制,虽然可以通过 madvise 来调整操作系统对 map 的行为,但很容易用错。使用 mmap 可能导致 DB 在错误的时机将 page 落盘,从而在事务处理中破坏系统的数据一致性。

为什么 Milvus 还是能用 mmap

其实最理想的情况还是自己实现 buffer pool,根据系统自身的 access pattern 去设计 buffer pool 可以获得更好的性能。但 Milvus 目前的情况用 mmap 也不会有太大问题,因为 Milvus 的数据都是 immutable 的,我们只需要 read-only map。