如何使用 Milvus 向量資料庫實現實時查詢

Zilliz發表於2022-04-01
編者按:本文詳細介紹 Milvus2.0 如何對查詢節點的資料進行管理,以及如何提供查詢能力

內容大綱:

  • 快速回顧 Milvus 進行資料插入與持久化儲存相關的流程及機制;
  • 如何將資料載入進查詢節點(Query Node)以進行查詢操作 ;
  • Milvus 上實現實時查詢的相關操作和流程。

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

Milvus 架構快速回顧

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

Coordinator services 承擔的主要工作是協調各個 worker node 的工作,其中的各個模組與 work 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 天,這期間即使下面的 work 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 進行監聽,如圖 querynode 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 模型,一種狀態驅動的模型,整個flowGraph包括 input node, filter node, insert node 和 service time四部分。首先,input node負責從流裡面收到 Insert 訊息,然後filter node 對訊息進行過濾。 為什麼需要過濾呢?因為使用者可能僅需要載入collection下的某一個 partition 資料。過濾完之後,insert node 把這些資料插到底層的 growing sagment 中。在這以後 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。

相關文章