准备工作
在本文中,我们将主要介绍如何在 Milvus 内存中记录向量数据,以及如何维护这些记录。
以下是我们的主要设计目标:
- 数据导入的效率要高。
- 数据导入后能尽快看到数据。
- 避免数据文件碎片化。
因此,我们建立了一个内存缓冲区(插入缓冲区)来插入数据,以减少磁盘和操作系统随机 IO 的上下文切换次数,提高数据插入的性能。基于 MemTable 和 MemTableFile 的内存存储架构能让我们更方便地管理和序列化数据。缓冲区的状态分为可变和不可变两种,这样就可以将数据持久化到磁盘上,同时保持外部服务可用。
准备工作
当用户准备向 Milvus 插入向量时,首先需要创建一个 Collection(* Milvus 在 0.7.0 版本中将 Table 更名为 Collection)。Collection 是在 Milvus 中记录和搜索向量的最基本单位。
每个 Collection 都有一个唯一的名称和一些可以设置的属性,向量会根据 Collection 名称进行插入或搜索。创建新的 Collection 时,Milvus 会在元数据中记录此 Collection 的信息。
数据插入
当用户发送插入数据的请求时,数据经过序列化和反序列化后到达 Milvus 服务器。现在,数据被写入内存。内存写入大致分为以下几个步骤:
2-data-insertion-milvus.png
- 在 MemManager 中,找到或创建与 Collections 名称相对应的新 MemTable。每个 MemTable 对应内存中的一个 Collection 缓冲区。
- 一个 MemTable 将包含一个或多个 MemTableFile。每当我们创建一个新的 MemTableFile 时,都会同时在 Meta 中记录这些信息。我们将 MemTableFile 分成两种状态:可变和不可变。当 MemTableFile 的大小达到阈值时,它将变为不可变。每个 MemTable 在任何时候都只能写入一个 Mutable MemTableFile。
- 每个 MemTableFile 的数据最终都将以设定索引类型的格式记录在内存中。MemTableFile 是管理内存数据的最基本单位。
- 在任何时候,插入数据的内存使用量都不会超过预设值(insert_buffer_size)。这是因为每次插入数据的请求到来时,MemManager 都能轻松计算出每个 MemTable 所包含的 MemTableFile 占用的内存,然后根据当前内存协调插入请求。
通过 MemManager、MemTable 和 MemTableFile 的多级架构,数据插入可以得到更好的管理和维护。当然,它们能做的远不止这些。
近乎实时的查询
在 Milvus 中,插入的数据从内存移动到磁盘最长只需等待一秒钟。整个过程大致可以用下图来概括:
2-near-real-time-query-milvus.png
首先,插入的数据将进入内存中的插入缓冲区。缓冲区会周期性地从初始可变状态变为不可变状态,为序列化做准备。然后,这些不可变缓冲区将由后台序列化线程定期序列化到磁盘。数据放置后,订单信息将记录在元数据中。此时,就可以搜索数据了!
现在,我们将详细描述图片中的步骤。
我们已经知道了将数据插入可变缓冲区的过程。下一步就是从可变缓冲区切换到不可变缓冲区:
3-mutable-buffer-imutable-buffer-milvus.png
不可变队列将为后台序列化线程提供不可变状态和准备序列化的 MemTableFile。每个 MemTable 都管理自己的不可变队列,当 MemTable 的唯一可变 MemTableFile 的大小达到阈值时,它将进入不可变队列。负责 ToImmutable 的后台线程会定期提取 MemTable 管理的不可变队列中的所有 MemTableFile,并将它们发送到总的不可变队列中。需要注意的是,向内存中写入数据和将内存中的数据变为不能写入的状态这两个操作不能同时进行,需要一个公共锁。不过,ToImmutable 的操作非常简单,几乎不会造成任何延迟,因此对插入数据的性能影响很小。
下一步是将序列化队列中的 MemTableFile 序列化到磁盘。这主要分为三个步骤:
4-serialize-memtablefile-milvus.png
首先,后台序列化线程会定期从不可变队列中提取 MemTableFile。然后,它们会被序列化为固定大小的原始文件(Raw TableFiles)。最后,我们将在元数据中记录这些信息。当我们进行向量搜索时,将在元数据中查询相应的 TableFile。从这里可以搜索这些数据!
此外,根据 index_file_size 设置,序列化线程在完成一个序列化周期后,会将一些固定大小的 TableFile 合并为一个 TableFile,并将这些信息记录在元数据中。此时,就可以为 TableFile 编制索引了。索引的建立也是异步的。另一个负责建立索引的后台线程会定期读取元数据 ToIndex 状态下的 TableFile,以执行相应的索引建立工作。
向量搜索
事实上,你会发现在 TableFile 和元数据的帮助下,向量搜索变得更加直观和方便。一般来说,我们需要从元数据中获取与查询的 Collections 相对应的 TableFile,在每个 TableFile 中进行搜索,最后进行合并。本文不深入讨论搜索的具体实现。
如果你想了解更多,欢迎阅读我们的源代码,或阅读我们关于 Milvus 的其他技术文章!
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word