攜程分散式圖資料庫Nebula Graph運維治理實踐—大規模叢集部署與二開優化

語言: CN / TW / HK

一、背景

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

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

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

1)Nebula Graph開源版本即擁有橫向擴充套件能力,為大規模部署提供了基本條件;

2)使用自研的原生儲存層,相比JanusGraph這類構建在第三方儲存系統上的圖資料庫,效能和資源使用效率上具有優勢;

3)支援兩種語言,尤其是相容主流的圖技術語言Cypher,有助於使用者從其他使用Cypher語言的圖資料庫(例如Neo4j)中遷移;

4)擁有後發優勢(2019起開源),社群活躍,且主流的網際網路公司都有參與(騰訊,快手,美團,網易等);

5)使用技術主流,程式碼清晰,技術債較少,適合二次開發;

 

二、Nebula Graph架構及叢集部署

Nebula Graph是一個分散式的計算儲存分離架構,如下圖:

其主要由Graphd,Metad和Storaged三部分服務組成,分別負責計算,元資料存取,圖資料(點,邊,標籤等資料)的存取。在攜程的網路環境中,我們提供了三種部署方式來支撐業務:

2.1 三機房部署

 

用於滿足一致性和容災的要求,優點是任意一個機房發生機房級別故障,叢集仍然可以使用,適用於核心應用。但缺點也是比較明顯的,資料通過raft協議進行同步的時候,會遇到跨機房問題,效能會受到影響。

2.2 單機房部署

 

叢集所有節點都在一個機房中,節點之間通訊可以避免跨機房問題(應用端與服務端之間仍然會存在跨機房呼叫),由於機房整體出現問題時該部署模式的系統將無法使用,所以適用於非核心應用進行訪問。

2.3 藍綠雙活部署

在實際使用中,以上兩種常規部署方式並不能滿足一些業務方的需求,比如效能要求較高的核心應用,三機房的部署方式所帶來的網路損耗可能會超出預期。根據攜程酒店某個業務場景真實測試資料來看,本地三機房的部署方式延遲要比單機房高50%+,但單機房部署無法抵抗單個IDC故障,此外還有使用者希望能存在類似資料回滾的能力,以應對應用釋出,叢集版本升級可能導致的錯誤。

考慮到使用圖資料庫的業務大多資料來自離線系統,通過離線作業將資料匯入到圖資料庫中,資料一致的要求並不高,在這種條件下使用藍綠部署能夠在災備和效能上得到很好的滿足。

與此同時我們還增加了一些配套的輔助功能,比如:

  • 分流:可以按比例分配機房的訪問,也可以主動切斷對某個機房的流量訪問
  • 災備:在發生機房級故障時,可自動切換讀訪問的流量,寫訪問的流量切換則通過人工進行操作

藍綠雙活方式是在效能、可用性、一致性上的一個折中的選擇,使用此方案時應用端架構也需要有更多的調整以配合資料的存取。

生產上的一個例子:

 

三機房情況

 

藍綠部署

 

三、中介軟體及運維管理

我們基於k8s crd和operator來進行Nebula Graph的部署,同時通過服務整合到現有的部署配置頁面和運維管理頁面,來獲得對pod的執行和遷移的控制能力。基於sidecar模式監控收集Nebula Graph的核心指標並通過telegraf傳送到攜程自研的Hickwall集中展示,並設定告警等一系列相關工作。

此外我們集成了跨機房的域名分配功能,為節點自動分配域名用於內部訪問(域名只用於叢集內部,叢集與外部連通是通過ip直連的),這樣做是為了避免節點漂移造成ip變更,影響叢集的可用性。

在客戶端上,相比原生客戶端,我們主要做了以下幾個改進和優化:

3.1 Session管理功能

原生客戶端Session管理比較弱,尤其是2.x早期幾個版本,多執行緒訪問Session並不是執行緒安全的,Session過期或者失效都需要呼叫方來處理,不適合大規模使用。同時雖然官方客戶端建立的Session是可以複用的,並不需要release,官方也鼓勵使用者複用,但是卻沒有提供統一的Session管理功能來幫助使用者複用,因此我們增加了Session Pool的概念來實現複用。

其本質上是管理一個或多個Session Object Queue,通過borrow-and-return的方式(下圖),確保了一個Session在同一時間只會由一個執行器在使用,避免了共用Session產生的問題。同時通過對佇列的管理,我們可以進行Session數量和版本的管理,比如預生成一定量的Session,或者在管理中心發出訊息之後變更Session的數量或者訪問的路由。

 

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

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

3.3 流量分配

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

a)通過直連而不是Virtual IP中轉可以減少一次轉發的損耗

b)在維持長連線的同時也能實現每次請求使用不同的鏈路,平攤graphd的訪問壓力

c)完全自主控制路由,可以實現更為靈活的路由方案

