如何使用 Milvus 向量數據庫實現實時查詢

編者按:本文詳細介紹 Milvus 2.0 如何對查詢節點的數據進行管理,以及如何提供查詢能力。

  • 快速回顧 Milvus 進行數據插入與持久化相關的流程與機制

    • Milvus 架構快速回顧

    • 數據插入流程

    • 數據組織機制

  • 如何將數據加載進查詢節點 query node

    • 數據加載流程詳解

    • 數據管理與維護

  • Milvus 上實現實時查詢的相關操作和流程



快速回顧 Milvus 進行數據插入與持久化相關的流程與機制

Milvus 架構快速回顧

如下圖所示,Milvus 向量數據庫的整體架構可以分爲 coordinator service、worker node、 message  storage 和 object storage 這幾大部分。

Coordinator services 承擔的主要工作是協調各個 worker node 的工作,其中的各個模塊與 worker node 是一一對應的關係,並協調管理各個 node 之間的工作。如架構圖中所示,query coordinator 對應並協調 query node,data coordinator 對應並協調 data node,index coordinator 對應並協調 index node。Data node 負責數據的持久化存儲,基本上是一個 I/O 密集型的工作負載,負責把數據從 log broker 中寫到最終的 object storage 當中。而 index node 負責實現向量索引的構建,最後由 query node 來承擔整個 Milvus 的查詢工作,這兩類 node 是數據計算密集型的節點, 除此之外,系統架構中還有兩個比較重要的部分:message storage 和 object storage。Message storage 相當於一個 WAL 的東西,當數據插入到這個地方之後,系統會保障數據不會有丟失。其中的 log broker 會默認將數據存放 7 天,這期間即使下面的 worker node 出現了部分宕機的情況,系統也可以從 log broker 中恢復一些數據及狀態。Object storage 負責實現數據持久化存儲,log broker 裏面的數據最終都會持久化到 object storage 裏面,以進行數據的長期保存。

總體來說這個架構相當於一個存儲與計算分離的一個系統, data  這邊負責數據存儲,然後 query 這邊負責查詢計算。

數據插入流程

第一步:Insert message 從 SDK 發到 proxy 之後,proxy 把這個 insert  message 插到相應的 log broker 中,插入到 log broker 中的每條消息都有唯一的主鍵和一個時間戳;

第二步:插入到 log broker 之後,數據會被 data node 消費;

第三步:Data node 會把數據寫入進持久化存儲當中,最終數據在持久化存儲中是基於 segment 的粒度來組織的,也就是說這個消息除了中主鍵和時間戳,還會被額外賦予一個  segment ID,以標識出這條數據最終會屬於哪個 segment。Data note 在收到這些信息之後,會把相應的信息寫入相應的 segment 中,並最終寫入到持久化存儲中去。

第四、五步:在數據被持久化之後,如果說基於這些數據直接做查詢的話,查詢速度會比較慢,因此一般情況下會考慮去構建一些索引去以加速查詢速度。這時 index node 就會把信息從持久化存儲里拉出來並構建索引,而構建的索引文件又會被回寫進持久化存儲中(S3 或 Minio 等等)。有時我們會需要構建多個索引,以從中選挑選出其中查詢速度最快的一個,這樣的操作也可以在 index node 中實現。

Log broker 和 object storage 也是 Milvus 架構中保障數據可靠性很重要的兩部分,在系統設計中這兩部分也可以分別選擇一些第三方組件,來保障不同情況下的可靠性。

一種常見的情況,是在查詢的同時也進行數據插入,這時一部分數據處在 log broker 中,而一部分數據處於 object storage 裏面。我們把這兩部數據分別做了定義,在 object storage 裏面的數據爲 批數據,而在 log broker 裏面的是流數據。顯而易見,在做實時查詢的場景下,如果想遍歷所有已經插入的數據,則必須要在流數據和批數據裏同時做查詢,才能返回正確的實時查詢數據。

數據組織機制

接下來看一下數據存儲的相關機制,數據分兩部分存儲。一部分是在 object storage;一部分是在 log broker。

首先看一下在 log broker 裏面,數據的組織形式是怎樣的呢?

可以看參考下圖,數據可分成這幾部分:唯一的 collection ID、唯一的 partiton ID、唯一 的 segment ID。

每個 collection 在系統裏面都會分配指定數量的 channel,可以理解成是類似 Kafka 中的 topic, 或類似傳統數據庫裏面的 shard 的概念。

