我們又重寫了一個關鍵服務

Zilliz發表於2022-11-30

01

QueryCoord 元件介紹

QueryCoord 是 Milvus 中查詢叢集的中心排程節點,在使用者將一個 Collection Load 到記憶體中時,QueryCoord 負責將該 Collection 的 Segment 排程到 QueryNode 叢集中,以支援後續的查詢。

QueryCoord 最核心的操作有4種:

  • Load:將資源載入到 QueryNode 中
  • Release:將資源從 QueryNode 釋放
  • Handoff:使用新的 Segment C 替換舊的 Segment A,B
  • Balance:在 QueryNodes 之間移動 Segment

此外,在 Milvus v2.1.0 我們加入了記憶體多副本的功能,透過增加 Segment 在記憶體中的副本數量來提升效能,提高可用性。

02

為什麼必須重構

在 Milvus v2.1.x 中,我們遇到了很多與 QueryCoord 相關的問題,其中不少問題無法在現有的設計上徹底解決,也有不少問題由於此前的設計耦合,在修復時很容易引入其它問題。
此前,有兩類問題是最為常見,也最為棘手的:

  1. QueryCoord 在某個操作之後長時間無響應
  2. QueryCoord 有時無法透過重啟恢復服務

我們在 Milvus v2.1.x 的開發過程中花費了大量的時間去解決無響應相關的問題。這個問題並不只是程式碼的問題,從根本來說是 QueryCoord 試圖提供的介面語義過於強大,以至於根本無法簡單實現我們期望的語義。

根本原因

此前的 QueryCoord,試圖為每一個介面都提供最終完成的語義,只要請求到達 QueryCoord,即使後續出現錯誤,QueryCoord 也會試圖最終將這些請求完成。

此外,QueryCoord 會記錄每個 Segment 所處的節點位置,並將這一資訊持久化。當發生 Balance 之類的操作時,根據 RPC 是否成功來決定是否要修改記錄的位置資訊。

我們很快意識到這一做法是無法做到準確的,因為 RPC 會有 false failure,QueryCoord 主動跟蹤資源位置,並以 RPC 結果作為資源位置變化的依據,是必定會發生資源洩漏的。

為了達成這種最終完成的語義,QueryCoord 選擇實現了一個完全序列的 scheduler,將每一種請求視作一類 task,可以認為這個 task scheduler 就是一個 WAL applier,而請求就是 WAL 中的 record,QueryCoord 會將所有 task 持久化,並在重啟時重放這些 task。

這一套機制像由若干小齒輪連線起來的精密器件,容錯率是很低的:

  • 由於完全序列執行,當某一個請求無法執行,或執行慢時,後續的所有請求都會被阻塞
  • 由於持久化了所有的 task,一個無法完成的請求會持續阻塞整個系統,即使重啟也無法恢復
  • 一個有副作用的請求失敗時(如 Load),無法保證介面的原子性

以 Load 請求為例,如果在 Load 了部分 Segment 後,請求無法繼續執行。

在這一套機制下就無法做出正確的處理。如果多次重試,則會讓後續請求的阻塞更長時間;如果直接丟棄請求,則可能無法釋放已經 Load 成功的 Segment。

03

重構思路

QueryCoord 的本質是一個資源排程中心,我們需要把 Segment 和 Channel 排程到 QueryNode 叢集中來支援查詢。

同時也要很好的支援記憶體多副本,在副本數量與預設值不同時能進行相應的上下線操作。新的 QueryCoord 設計參考了 Placement Driver 等業內成熟系統,我們有一些基本的出發點:

  • QueryCoord 必須感知真實的資源分佈情況
  • 資源上下線操作是否完成不能以 RPC 是否成功作為判斷條件
  • 啟動時依賴的持久化資訊需要儘量少
  • 需要一個元件不斷根據分佈對資源進行上下線調整

從這些點出發,重構後的 QueryCoord 作出了下面的調整:

心跳機制

依賴 RPC 的成功與否去判斷資源的上下線是否成功是不可行的,RPC 存在 false failure,因此可能會導致資源洩露。

叢集中資源的分佈情況,最真實的來源就是節點本身報告自己持有哪些資源,因此必須加入一套心跳機制,在 QueryCoord 與 QueryNode 之間同步資源分佈情況。

在加入心跳機制後,QueryCoord 不再去持久化資源的分佈資訊,而是在啟動時詢問所有的 QueryNode 來恢復出分佈資訊,這樣可以避免髒資料對重啟帶來影響。

同時,依賴心跳彙報的資源分佈資訊來判斷資源的上下線是否完成,是最準確的,在 Balance 這個操作中,我們需要對一個 Segment 進行先上後下,以保證 Balance 過程中不會影響服務的可用性,透過心跳機制,我們可以準確地判斷上線操作是否真的已經完成,來決定是否進行下線操作。

拋棄最終完成語義

從系統自身的角度來看,最終完成語義是很難保證的,這一性質要求持久化所有請求,並在重啟後進行重放。一些請求並不能保證總是可執行的,例如一個載入 Segment 的請求,可能在重啟後這個 Segment 已經被 Compact,從而無法載入。

從使用者的角度來看,當服務不可用時,使用者通常期望可以透過重啟去清理掉一些髒資料,讓服務能夠重新工作。拋棄最終完成的語義之後,QueryCoord 只對介面提供原子性保證,不再持久化請求,避免恢復時被一些無法完成的請求阻塞。

在做出上面的修改後,我們也不再需要一個全域性序列的 scheduler,而是將請求的併發粒度降低到了 Collection 級別,並且需要進入 scheduler 的請求只有 Load/Release 兩類。可以極大提高 QueryCoord 的響應速度。

資源分佈檢查

在一個分散式的環境中,任何網路請求都有失敗的可能。一個對一批資源進行處理的請求甚至可能是部分成功的,我們希望系統具有更高的容錯率,在任何情況下都有能夠將資源副本數量調整到預設值的能力。

因此在新的 QueryCoord 中,我們加入了若干的資源檢查器(Checker):

  • Balance Checker:檢查叢集的負載情況,並作出適當的資源調整,平衡叢集負載
  • Channel Checker:檢查叢集中各個 Channel 的分佈情況,保證 Channel 的副本數量不多不少
  • Segment Checker:檢查叢集中各個 Segment 的分佈情況,保證 Segment 的副本數量不多不少

在 Release 的情況下,檢查器能夠保證即使在 Release 實現存在 Bug 的情況下,也可以把洩露的資源釋放掉。

04

資源排程系統的設計感悟

得益於 Milvus 的架構設計,在這次重構中我們幾乎重寫了整個 QueryCoord,但依然能夠無縫的替換掉原來的 QueryCoord。在新的 QueryCoord 上線後,Milvus 系統:

  • Query 叢集更加健壯,穩定
  • 徹底解決 Load/Release 等請求無響應的問題
  • 保證了 Query 相關服務在重啟後能夠恢復
  • QueryCoord 更加容易維護,排查問題更加容易

而我個人也從這次重構中收穫良多,在資源排程系統的設計上,與程式碼實現上獲得了許多感悟:

  • 分佈資訊作為排程的輸入,必須是真實的,也就是各節點上報的資訊彙總
  • 簡單,清晰的語義比看上去強大的複雜語義更強大, 簡單粗暴往往意味著高容錯
  • 原子性是一個非常有力的性質,系統在各類操作有了原子性保證後會更容易維護,在併發的情況下,為了保證原子性有時需要很小心

在 QueryCoord 的後續演進中,我們會繼續強化 QueryCoord 的可用性與 Query 叢集的效能。

相關文章