d)當存在節點無法訪問的時候,客戶端可以自動臨時排除有問題的IP,在短時間內避免再次使用。而如果使用Virtual IP的話,由於一個Virtual IP會對應多個物理IP,就沒有辦法直接這樣操作。

通過構造面向不同idc的Session Pool,並根據配置進行權重輪詢,就可以達到按比例分配訪問流量的目的(下圖)。

 

將流量分配整合進藍綠模式,就基本實現了基本的客戶端改造(下圖)。

 

3.4 結構化語句查詢

圖DSL目前主流的有兩種,Gremlin和Cypher,前者是過程式語言而後者是宣告式語言。Nebula Graph支援了openCypher(Cypher的開源專案)語法和自己設計的nGQL原生語法,這兩種都是宣告式語言,在風格上比較類似SQL。儘管如此,對於一些較為簡單的語句,類似Gremlin風格的過程式語法對使用者會更為友好,並且有利用監控埋點。基於這個原因,我們封裝了一個過程式的語句生成器。

例如:

Cypher風格 MATCH (v:user{name:"XXX"})-[e:follow|:serve]->(v2) RETURN v2 AS Friends;
新增的過程式風格 Builder.match().vertex("v").hasTag("user").property("name", "XXX", DataType.String()).edge("e", Direction.OUTGOING).type("follow").type("serve").vertex("v2").ret("v2", "Friends")

 

四、系統調優實踐

由於建模,使用場景,業務需求的差異,使用Nebula Graph的過程中所遇到的問題很可能會完全不同,以下以攜程酒店資訊圖譜線上具體的例子進行說明,在整個落地過程我們遇到的問題及處理過程(文中以下內容是基於Nebula Graph 2.6.1進行的)。

關於酒店該業務的更多細節,可以閱讀《資訊圖譜在攜程酒店的應用》這篇文章。

4.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資訊是會在Nebula叢集內持久化的,所以考慮問題可能出在這裡。閱讀原始碼我們注意到,graphd會從metad中同步所有的session資訊,並在修改之後將資料再全部回寫到metad中,所以如果流量都是session資訊的話,那麼問題就可能:

a)Session沒有過期

b)建立了太多的Session

檢查發現該叢集沒有配置超時時間,所以我們修改以下配置來處理這個問題:

型別 配置項 原始值 修改值 說明
Graphd session_idle_timeout_secs 預設(0) 86400 此配置控制session的過期,由於初始我們沒有設定這個引數,這意味著session永遠不會過期,這會導致過去訪問過該graphd的session會永遠存在於metad儲存層,造成session元資料累積。
session_reclaim_interval_secs 預設(10) 30 原設定說明每10s graphd會將session資訊傳送給metad持久化。這也會導致寫入資料量過多。考慮到即使down機也只是損失部分的Session元資料更新,這些損失帶來的危害比較小,所以我們改成了30s以減少於metad之間同步元資料的次數。
Metad wal_ttl 預設(14400) 3600 wal用於記錄修改操作的,一般來說是不需要保留太久的,況且nebula graph為了安全,都至少會為每個分片保留最後2個wal檔案,所以減少ttl加快wal淘汰,將空間節約出來

修改之後,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導致超時

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

酒店業務方在增加訪問量的時候,每次到80%的時候叢集中就有少數storaged不穩定,cpu使用率突然暴漲,導致整個叢集響應增加,從而應用端產生大量超時報錯,如下圖所示:

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

1)嘗試通過Balance來分攤訪問壓力

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

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

但在執行一段時間之後仍然遇到了cpu突然增加的情況,稠密點顯然沒有被平衡掉,也說明在分片這個層面是沒法緩解稠密點帶來的訪問壓力的。

2)嘗試通過配置緩解鎖競爭

進一步調研出現問題的storaged的cpu的使用率,可以看到當流量增加的時候,核心佔用的cpu非常高,如下圖所示:

抓取perf看到,鎖競爭比較激烈,即使在“正常”情況下,鎖的佔比也很大,而在競爭激烈的時候,出問題的storaged服務上這個比例超過了50%。如下圖所示:

所以我們從減少衝突入手,對nebula graph叢集主要做了如下改動:

型別 配置項 原始值 修改值 說明
Storaged rocksdb_block_cache 預設(4) 8192 block cache用快取解壓縮之後的資料,cache越大,資料淘汰情況越低,這樣就越可能更快的命中資料,減少反覆從page cache載入及depress的操作
enable_rocksdb_prefix_filtering false true 在記憶體足夠的情況下,我們開啟prefix過濾,是希望通過其通過字首更快的定位到資料,減少查詢非必要的資料,減少資料競爭
RocksDB disable_auto_compactions 預設 false 開啟自動compaction,緩解因為資料碎片造成的查詢cpu升高
write_buffer_size 預設 134217728 將memtable設定為128MB,減少其flush的次數
max_background_compactions 預設 4 控制後臺compactions的執行緒數

