在这篇文章,我们会主要描述 Milvus 里向量数据是如何被记录在内存中,以及这些记录以怎样的形式维护。

我们的设计目标主要有下面三点:

  1. 数据导入效率要高
  2. 数据导入后尽快可见
  3. 避免数据文件碎片化

因此,我们建立了插入数据的内存缓冲区(insert buffer),以减少磁盘随机 IO 和操作系统中上下文切换的次数,从而提升数据插入的性能。基于 MemTable 和 MemTableFile 的内存存储架构,能使我们更加方便的管理和序列化数据。将 buffer 的状态分为 Mutable 和 Immutable,能让数据持久化到磁盘的同时保持对外服务可用。

|准备

当用户准备插入向量到 Milvus 时,首先需要创建一个 Collection(*Milvus 在0.7.0版本中将 Table 更名为 Collection)。Collection 是 Milvus 记录和搜索向量的最基本单位。每个 Collection 有一个独特的名字和一些可以被设置的属性,并且根据 Collection 的名字进行向量的插入或搜索。创建一个新的 Collection 时,Milvus 会在元数据里记录下这个 Collection 的信息。

|数据的插入

当用户发出插入数据的请求时,数据经过序列化和反序列化,到达 Milvus server。数据这时候开始写入内存。内存写入大致分为下面几个步骤:

  1. 在 MemManager 中,找到或新创建与Collection 名字对应的 MemTable。每个 MemTable对应一个 Collection 在内存中的 buffer。
  2. 一个 MemTable 会包含一个或多个 MemTableFile。每当我们创建一个新的 MemTableFile,我们会同时在 Meta 中记录这个信息。我们将 MemTableFile 分为两种状态:Mutable 和 Immutable。当 MemTableFile 大小达到阈值,会变成 Immutable 状态。每个 Memtable 在任意时间只会存在一个 Mutable MemTableFile 可被写入。
  3. 每个 MemTableFile 的数据会最终以被设置的 index 类型的格式记录在内存里。MemTableFile 是在内存中管理数据的最基本单位。
  4. 任意时刻,插入数据的内存的占用量都不会超过预先设置的值(insert_buffer_size)。这是因为每一个插入数据的请求进来,MemManager 都可以很方便的计算到每个 MemTable 下包含的 MemTableFile 所占内存,然后根据当前内存协调插入请求。

通过 MemManager, MemTable 和 MemTableFile 多层级的架构,数据的插入可以更好地被管理和维护。当然,它们能做的远不止这些。

| 近实时查询

在 Milvus 里,从数据被记录在内存,到数据能被搜到,你最快只需要等待一秒。这整个过程可以大概由下面这张图来概括:

首先,插入的数据会进入一个内存中的 insert buffer。这些 buffer 会由开始的 Mutable 状态周期性的转为 Immutable 状态,以准备序列化。然后,这些 Immutable buffer 会周期性的被后台序列化线程序列化到磁盘。数据落盘后,落盘信息会被记录在元数据里。至此,数据就能被搜到了!

现在,我们会具体描述图中的步骤。

数据插入 Mutable buffer 的过程我们都已经知道了,接下来,就是从 Mutable buffer 转为 Immutable buffer 的过程:

Immutable queue 这个队列会向后台序列化线程提供 immutable 状态的,已经准备好被序列化的 MemTableFile。每个 MemTable 管理着自己的 immutable queue,当 MemTable 唯一 mutable 的 MemTableFile 大小达到阈值,就会进入 immutable queue。一个负责 ToImmutable 的后台线程会周期性的拉取所有 MemTable 管理的 immutable queue 中的 MemTableFile,并将他们输送到总的 Immutable queue。需要注意的是,数据写入内存和将内存中的数据变为不可被写的状态这两个操作不能同时发生,需要共用一把锁。但是,ToImmutable 这个操作因为过程很简单,几乎不会造成任何延迟,所以对插入数据的性能影响微乎其微。

接下来就是将 serialization queue 中的 MemTableFile 序列化到磁盘了。这主要分为三步:

首先,后台序列化线程会周期性的从 immutable queue 中拉取 MemTableFile。然后,他们被序列化成固定大小的原始文件(Raw TableFiles)。最后,我们会将这个信息记录在元数据中。当我们进行向量搜索时,我们会在元数据中查询对应的 TableFile。至此为止,这些数据就能被搜索到了!

此外,根据设置的 index_file_size,后台序列化线程在完成一次序列化周期后,会将一些固定大小的 TableFile 合并成一个 TableFile,并且同样在元数据中记录这些信息。这时候,这个 TableFile 就可以被构建索引了。构建索引同样也是异步的,另外一个负责构建索引的后台线程会周期性的读取元数据中 ToIndex 状态的 TableFile,进行对应的索引构建。

| 向量搜索

实际上,你会发现,通过 TableFile 和元数据的帮助,向量的搜索变得更加直观和方便。大体上说,我们需要从元数据中获取与被查询 Collection 对应的 TableFiles,在每个 TableFile 进行搜索,最后进行归并。在这篇文章里,我们不深入探讨搜索的具体实现。如果你想要了解更多,欢迎阅读我们的源码,或者阅读 Milvus 系列的其他文章!

更多Milvus 原理与最佳实践系列文章

Milvus 分布式向量检索-Mishards架构介绍

Milvus 查询任务调度原理

Milvus 最佳实践之如何设置API参数 (3)

Milvus 最佳实践之如何设置系统配置项 (2)

Milvus 最佳实践之如何选择索引类型

欢迎加入 Milvus 社区

http://github.com/milvus-io/milvus| 源码

http://milvus.io| 官网

http://milvusio.slack.com| Slack 社区

http://zilliz.blog.csdn.net| CSDN 博客


Zilliz
154 声望829 粉丝

Vector database for Enterprise-grade AI