Milvus 如何實現數據動態更新與查詢

在這篇文章,我們會主要描述 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。數據這時候開始寫入內存。內存寫入大致分為下面幾個步驟:

Milvus 如何實現數據動態更新與查詢


  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 裡,從數據被記錄在內存,到數據能被搜到,你最快只需要等待一秒。這整個過程可以大概由下面這張圖來概括:


Milvus 如何實現數據動態更新與查詢


首先,插入的數據會進入一個內存中的 insert buffer。這些 buffer 會由開始的 Mutable 狀態週期性的轉為 Immutable 狀態,以準備序列化。然後,這些 Immutable buffer 會週期性的被後臺序列化線程序列化到磁盤。數據落盤後,落盤信息會被記錄在元數據裡。至此,數據就能被搜到了!


現在,我們會具體描述圖中的步驟。


數據插入 Mutable buffer 的過程我們都已經知道了,接下來,就是從 Mutable buffer 轉為 Immutable buffer 的過程:


Milvus 如何實現數據動態更新與查詢


Immutable queue 這個隊列會向後臺序列化線程提供 immutable 狀態的,已經準備好被序列化的 MemTableFile。每個 MemTable 管理著自己的 immutable queue,當 MemTable 唯一 mutable 的 MemTableFile 大小達到閾值,就會進入 immutable queue。一個負責 ToImmutable 的後臺線程會週期性的拉取所有 MemTable 管理的 immutable queue 中的 MemTableFile,並將他們輸送到總的 Immutable queue。需要注意的是,數據寫入內存和將內存中的數據變為不可被寫的狀態這兩個操作不能同時發生,需要共用一把鎖。但是,ToImmutable 這個操作因為過程很簡單,幾乎不會造成任何延遲,所以對插入數據的性能影響微乎其微。


接下來就是將 serialization queue 中的 MemTableFile 序列化到磁盤了。這主要分為三步:


Milvus 如何實現數據動態更新與查詢

首先,後臺序列化線程會週期性的從 immutable queue 中拉取 MemTableFile。然後,他們被序列化成固定大小的原始文件(Raw TableFiles)。最後,我們會將這個信息記錄在元數據中。當我們進行向量搜索時,我們會在元數據中查詢對應的 TableFile。至此為止,這些數據就能被搜索到了!


此外,根據設置的 index_file_size,後臺序列化線程在完成一次序列化週期後,會將一些固定大小的 TableFile 合併成一個 TableFile,並且同樣在元數據中記錄這些信息。這時候,這個 TableFile 就可以被構建索引了。構建索引同樣也是異步的,另外一個負責構建索引的後臺線程會週期性的讀取元數據中 ToIndex 狀態的 TableFile,進行對應的索引構建。


| 向量搜索

實際上,你會發現,通過 TableFile 和元數據的幫助,向量的搜索變得更加直觀和方便。大體上說,我們需要從元數據中獲取與被查詢 Collection 對應的 TableFiles,在每個 TableFile 進行搜索,最後進行歸併。在這篇文章裡,我們不深入探討搜索的具體實現。如果你想要了解更多,歡迎閱讀我們的源碼,或者閱讀 Milvus 系列的其他文章!(Milvus 官網:milvus.io)


分享到:


相關文章: