數據庫運維 | 攜程分佈式圖數據庫NebulaGraph運維治理實踐

作者簡介:Patrick Yu,攜程雲原生研發專家,關注非關係型分佈式數據存儲及相關技術。

背景

隨着互聯網世界產生的數據越來越多,數據之間的聯繫越來越複雜層次越來越深,人們希望從這些紛亂複雜的數據中探索各種關聯的需求也在與日遞增。爲了更有效地應對這類場景,圖技術受到了越來越多的關注及運用。

DB-ENGINES 趨勢報告顯示圖數據庫趨勢增長遙遙領先

在攜程,很早就有一些業務嘗試了圖技術,並將其運用到生產中,以 Neo4j 和 JanusGraph 爲主。2021 年開始,我們期望規範業務的使用,並適配攜程已有的各種系統,更好地服務業務方。經過調研,我們選擇分佈式圖數據庫 NebulaGraph 作爲管理的對象,主要基於以下幾個因素考慮:

  1. NebulaGraph 開源版本即擁有橫向擴展能力,爲大規模部署提供了基本條件;
  2. 使用自研的原生存儲層,相比 JanusGraph 這類構建在第三方存儲系統上的圖數據庫,性能和資源使用效率上具有優勢;
  3. 支持兩種語言,尤其是兼容主流的圖技術語言 openCypher,有助於用戶從其他使用 Cypher 語言的圖數據庫(例如 Neo4j)中遷移;
  4. 擁有後發優勢(2019 年起開源),社區活躍,且主流的互聯網公司都有參與(騰訊,快手,美團,網易等);
  5. 使用技術主流,代碼清晰,技術債較少,適合二次開發;

NebulaGraph 架構及集羣部署

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 的數量或者訪問的路由。

Session 管理功能

藍綠部署(包括讀寫分離)

上面章節中介紹了藍綠部署,相應的客戶端也需要改造以支持訪問 2 個集羣。由於生產中,讀和寫的邏輯往往不同,比如:讀操作希望可以由 2 個集羣共同提供數據,而寫的時候只希望影響單邊,所以我們在進行藍綠處理的時候也增加了讀寫分離(下圖)。

藍綠部署

流量分配

如果要考慮到單邊切換以及讀寫不同的路由策略,就需要增加流量分配功能。我們沒有采用攜程內廣泛使用的 Virtual IP 作爲訪問路由,希望有更爲強大的定製管理能力及更好的性能。

  1. 通過直連而不是 Virtual IP 中轉可以減少一次轉發的損耗;
  2. 在維持長連接的同時也能實現每次請求使用不同的鏈路,平攤 graphd 的訪問壓力;
  3. 完全自主控制路由,可以實現更爲靈活的路由方案;
  4. 當存在節點無法訪問的時候,客戶端可以自動臨時排除有問題的 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 信息的話,那麼問題就可能:

  1. Session 沒有過期
  2. 創建了太多的 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 使用率突然暴漲,導致整個集羣響應增加,從而應用端產生大量超時報錯,如下圖所示:

酒店集羣存儲服務 CPU 使用率過高

和酒店方排查下來初步懷疑是存在稠密點問題(在圖論中,稠密點是指一個點有着極多的相鄰邊,相鄰邊可以是出邊或者是入邊),部分 storaged 被集中訪問引起系統不穩定。由於業務方強調稠密點是其業務場景難以避免的情況,我們決定採取一些調優手段來緩解這個問題。

優化稠密點之嘗試通過 Balance 來分攤訪問壓力

回憶之前的官方架構圖,數據在 storaged 中是分片的,且 raft 協議中只有 leader 纔會處理請求,所以,重新進行數據平衡操作,是有可能將多個稠密點分攤到不同的服務上以減輕單一服務的壓力。同時,我們對整個集羣進行 Compaction 操作(由於 storaged 內部使用了 RocksDB 作爲存儲引擎,數據是通過追加來進行修改的,Compaction 可以清楚過時的數據,提高訪問效率)。

操作之後集羣的整體 CPU 是有一定的下降,同時服務的響應速度也有小幅的提升,如下圖。

通過 Balance 來分攤訪問壓力

通過 Balance 來分攤訪問壓力

但在運行一段時間之後仍然遇到了 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 信息的更新,排序的調整及數據的淘汰,存在吞吐量的限制。

使用 ClockCache

由於我們主要面臨的就是鎖競爭,在業務數據沒法變更的情況下,我們希望其他 cache 模塊來提升訪問的吞吐。按照 RocksDB 官方介紹,其還支持一種 cache 類型 ClockCache,特點是在查詢時不需要加鎖,只有在插入時才需要加鎖,會有更大的訪問吞吐,考慮到我們主要是讀操作,看起來 ClockCache 會比較合適。

LRU cache和Clock cache的區別:https://rocksdb.org.cn/doc/Block-Cache.html

經過修改源碼和重新編譯,我們將緩存模塊改成了 ClockCache,如下圖所示:

使用 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 的使用,在這種情況下,由於沒有文件信息需要置換出去,算法邏輯被關閉。

總結下來核心修改如下表:

關閉數據壓縮,關閉 block cache

避免文件被 table cache 淘汰,避免文件描述符被關閉,加快文件的讀取

關閉了 block cache 後,整個系統進入了一個非常穩定的狀態,線上集羣在訪問量增加一倍以上的情況下,系統的 CPU 峯值反而穩定在 30% 以下,且絕大部分時間都在 10% 以內(下圖)。

關閉數據壓縮,關閉 block cache

需要說明的是,酒店場景中關閉 block cache 是一個非常有效的手段,能夠對其特定情況下的熱點訪問起到比較好的效果,但這並非是一個常規方式,我們在其他業務方的 NebulaGraph 集羣中並沒有關閉 block cache。

數據寫入時服務 down 機

起因酒店業務在全量寫入的時候,即使量不算很大(4~5w/s),在不特定的時間就會導致整個 graphd 集羣完全 down 機。由於 graphd 集羣都是無狀態的,且互相之間沒有關係,如此統一的在某個時刻集體 down 機,我們猜測是由於訪問請求造成。通過查看堆棧發現了明顯的異常(下圖):

數據寫入時服務 down 機

可以看到上圖中的三行語句被反覆執行,很顯然這裏存在遞歸調用,並且無法在合理的區間內退出,猜測爲堆棧已滿。在增加了堆棧大小之後,整個執行沒有任何好轉,說明遞歸不僅層次很深,且可能存在指數級的增加的情況。同時觀察 down 機時的業務請求日誌,失敗瞬間大量執行失敗,但有一些執行失敗顯示爲 null 引用錯誤,如下圖所示:

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 用戶一起交流圖數據庫技術和應用技能,留下「你的名片」一起玩耍呀~

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