資料庫運維 | 攜程分散式圖資料庫NebulaGraph運維治理實踐
作者簡介:Patrick Yu,攜程雲原生研發專家,關注非關係型分散式資料儲存及相關技術。
背景
隨著網際網路世界產生的資料越來越多,資料之間的聯絡越來越複雜層次越來越深,人們希望從這些紛亂複雜的資料中探索各種關聯的需求也在與日遞增。為了更有效地應對這類場景,圖技術受到了越來越多的關注及運用。
在攜程,很早就有一些業務嘗試了圖技術,並將其運用到生產中,以 Neo4j 和 JanusGraph 為主。2021 年開始,我們期望規範業務的使用,並適配攜程已有的各種系統,更好地服務業務方。經過調研,我們選擇分散式圖資料庫 NebulaGraph 作為管理的物件,主要基於以下幾個因素考慮:
- NebulaGraph 開源版本即擁有橫向擴充套件能力,為大規模部署提供了基本條件;
- 使用自研的原生儲存層,相比 JanusGraph 這類構建在第三方儲存系統上的圖資料庫,效能和資源使用效率上具有優勢;
- 支援兩種語言,尤其是相容主流的圖技術語言 openCypher,有助於使用者從其他使用 Cypher 語言的圖資料庫(例如 Neo4j)中遷移;
- 擁有後發優勢(2019 年起開源),社群活躍,且主流的網際網路公司都有參與(騰訊,快手,美團,網易等);
- 使用技術主流,程式碼清晰,技術債較少,適合二次開發;
NebulaGraph 架構及叢集部署
NebulaGraph 是一個分散式的計算儲存分離架構,如下圖:
其主要由 graphd,metad 和 storaged 三部分服務組成,分別負責計算,元資料存取,圖資料(點,邊,標籤等資料)的存取。在攜程的網路環境中,我們提供了三種部署方式來支撐業務,分別是:三機房部署、單機房部署和藍綠雙活部署。
三機房部署
用於滿足一致性和容災的要求,優點是任意一個機房發生機房級別故障,叢集仍然可以使用,適用於核心應用。但缺點也是比較明顯的,資料通過 raft 協議進行同步的時候,會遇到跨機房問題,效能會受到影響。
單機房部署
叢集所有節點都在一個機房中,節點之間通訊可以避免跨機房問題(應用端與服務端之間仍然會存在跨機房呼叫),由於機房整體出現問題時該部署模式的系統將無法使用,所以適用於非核心應用進行訪問。
藍綠雙活部署
在實際使用中,以上兩種常規部署方式並不能滿足一些業務方的需求,比如:效能要求較高的核心應用,三機房的部署方式所帶來的網路損耗可能會超出預期。根據攜程酒店某個業務場景真實測試資料來看,本地三機房的部署方式延遲要比單機房高 50%+,但單機房部署無法抵抗單個 IDC 故障。此外,還有使用者希望能存在類似資料回滾的能力,以應對應用釋出,叢集版本升級可能導致的錯誤。
考慮到使用圖資料庫的業務大多資料來自離線系統,通過離線作業將資料匯入到圖資料庫中,資料一致的要求並不高,在這種條件下使用藍綠部署能夠在災備和效能上得到很好的滿足。
與此同時我們還增加了一些配套的輔助功能,比如:
- 分流:可以按比例分配機房的訪問,也可以主動切斷對某個機房的流量訪問
- 災備:在發生機房級故障時,可自動切換讀訪問的流量,寫訪問的流量切換則通過人工進行操作
藍綠雙活方式是在效能、可用性、一致性上的一個折中的選擇,使用此方案時應用端架構也需要有更多的調整以配合資料的存取。
生產上的一個例子:
上圖為三機房情況,下圖為藍綠部署情況:
中介軟體及運維管理
我們基於 K8s CRD 和 Operator 來進行 NebulaGraph 的部署,同時通過服務整合到現有的部署配置頁面和運維管理頁面,來獲得對 Pod 的執行和遷移的控制能力。基於 sidecar
模式監控、收集 NebulaGraph 的核心指標並通過 Telegraf 傳送到攜程自研的 Hickwall 集中展示,並設定告警等一系列相關工作。
此外,我們集成了跨機房的域名分配功能,為節點自動分配域名用於內部訪問(域名只用於叢集內部,叢集與外部連通是通過 IP 直連的),這樣做是為了避免節點漂移造成 IP 變更,影響叢集的可用性。
在客戶端上,相比原生客戶端,我們主要做了以下幾個改進和優化:
Session 管理功能
原生客戶端 Session 管理比較弱,尤其是 v2.x 早期幾個版本,多執行緒訪問 Session 並不是執行緒安全的,Session 過期或者失效都需要呼叫方來處理,不適合大規模使用。同時,雖然官方客戶端建立的 Session 是可以複用的,並不需要 release,官方也鼓勵使用者複用,但是卻沒有提供統一的 Session 管理功能來幫助使用者複用。因此,我們增加了 Session Pool 的概念來實現複用。
其本質上是管理一個或多個 Session Object Queue,通過 borrow-and-return
的方式(下圖),確保了一個 Session 在同一時間只會由一個執行器在使用,避免了共用 Session 產生的問題。同時通過對佇列的管理,我們可以進行 Session 數量和版本的管理,比如:預生成一定量的 Session,或者在管理中心發出訊息之後變更 Session 的數量或者訪問的路由。
藍綠部署(包括讀寫分離)
上面章節中介紹了藍綠部署,相應的客戶端也需要改造以支援訪問 2 個叢集。由於生產中,讀和寫的邏輯往往不同,比如:讀操作希望可以由 2 個叢集共同提供資料,而寫的時候只希望影響單邊,所以我們在進行藍綠處理的時候也增加了讀寫分離(下圖)。
流量分配
如果要考慮到單邊切換以及讀寫不同的路由策略,就需要增加流量分配功能。我們沒有采用攜程內廣泛使用的 Virtual IP 作為訪問路由,希望有更為強大的定製管理能力及更好的效能。
- 通過直連而不是 Virtual IP 中轉可以減少一次轉發的損耗;
- 在維持長連線的同時也能實現每次請求使用不同的鏈路,平攤 graphd 的訪問壓力;
- 完全自主控制路由,可以實現更為靈活的路由方案;
- 當存在節點無法訪問的時候,客戶端可以自動臨時排除有問題的 IP,在短時間內避免再次使用。而如果使用 Virtual IP 的話,由於一個 Virtual IP 會對應多個物理 IP,就沒有辦法直接這樣操作。
通過構造面向不同 IDC 的 Session Pool,並根據配置進行權重輪詢,就可以達到按比例分配訪問流量的目的(下圖)。
將流量分配整合進藍綠模式,就基本實現了基本的客戶端改造(下圖)。
結構化語句查詢
圖 DSL 目前主流的有兩種,Gremlin 和 Cypher,前者是過程式語言而後者是宣告式語言。NebulaGraph 支援了 openCypher(Cypher 的開源專案)語法和自己設計的 nGQL 原生語法,這兩種都是宣告式語言,在風格上比較類似 SQL。儘管如此,對於一些較為簡單的語句,類似 Gremlin 風格的過程式語法對使用者會更為友好,並且有利用監控埋點。基於這個原因,我們封裝了一個過程式的語句生成器。
例如:
系統調優實踐
由於建模,使用場景,業務需求的差異,使用Nebula Graph的過程中所遇到的問題很可能會完全不同,以下以攜程酒店資訊圖譜線上具體的例子進行說明,在整個落地過程我們遇到的問題及處理過程(文中以下內容是基於Nebula Graph 2.6.1進行的)。
關於酒店該業務的更多細節,可以閱讀《資訊圖譜在攜程酒店的應用》這篇文章。
酒店叢集不穩定
起因是酒店應用上線後發生了一次故障,大量的訪問超時,並伴隨著 “The leader has changed”
這樣的錯誤資訊。稍加排查,我們發現 metad 叢集有問題,metad0 的 local ip 和 metad_server_address 的配置不一致,所以 metad0 實際上一直沒有工作。
但這本身並不會導致系統問題,因為 3 節點部署,只需要 2 個節點工作即可。後來 metad1 容器又意外被漂移了,導致 IP 變更,這個時候實際上 metad 叢集已經無法工作(下圖),導致整個叢集都受到了影響。
在處理完以上故障並重啟之後,整個系統卻並沒有恢復正常,CPU 的使用率很高。此時,外部應用並沒有將流量接入進來,但整個 metad 叢集內部網路流量卻很大,如下圖所示:
監控顯示 metad 磁碟空間使用量很大,檢查下來 WAL 在不斷增加,說明這些流量主要是資料的寫入操作。我們開啟 WAL 資料的某幾個檔案,其大部分都是 Session 的元資料,因為 Session 資訊是會在 NebulaGraph 叢集內持久化的,所以考慮問題可能出在這裡。通過閱讀原始碼我們注意到,graphd 會從 metad 中同步所有的 Session 資訊,並在修改之後將資料再全部回寫到 metad 中,所以如果流量都是 session 資訊的話,那麼問題就可能:
- Session 沒有過期
- 建立了太多的 Session
檢查發現該叢集沒有配置 Session 超時時間,所以我們修改以下配置來處理這個問題:
修改之後,metad 的磁碟空間佔用下降,同時通訊流量和磁碟讀寫也明顯下降(下圖):
系統逐步恢復正常,但是還有一個問題沒有解決,就是為什麼有如此之多的 Session 資料?檢視應用端日誌,我們注意到 Session 建立次數超乎尋常,如下圖所示:
通過日誌發現是我們自己開發的客戶端中的 bug 造成的。我們會在報錯時讓客戶端釋放對應的 Session,並重新建立。但,由於系統抖動,這個行為造成了比較多的超時,導致更多的 Session 被釋放並重建,引起了惡性迴圈。針對這個問題,對客戶端進行了如下優化:
序號 | 修改 |
---|---|
1 | 將建立 session 行為由併發改為序列,每次只允許一個執行緒進行建立工作,不參與建立的執行緒監聽 session pool |
2 | 進一步增強 session 的複用,當 session 執行失敗的時候,根據失敗原因來決定是否需要 release。原有的邏輯是一旦執行失敗就 release 當前 session,但有些時候並非是 session 本身的問題,比如超時時間過短,nGQL 有錯誤這些應用層的情況也會導致執行失敗,這個時候如果直接 release,會導致 session 數量大幅度下降從而造成大量 session 建立。根據問題合理的劃分錯誤情況來進行處理,可以最大程度保持 session 狀況的穩定 |
3 | 增加預熱功能,根據配置提前建立好指定數量的 session,以避免啟動時集中建立 session 導致超時 |
酒店叢集儲存服務 CPU 使用率過高
酒店業務方在增加訪問量的時候,每次到 80% 的時候叢集中就有少數 storaged 不穩定,CPU 使用率突然暴漲,導致整個叢集響應增加,從而應用端產生大量超時報錯,如下圖所示:
和酒店方排查下來初步懷疑是存在稠密點問題(在圖論中,稠密點是指一個點有著極多的相鄰邊,相鄰邊可以是出邊或者是入邊),部分 storaged 被集中訪問引起系統不穩定。由於業務方強調稠密點是其業務場景難以避免的情況,我們決定採取一些調優手段來緩解這個問題。
優化稠密點之嘗試通過 Balance 來分攤訪問壓力
回憶之前的官方架構圖,資料在 storaged 中是分片的,且 raft 協議中只有 leader 才會處理請求,所以,重新進行資料平衡操作,是有可能將多個稠密點分攤到不同的服務上以減輕單一服務的壓力。同時,我們對整個叢集進行 Compaction 操作(由於 storaged 內部使用了 RocksDB 作為儲存引擎,資料是通過追加來進行修改的,Compaction 可以清楚過時的資料,提高訪問效率)。
操作之後叢集的整體 CPU 是有一定的下降,同時服務的響應速度也有小幅的提升,如下圖。
但在執行一段時間之後仍然遇到了 CPU 突然增加的情況,稠密點顯然沒有被平衡掉,也說明在分片這個層面是沒法緩解稠密點帶來的訪問壓力的。
優化稠密點之嘗試通過配置緩解鎖競爭
進一步調研出現問題的 storaged 的 CPU 的使用率,可以看到當流量增加的時候,核心佔用的 CPU 非常高,如下圖所示:
抓取 perf 看到,鎖競爭比較激烈,即使在“正常”情況下,鎖的佔比也很大,而在競爭激烈的時候,出問題的 storaged 服務上這個比例超過了 50%。如下圖所示:
所以我們從減少衝突入手,對 NebulaGraph 叢集主要做了如下改動:
重新上線之後,整個叢集服務變得比較平滑,CPU 的負載也比較低,正常情況下鎖競爭也下降不少(下圖),酒店也成功地將流量推送到了 100%。
但運行了一段時間之後,我們仍然遇到了服務響應突然變慢的情況,熱點訪問帶來的壓力的確超過了優化帶來的提升。
優化稠密點之嘗試減小鎖的顆粒度
考慮到在分片級別的 balance 不起作用,而 CPU 的上升主要是因為鎖競爭造成的,那我們想到如果減小鎖的顆粒度,是不是就可以儘可能減小競爭?RocksDB 的 LRUCache 允許調整 shared 數量,我們對此進行了修改:
版本 | LRUCache 預設分片數 | 方式 |
---|---|---|
v2.5.0 | 2^8 | 修改程式碼,將分片改成 2^10 |
v2.6.1及以上 | 2^8 | 通過配置 cache_bucket_exp = 10 ,將分片數改為 2^10 |
觀察下來效果不明顯,無法解決熱點競爭導致的雪崩問題。其本質同 balance 操作一樣,只是粒度的大小的區別,在熱點非常集中的情況下,在資料層面進行處理是走不通的。
優化稠密點之嘗試使用 ClockCache
競爭的鎖來源是 block cache 造成的。NebulaGraph storaged 使用 RocksDB 作為儲存,其使用的是 LRUCache 作為 block cache 等一系列 cache 的儲存模組,LRUCache 在任何型別的訪問的時候需要需要加鎖操作,以進行一些 LRU 資訊的更新,排序的調整及資料的淘汰,存在吞吐量的限制。
由於我們主要面臨的就是鎖競爭,在業務資料沒法變更的情況下,我們希望其他 cache 模組來提升訪問的吞吐。按照 RocksDB 官方介紹,其還支援一種 cache 型別 ClockCache,特點是在查詢時不需要加鎖,只有在插入時才需要加鎖,會有更大的訪問吞吐,考慮到我們主要是讀操作,看起來 ClockCache 會比較合適。
LRU cache和Clock cache的區別:https://rocksdb.org.cn/doc/Block-Cache.html
經過修改原始碼和重新編譯,我們將快取模組改成了 ClockCache,如下圖所示:
但叢集使用時沒幾分鐘就 core,查詢資料我們發現目前 ClockCache 支援還存在問題(https://github.com/facebook/rocksdb/pull/8261),此方案目前無法使用。
優化稠密點之限制執行緒使用
可以看到整個系統在當前配置下,是存在非常多的執行緒的,如下圖所示。
如果是單執行緒,就必然不會存在鎖競爭。但作為一個圖服務,每次訪問幾乎會解析成多個執行器來併發訪問,強行改為單執行緒必然會造成訪問堆積。
所以我們考慮將原有的執行緒池中的程序調小,以避免太多的執行緒進行同步等待帶來的執行緒切換,以減小系統對 CPU 的佔用。
調整之後整個系統 CPU 非常平穩,絕大部分物理機 CPU 在 20% 以內,且沒有之前遇到的突然上下大幅波動的情況(瞬時激烈鎖競爭會大幅度提升 CPU 的使用率),說明這個調整對當前業務來說是有一定效果的。
隨之又遇到了下列問題,前端服務突然發現 NebulaGraph 的訪問大幅度超時,而從系統監控的角度卻毫無波動(下圖 24,19:53 系統其實已經響應出現問題了,但 CPU 沒有任何波動)。
原因是在於,限制了 thread 確實有效果,減少了競爭,但隨著壓力的正大,執行緒吞吐到達極限。但如果增加執行緒,資源的競爭又會加劇,無法找到平衡點。
優化稠密點之關閉資料壓縮,關閉 block cache
在沒有特別好的方式避免鎖競爭的情況,我們重新回顧了鎖競爭的整個發生過程,鎖產生本身就是由 cache 自身的結構帶來的,尤其是在讀操作的時候,我們並不希望存在什麼鎖的行為。
使用 block cache,是為了在合理的快取空間中儘可能的提高快取命中率,以提高快取的效率。但如果快取空間非常充足,且命中長期的資料長期處於特定的範圍內,實際上並沒有觀察到大量的快取淘汰的情況,且當前服務的快取實際上也並沒有用滿,所以想到,是不是可以通過關閉 block cache,而直接訪問 page cache 來避免讀操作時的加鎖行為。
除了 block cache,儲存端還有一大類記憶體使用是 indexes and filter blocks,與此有關的設定在 RocksDB 中是 cache_index_and_filter_blocks
。當這個設定為 true 的時候,資料會快取到 block cache 中,所以如果關閉了 block cache,我們就需要同樣關閉 cache_index_and_filter_blocks
(在 NebulaGraph 中,通過配置項 enable_partitioned_index_filter
替代直接修改 RocksDB 的 cache_index_and_filter_blocks
)。
但僅僅修改這些並沒有解決問題,實際上觀察 perf 我們仍然看到鎖的競爭造成的阻塞(下圖):
這是因為當 cache_index_and_filter_blocks
為 false 的時候,並不代表 index 和 filter 資料不會被載入到記憶體中,這些資料其實會被放進 table cache 裡,仍然需要通過 LRU 來維護哪些檔案的資訊需要淘汰,所以 LRU 帶來的問題並沒有完全解決。處理的方式是將 max_open_files
設定為 -1,以提供給系統無限制的 table cache 的使用,在這種情況下,由於沒有檔案資訊需要置換出去,演算法邏輯被關閉。
總結下來核心修改如下表:
避免檔案被 table cache 淘汰,避免檔案描述符被關閉,加快檔案的讀取
關閉了 block cache 後,整個系統進入了一個非常穩定的狀態,線上叢集在訪問量增加一倍以上的情況下,系統的 CPU 峰值反而穩定在 30% 以下,且絕大部分時間都在 10% 以內(下圖)。
需要說明的是,酒店場景中關閉 block cache 是一個非常有效的手段,能夠對其特定情況下的熱點訪問起到比較好的效果,但這並非是一個常規方式,我們在其他業務方的 NebulaGraph 叢集中並沒有關閉 block cache。
資料寫入時服務 down 機
起因酒店業務在全量寫入的時候,即使量不算很大(4~5w/s),在不特定的時間就會導致整個 graphd 叢集完全 down 機。由於 graphd 叢集都是無狀態的,且互相之間沒有關係,如此統一的在某個時刻集體 down 機,我們猜測是由於訪問請求造成。通過檢視堆疊發現了明顯的異常(下圖):
可以看到上圖中的三行語句被反覆執行,很顯然這裡存在遞迴呼叫,並且無法在合理的區間內退出,猜測為堆疊已滿。在增加了堆疊大小之後,整個執行沒有任何好轉,說明遞迴不僅層次很深,且可能存在指數級的增加的情況。同時觀察 down 機時的業務請求日誌,失敗瞬間大量執行失敗,但有一些執行失敗顯示為 null 引用錯誤,如下圖所示:
這是因為返回了報錯,但沒有 error message,導致發生了空引用(空引用現象是客戶端未合理處理這種情況,也是我們客戶端的 bug),但這種情況很奇怪,為什麼會沒有 error message,檢查其 trace 日誌,發現這些請求執行 NebulaGraph 時間都很長,且存在非常大段的語句。如下圖所示:
預感是這些語句導致了 graphd 的 down 機,由於執行被切斷導致客戶端生成了一個 null 值。將這些語句進行重試,可以必現 down 機的場景。檢查這樣的請求發現其是由 500 條語句組成(業務方語句拼接上限 500),並沒有超過配置設定的最大執行語句數量(512)。
看起來這是一個 NebulaGraph 官方的 bug,我們已經將此問題提交給官方。同時,業務方語句拼接限制從 500 降為 200 後順利避免該問題導致的 down 機,該 bug 已在新版中修復。
NebulaGraph 二次開發
當前我們對 NebulaGraph 的修改主要集中的幾個運維相關的環節上,比如新增了命令來指定遷移 storaged 中的分片,以及將 leader 遷移到指定的例項上(下圖)。
未來規劃
- 與攜程大資料平臺整合,充分利用 Spark 或者 Flink 來實現資料的傳輸和 ETL,提高異構叢集間資料的遷移能力。
- 提供 Slowlog 檢查功能,抓取造成 slowlog 的具體語句。
- 引數化查詢功能,避免依賴注入。
- 增強視覺化能力,增加定製化功能。
謝謝你讀完本文 (///▽///)
如果你想嚐鮮圖資料庫 NebulaGraph,體驗雲上圖資料庫一鍵服務你的業務 ->☆白嫖 NebulaGraph 雲服務;NebulaGraph 也是一款開源的圖資料庫,上 GitHub 看程式碼、(з)-☆ star 它 -> GitHub;和其他的 NebulaGraph 使用者一起交流圖資料庫技術和應用技能,留下「你的名片」一起玩耍呀~