重新上線之後,整個叢集服務變得比較平滑,cpu的負載也比較低,正常情況下鎖競爭也下降不少(下圖),酒店也成功的將流量推送到了100%。

但運行了一段時間之後,我們仍然遇到了服務響應突然變慢的情況,熱點訪問帶來的壓力的確超過了優化帶來的提升。

3)嘗試減小鎖的顆粒度

考慮到在分片級別的balance不起作用,而cpu的上升主要是因為鎖競爭造成的,那我們想到如果減小鎖的顆粒度,是不是就可以儘可能減小競爭?RocksDB的LRUCache允許調整shared數量,我們對此進行了修改:

版本 LRUCache預設分片數 方式
2.5.0 28 修改程式碼,將分片改成210
2.6.1及以上 28 通過配置cache_bucket_exp = 10,將分片數改為210

觀察下來效果不明顯,無法解決熱點競爭導致的雪崩問題。其本質同balance操作一樣,只是粒度的大小的區別,在熱點非常集中的情況下,在資料層面進行處理是走不通的。

4)嘗試使用ClockCache

競爭的鎖來源是block cache造成的。nebula 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), 此方案目前無法使用。

5)限制執行緒使用

可以看到整個系統在當前配置下,是存在非常多的執行緒的,如下圖所示。

 

如果是單執行緒,就必然不會存在鎖競爭。但作為一個圖服務,每次訪問幾乎會解析成多個執行器來併發訪問,強行改為單執行緒必然會造成訪問堆積。

所以我們考慮將原有的執行緒池中的程序調小,以避免太多的執行緒進行同步等待帶來的執行緒切換,以減小系統對cpu的佔用。

型別 配置項 原始值 修改值 說明
Storaged num_io_threads 預設(16) 4或者8  
num_worker_threads 預設(32) 4或者8  
reader_handlers 預設(32) 8或者12 官方未公開配置

調整之後整個系統cpu非常平穩,絕大部分物理機cpu在20%以內,且沒有之前遇到的突然上下大幅波動的情況(瞬時激烈鎖競爭會大幅度提升cpu的使用率),說明這個調整對當前業務來說是有一定效果的。

隨之又遇到了下列問題,前端服務突然發現nebula的訪問大幅度超時,而從系統監控的角度卻毫無波動(下圖24,19:53系統其實已經響應出現問題了,但cpu沒有任何波動)。

原因是在於,限制了thread 確實有效果,減少了競爭,但隨著壓力的正大,執行緒吞吐到達極限,但如果增加執行緒,資源的競爭又會加劇,無法找到平衡點。

6)關閉資料壓縮,關閉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(在Nebula Graph中,通過配置項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的使用,在這種情況下,由於沒有檔案資訊需要置換出去,演算法邏輯被關閉。

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

型別 配置項 原始值 修改值 說明
Storaged rocksdb_block_cache 8192 -1 關閉block cache
rocksdb_compression_per_level lz4 no:no:no:no:lz4:lz4:lz4 在L0~L3層關閉壓縮
  enable_partitioned_index_filter true false 避免將index和filter快取進block cache
RocksDB max_open_files 4096 -1 避免檔案被table cache淘汰,避免檔案描述符被關閉,加快檔案的讀取

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

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

4.3 資料寫入時服務down機

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

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

這是因為返回了報錯,但沒有error message,導致發生了空引用(空引用現象是客戶端未合理處理這種情況,也是我們客戶端的bug),但這種情況很奇怪,為什麼會沒有error message,檢查其trace日誌,發現這些請求執行nebula時間都很長,且存在非常大段的語句,如下圖所示:

 

預感是這些語句導致了graphd的down機,由於執行被切斷導致客戶端生成了一個null值。將這些語句進行重試,可以必現down機的場景。檢查這樣的請求發現其是由500條語句組成(業務方語句拼接上限500),並沒有超過配置設定的最大執行語句數量(512)。

看起來這是一個nebula官方的bug,我們已經將此問題提交給官方。同時業務方語句拼接限制從500降為200後順利避免該問題導致的down機。

 

五、Nebula Graph二次開發

當前我們對Nebula Graph的修改主要集中的幾個運維相關的環節上,比如新增了命令來指定遷移Storaged中的分片,以及將leader遷移到指定的例項上(下圖)。

 

六、未來規劃

  • 與攜程大資料平臺整合,充分利用Spark或者Flink來實現資料的傳輸和ETL,提高異構叢集間資料的遷移能力。
  • 提供Slowlog檢查功能,抓取造成slowlog的具體語句。
  • 引數化查詢功能,避免依賴注入。
  • 增強視覺化能力,增加定製化功能。

 

【作者簡介】

Patrick Yu,攜程雲原生研發專家,關注非關係型分散式資料儲存及相關技術。