首發於 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/task-management-design-in-nebula-graph/git
講解 Task Manager 以前,在這裏先介紹一些 Task Manager 會使用到的概念術語。github
圖數據庫 Nebula Graph 中,存在一些長期在後臺運行的任務,咱們稱之爲 Job。存儲層存在的 DBA 使用的部分指令,好比:數據完成導入後,想在全局作一次 compaction,都是 Job 範疇。數據庫
做爲一個分佈式的系統,Nebula Graph 中 Job 由不一樣的 storaged 完成,而咱們管一個 storaged 上運行的 Job 子任務叫作 Task。Job 的控制由 metad 上的 Job Manager 負責,而 Task 的控制由 storaged 上的 Task Manager 負責。安全
在本文中,咱們着重講述如何對長耗時的 Task 進行管理與調度進一步提高數據庫性能。微信
Task Manager 要解決的問題
上文說到 storaged 上的 Task Manager 控制的 Task 是 meta 控制的 Job 的子任務,那 Task Manager 它本身具體解決什麼問題呢?在 Nebula Graph 中 Task Manager 主要解決了如下 2 個問題:多線程
- 將以前經過 HTTP 的傳送方式改成 RPC(Thrift) 通常用戶在搭建集羣時,知道 storaged 之間通訊使用 Thrift 協議,會爲 Thrift 所需端口開放防火牆,可是可能意識不到 Nebula Graph 還須要使用 HTTP 端口,咱們遇到過屢次社區用戶實踐忘記開放 HTTP 端口的事情。
- storaged 對於 Task 有調度能力 這塊內容將在本文下面章節展開講述。
Task Manager 在 Nebula Graph 中的位置
Task Manager 體系中的 meta
在 Task Manager 體系中, metad(JobManager)的任務是根據 graphd 中傳過來的一個 Job Request,選出對應的 storaged host,並拼組出 Task Request 發給對應的 storaged。不難發現,體系中 meta 接受 Job Request,拼組 Task Request , 發送 Task Request 及接受 Task 返回結果,這些邏輯的套路是穩定的。而如何拼組 TaskRequest,將 Task Request 發給哪些 storaged 則會根據不一樣的 Job 有所變化。JobManager 用 模板策略
+ 簡單工廠
以應對將來的擴展。併發
讓將來的 Job 一樣繼承於 MetaJobExecutor,並實現 prepare() 和 execute() 方法便可。分佈式
Task Manager 的調度控制
以前提到的,Task Manager 的調度控制但願作到 2 點:高併發
- 系統資源足夠時,儘量的高併發執行 Task
- 系統資源吃緊時,讓全部運行中的 Task 佔用的資源不要超過某一個設定的閾值。
高併發執行 Task
Task Manager 將系統資源中本身持有的線程稱之爲 Worker。Task Manager 有一個現實中的模擬原型——銀行的營業廳。想象一下, 咱們去銀行辦業務時會有如下幾步:post
- 場景 1:在門口的排號機拿一個號
- 場景 2:在大廳找個位置, 邊玩手機邊等叫號
- 場景 3:等叫到號時, 到指定窗口辦理
同時, 你還會碰到這樣那樣的問題:
- 場景4:VIP 能夠插隊
- 場景5:你可能排着隊, 由於某些緣由, 放棄了本次業務
- 場景6:你可能排着排着隊, 銀行就關門了
那麼, 整理一下, 這也就是 Task Manager 的基本需求
- Task 按 FIFO 順序執行:不一樣的 Task 有不一樣的優先級,高優先級的能夠插隊
- 用戶可取消一個排隊中的 Task
- storaged 隨時 shutdown
- 一個 Task,爲了使其儘量高的併發,會被拆分爲多個 SubTask,SubTask 是每一個 Worker 真正執行的任務
- Task Manager 是全局惟一實例,要考慮多線程安全性
因而, 有了以下實現:
- 實現 1:用 Thrift 結構中的 JobId 和 TaskId,肯定一個 Task,稱爲 Task Handle。
- 實現 2:TaskManager 會有一個 Blocking Queue,負責讓 Task 的 Handle 排隊執行(排號機),而 Blocking Queue 自己線程安全。
- 實現 3:Blocking Queue 同時支持不一樣的優先級, 高優先級先出隊(VIP 插隊的功能)。
- 實現 4:Task Manager 維持一個全局惟一的 Map,key 是 Task Handle,value 是具體的 Task(銀行的大廳)。在 Nebula Graph 中採用了 folly 的 Concurrent Hash Map,線程安全的 Map。
- 實現 5:若是有用戶 cancel Task,直接在根據 Handle 找到 Map 中對應的 Task,並標記 cancel,對 queue 中的 Handle 不作處理。
- 實現 6:若是有正在運行的 Task,對於 storaged 的 shutdown 會等到這個 Task 正在執行的 subTask 執行完畢才返回。
限定 Task 佔用的資源閾值
保證不超過閾值仍是很簡單的,由於 Worker 就是線程,只要讓全部的 Worker 都出自一個線程池,就能夠保證最大的 Worker 數。麻煩的是將子任務平均地分配到 Worker 中, 咱們來討論下方案:
方法一:使用 Round-robin 添加任務
最簡單的方法是用 Round-robin 的方式來添加任務。也就是將 Task 分解爲 Sub Task 以後, 依次追加到如今的各個 Worker 中。
可是可能會有問題, 好比說, 我有 3 個 Worker, 2 個 Task(藍色爲 Task 1,黃色爲 Task 2):
Round-robin 圖 1
假如 Task 2 中的 Sub Task 執行遠快於 Task1 的, 那麼好的並行策略應該是這樣:
Round-robin 圖 2
簡單粗暴的 Round-robin 會讓 Task 2 的完成時間依賴於 Task 1(見 Round-robin 圖1)。
方法二:一組 worker 處理一個 Task
針對方法一可能會出現的狀況,設定專門的 Worker 只處理指定的 Task,從而避免多個 Task 相互依賴問題。可是依然不夠好, 好比說:
很難保證每一個 Sub Task 執行時間基本相同,假設 Sub Task 1 的執行明顯慢於其餘的 Sub Task,那麼好的執行策略應該是這樣的:
這個方案仍是避免不了 1 核有難,10 核圍觀的問題 👀。
方法三:Nebula Graph 採用的解決方案
在 Nebula Graph 中 Task Manager 會將 Task 的 Handle 交給 N 個 Worker。N 由總 Worker 數、總 Sub Task 數,以及 DBA 在提交 Job 時指定的併發參數共同決定。
每一個 Task 內部維護一個 Blocking Queue(下圖的 Sub Task Queue),存放 Sub Task。Worker 在執行時,根據本身持有的 Handle 先找到 Task,再從 Task 的 Block Queue 中獲取 Sub Task。
設計補充說明
問題 1: 爲何不直接將 Task 放到 Blocking Queue 排隊,而是拆成兩部分,將 Task 保存在 Map 裏,讓 Task Handle 排隊?
主要緣由是 C++ 多線程基礎設施很差支持這種邏輯。Task 須要支持 cancel。假設 Task 放在 Blocking Queue 中,就須要 Blocking Queue 支持定位到其中的某一個 Task 的能力。而當前 folly 中的 Blocking Queue 都沒有此類接口。
問題 2: 什麼樣的 Job 有 VIP 待遇?
當前 Task Manager 支持的 compaction / rebuild index 對執行時間並不敏感,支持相似 count() 查詢操做功能尚在開發中。考慮到用戶但願在一個相對短的時間內完成 count() ,那麼假如正好碰上了 storaged 在作多個 compaction,仍是但願 count(*) 能夠優先運行,而非在全部 compaction 以後再開始作。
本文中若有任何錯誤或疏漏歡迎去 GitHub:https://github.com/vesoft-inc/nebula issue 區向咱們提 issue 或者前往官方論壇:https://discuss.nebula-graph.com.cn/ 的 建議反饋
分類下提建議 👏;加入 Nebula Graph 交流羣,請聯繫 Nebula Graph 官方小助手微信號:NebulaGraphbot
做者有話說:Hi,我是 我是 lionel.liu,是圖數據 Nebula Graph 研發工程師,對數據庫查詢引擎有濃厚的興趣,但願本次的經驗分享能給你們帶來幫助,若有不當之處也但願能幫忙糾正,謝謝~