在圖示中,假如我們對 collection 分配了三個 channel,假設我們要插入 100 條數據,那麼這 100 條數據會平均的分到這三個 channel 中,然後在三個 channel 裏面,數據又是以 segment 爲粒度進行拆分。目前每個 segment 的容量是有上限的,系統默認最大到 512M。在持續的數據插入過程中,會優先持續往一個 segment 中寫入,但如果容量超過 512M,系統會新分配一個 segment ID 繼續數據插入。所以在真實的場景中,每個 channel 裏面都會包含很多個 segment。總結來說,數據在 log broker 中,可以拆分成,collection、partition 和 segment,最終我們存儲在系統裏面,實際上是很多個小的 segment。

接下來,我們再看一下在 object storage 中的數據組織方式。與 log broker 一樣,data node 在收到 insert message 之後,也是按照 segment 進行組織的。當一個 segment 達到 512M 的默認上限時,或者用戶直接強制停止對這個 segment 插入數據,這時 segment 會被持久化存儲進 object storage當中。在持久化存儲中,每個 segment 中的存儲格式是一個一個更小的 log snapshot ,而且是分成多列的。具體的這個列數是和待插入的 collection 的 schema 有關。如果 collection 的 schema 有 4 列,數據插入 segment 中也會有 4 列。所以,最終在 object storage 中,數據存儲的形式是很多個 log snapshot。

如何將數據加載進查詢節點 query node

數據加載流程詳解

在明確了數據的組織方式後,接下來我們看看數據進行查詢加載的具體流程。

在 query node 中,把 log broker 中的流數據稱爲 streaming,把 object storage 中的批數據稱爲 historical。流數據和批量數據的加載流程如下:


首先,query coord 會詢問 data coord。Data coord 因爲一直在負責持續的插入數據,它可以反饋給 query coord 兩種信息:一種是已經持久化存儲了哪些 segment,另一種是這些已經持久化的 segment 所對應 checkpoint 信息,根據 checkpoint 可以知道從 log broker 中獲得這些 segment 所消費到的最後位置。

接着,在收到這兩部分信息後,query coord 會輸出一定的分配策略。這些策略也分成兩部分:按照 segment 進行分配(如圖示 segment allocator),或按照 channel 進行分配(如圖示 channel allocator)。

Segment allocator 會把持久化存儲- 也就是批數據- 中的不同的 segment 分配給不同的 query node 進行處理,如圖將 S1、S3 分配給 query node 1,將S2、S4分配給 query node 2。Channel allocator 會把 log broker 中不同的 channel 分配給不通的 query node 進行監聽,如圖 query node 1 監聽 Ch 1, query node 2 監聽 Ch 2。

這些分配策略下發到各個 query node 之後,query node 就會按照策略進行相應的 load 和 watch 操作。如圖示 query node 1 中,historical (批數據)部分會將分配給它的 S1、S3 數據從持久化存儲中加載進來,而 streaming 部分會訂閱 log broker 中的 Ch1,將這部分流數據接入。

因爲 Ch1 可以持續不斷的插入數據(流數據), 而由這部分接入 query node 中的數據我們定義爲 growing segment,因爲會持續不斷的增長,是增量數據,如圖示的 G5。相對應的,histroical 中的 segment 定義 sealed segment,是靜態的存量數據。

數據管理與維護

對於 sealed segment 的的管理,系統的設計主要考慮負載均衡和宕機的情況。

如圖示,假如 query node 4  上面有很多這個 sealed segment ,但是其他節點比較少,在這種情況下 query node 4 的查詢可能是整個查詢裏面的一個瓶頸。所以這時,系統就要考慮說把這些 sealed segment 負載均衡到到其他節點上去。

另一種情況,如果某一個節點突然掛掉了,這個時候它上面的負載也能夠快速的遷移到其他正常節點上,以保證查詢到的結果是正確的。

對增量數據來講,剛纔提到說 query node 監聽相應的 dmchannel 之後,這些增量數據都就會進入到 query node 裏。但具體是怎麼進入的呢?這裏我們用到了一個 flow graph 模型,一種狀態驅動的模型,整個 flow graph 包括 input node, filter node, insert node 和 service time 四部分。首先,input node 負責從流裏面收到 Insert 消息,然後 filter node 對消息進行過濾。爲什麼需要過濾呢?因爲用戶可能僅需要加載 collection 下的某一個 partition 數據。過濾完之後,insert node 把這些數據插到底層的 growing segment 中,在這以後 server time node 負責更新查詢的服務時間。

最開始我們回顧數據 insert 流程時提到,每一條 insert message 中都有分配了一個時間戳。

大家可以參看圖示左側的例子,假如說數據從左到右只依次插入,那麼第一條消息插入的時間戳是 1,第二條消息插入的時間戳是 2,第三條消息操作時間戳是 6,第四條這裏爲什麼標紅呢?這是系統插入的 timetick message,它代表的不是 insert message。Timticker 表示 timestamp 小於這個 timetick 的插入數據都已經在 log broker 中了。換句話說,在這個 timetick 5 之後出現的 insert message  它們所對應的時間戳不會小於 5,可以看到後面的幾條時間戳分別是 7、8、9、10,時間戳都是大於 5 的,也就是說時間戳小於 5 的 insert message 消息肯定都會出現在左側。換句話說,當 query node 收到 timetick = 5 的消息時,可以確定說時間戳在 5 之前的所有消息都已經進入到 qurey node 中,從而來確認查詢的正確。那麼這裏的server time node 就是在從 insert node 接收到 timetiker 後,比如圖示的 5 或 9,會更新一個 tsafe, 相當於一個表示安全的時間戳,只要 tsafe 到了 5,那麼 5 之前的數據都是可以查的。

有了這些鋪墊,下面開始講如何真正的做 query 的這部分。

Milvus 上是實現實時查詢的相關操作和流程


首先講一下查詢請求(query message)是如何定義的。

Query message 同樣由 proxy 插入到 log broker, 在之後 query node 會通過監聽 log broker 中的 query channel, 來獲取到 query message。

Query message 具體長什麼樣呢?

  • Message ID,對這個查詢系統分配的一個全局分配的 ID;
  • Collection ID:query 請求對應的 collection ID,假如說 query 是制定在 collection 中查詢,那麼它要指定對應的 collection ID。當然在 SDK 那邊,其實這個地方指定的是 collection name, 在系統內會對 name 和 ID 做一對一的映射。
  • execPlan:執行數,對應 SDK 那邊的操作,相當於在 SDK 做查詢的時候指定了表達式,也就是一個 PR 。對於向量查詢來講,主要是做屬性過濾的,假如說某一個屬性大於 10 或者是等於 10 做一些使用過濾。
  • Service timestamp:上文提到的 tsafe 更新之後,service timestamp 也會相應更新,用來說明現在服務的時間到哪個點了,在此之前插入的數據都可以進行查詢。
  • Travel timestamp:如果需要對對某一個時間段之前的數據進行查詢,可以通過 (services timestamp - travle timestamp)來標定新的時間戳和數據範圍;
  • Guarantee timestmap:如果需要對某一個時間段之後的在進行數據查詢,只有當 services timestam 大於等於 guarantee timestamp 這個條件滿足時,查詢工作纔會開始。

現在看一下具體的查詢操作流程:

收到 query message 之後,系統會先去做一個判斷,如果 service time 大於 query message 中的 guarantee timestamp,那麼就會執行這個查詢。查詢分成兩個同時並行的部分, 一部分是持久化存儲的 historical data,另一部分是 log broker 中的 streaming data。最後會做一個 local reduce 。之前也講過 historical 和 streaming 中間因爲種種原因是可能會出現一些數據的重複的,那麼這裏最後就需要先做一個 reduce。

以上是比較順利的流程。而如果說在第一步判斷時間戳是,可服務時間還沒能推進到 guarantee timestamp,那麼這個查詢會放進 unsolved meessage, 一直等待,直到滿足條件可以進行查詢。

最終結果會被推送到 result channel,由 proxy 來接受。當然 proxy 會從很多 query node 上面接受結果,也會在做一輪 global reduce。到此整個查詢流程完畢。

但這裏還有一個問題,就是 proxy 在向 SDK 返回最終結果之前,如何去確定已經收到了全部的查詢結果。爲此我們做了一個策略:在返回的 result message 中,也會記錄下,哪些 sealed segments被查詢過 (searched sealed segments),以及哪些 dmChannel 被查詢過(dmchannels searched), 以及在 querynode 上有哪些 segment (global sealed segments)。如果所有 query node 的 search result 裏 searched sealed segments 的並集大於 global sealed segments,而且這個 collection 的所有 dmchannel 對應的增量數據都被查詢過,就認爲所有的查詢結果都收到了,proxy 就可以進行 reduce 操作,並將結果最終返回給 SDK。

完整版視頻講解請戳:https://www.bilibili.com/video/BV1cg411F7wd?spm_id_from=333.337.search-card.all.click

如果你在使用的過程中,對 Milvus 有任何改進或建議,歡迎在 GitHub 或者各種官方渠道和我們保持聯繫~

Z i l l i z  
Zilliz  Milvus Milvus  LF AI & Data  用。


閱讀原文,解鎖更多應用場景

本文分享自微信公衆號 - ZILLIZ(Zilliztech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章