本文首發於 Nebula Graph Community 公眾號
一、專案背景
微瀾是一款用於查詢技術、行業、企業、科研機構、學科及其關係的知識圖譜應用,其中包含著百億級的關係和數十億級的實體,為了使這套業務能夠完美執行起來,經過調研,我們使用 Nebula Graph 作為承載我們知識圖譜業務的主要資料庫,隨著 Nebula Graph 的產品迭代,我們最終選擇使用 v2.5.1 版本的 Nebula Graph 作為最終版本。
二、為什麼選擇 Nebula Graph?
在開源圖資料庫領域,無疑存在著很多選擇,但為了支撐如此大規模資料的知識圖譜服務,Nebula Graph 對比其他的圖資料庫具有以下幾個優點,這也是我們選擇 Nebula Graph 的原因:
- 對於記憶體的佔用較小
在我們的業務場景下,我們的 QPS 比較低且沒有很高的波動,同時相比起其他的圖資料庫,Nebula Graph 具有更小的閒時記憶體佔用,所以我們可以通過使用記憶體配置更低的機器去執行 Nebula Graph 服務,這無疑為我們節省了成本。
- 使用 multi-raft 一致性協議
multi-raft 相比於傳統的 raft,不僅增加了系統的可用性,而且效能比傳統的 raft 要高。共識演算法的效能主要在於其是否允許空洞和粒度切分,在應用層無論 KV 資料庫還是 SQL ,能成功利用好這兩個特性,效能肯定不會差。由於 raft 的序列提交極其依賴狀態機的效能,這樣就導致即使在 KV 上,一個 key 的 op 慢,顯著會拖慢其他 key。所以,一個一致性協議的效能高低的關鍵,一定是在於狀態機如何讓可以並行地儘量並行,縱使 multi-raft 的粒度切分比較粗(相比於 Paxos),但對於不允許空洞的 raft 協議來說,還是有巨大的提升。
- 儲存端使用 RocksDB 作為儲存引擎
RocksDB 作為一款儲存引擎/嵌入式資料庫,在各種資料庫中作為儲存端得到了廣泛地使用。更關鍵的是 Nebula Graph 可以通過調整 RocksDB 的原生引數來改善資料庫效能。
- 寫入速度快
我們的業務需要頻繁地大量寫入,Nebula Graph 即使在具有大量長文字內容的 vertex 的情況下(叢集內3 臺機器、3 份資料,16 執行緒插入)插入速度也能達到 2 萬/s 的插入速度,而無屬性邊的插入速度在相同條件下可以達到 35 萬/s。
三、使用 Nebula Graph 時我們遇到了哪些問題?
在我們的知識圖譜業務中,很多場景需要向使用者展示經過分頁的一度關係,同時我們的資料中存在一些超級節點,但根據我們的業務場景,超級節點一定會是使用者訪問可能性最高的節點,所以這不能被簡單歸類到長尾問題上;又因為我們的使用者量並不大,所以快取必然不會經常被撞到,我們需要一套解決方案來使使用者的查詢延遲更小。
舉例:業務場景為查詢這個技術的下游技術,同時要根據我們設定的排序鍵進行排序,此排序鍵是區域性排序鍵。比如,某個機構在某一領域排名特別高,但是在全域性或者其他領域比較一般,這種場景下我們必須把排序屬性設定在邊上,並且對於全域性排序項進行擬合與標準化,使得每個維度的資料的方差都為 1,均值都為 0,以便進行區域性的排序,同時還要支援分頁操作方便使用者查詢。
語句如下:
MATCH (v1:technology)-[e:technologyLeaf]->(v2:technology) WHERE id(v2) == "foobar" \
RETURN id(v1), v1.name, e.sort_value AS sort ORDER BY sort | LIMIT 0,20;
此節點有 13 萬鄰接邊,這種情況下即使對 sort_value 屬性加了索引,查詢耗時還是將近兩秒。這個速度顯然無法接受。
我們最後選擇使用螞蟻金服開源的 OceanBase 資料庫來輔助我們實現業務,資料模型如下:
technologydownstream
technology_id | downstream_id | sort_value |
---|---|---|
foobar | id1 | 1.0 |
foobar | id2 | 0.5 |
foobar | id3 | 0.0 |
technology
id | name | sort_value |
---|---|---|
id1 | aaa | 0.3 |
id2 | bbb | 0.2 |
id3 | ccc | 0.1 |
查詢語句如下:
SELECT technology.name FROM technology INNER JOIN (SELECT technologydownstream.downstream_id FROM technologydownstream
WHERE technologydownstream.technology_id = 'foobar' ORDER BY technologydownstream.sort_value DESC LIMIT 0,20) AS t
WHERE t.downstream_id=technology.id;
此語句耗時 80 毫秒。這裡是整個架構設計
四、使用 Nebula Graph 時我們如何調優?
前面講過 Nebula Graph 的一個很大的優勢就是可以使用原生 RocksDB 引數進行調優,減少學習成本,關於調優項的具體含義以及部分調優策略我們分享如下:
RocksDB 引數 | 含義 |
---|---|
max_total_wal_size | 一旦 wal 的檔案超過了 max_total_wal_size 會強制建立新的 wal 檔案,預設值為 0時,max_total_wal_size = write_buffer_size max_write_buffer_number 4 |
delete_obsolete_files_period_micros | 刪除過期檔案的週期,過期的檔案包含 sst 檔案和 wal 檔案, 預設是 6 小時 |
max_background_jobs | 最大的後臺執行緒數目 = max_background_flushes + max_background_compactions |
stats_dump_period_sec | 如果非空,則每隔 stats_dump_period_sec 秒會列印 rocksdb.stats 資訊到 LOG 檔案 |
compaction_readahead_size | 壓縮過程中預讀取硬碟的資料量。如果在非 SSD 磁碟上執行 RocksDB,為了效能考慮則應將其設定為至少 2 MB。如果是非零,同時會強制new_table_reader_for_compaction_inputs=true |
writable_file_max_buffer_size | WritableFileWriter 使用的最大緩衝區大小 RocksDB 的寫快取,對於 Direct IO 模式的話,調優該引數很重要。 |
bytes_per_sync | 每次sync的資料量,累計到 bytes_per_sync 會主動 Flush 到磁碟,這個選項是應用到 sst 檔案,wal 檔案使用 wal_bytes_per_sync |
wal_bytes_per_sync | wal 檔案每次寫滿 wal_bytes_per_sync 檔案大小時,會通過呼叫 sync_file_range 來重新整理檔案,預設值為 0 表示不生效 |
delayed_write_rate | 如果發生 Write Stall, 寫入的速度將被限制在 delayed_write_rate 以下 |
avoid_flush_during_shutdown | 預設情況下,DB 關閉時會重新整理所有的 memtable,如果設定了該選項那麼將不會強制重新整理,可能造成資料丟失 |
max_open_files | RocksDB 可以開啟檔案的控制程式碼數量(主要是 sst檔案),這樣下次訪問的時候就可以直接使用,而不需要重新在開啟。當快取的檔案控制程式碼超過 max_open_files 之後,一些控制程式碼就會被 close 掉,要注意控制程式碼 close 的時候相應 sst 的 index cache 和 filter cache 也會一起釋放掉,因為 index block 和 filter block 快取在堆上,數量上限由 max_open_files 選項控制。依據 sst 檔案的 index_block 的組織方式判斷,一般來說 index_block 比 data_block 大 1 到 2 個數量級,所以每次讀取資料必須要先載入 index_block,此時 index 資料放在堆上,並不會主動淘汰資料;如果大量的隨機讀的話,會導致嚴重的讀放大,另外可能導致 RocksDB 不明原因的佔據大量的實體記憶體,所以此值的調整非常重要,需要根據自己的 workload 在效能和記憶體佔用上做取捨。如果此值為 -1,RocksDB 將一直快取所有開啟的控制程式碼,但這個會造成比較大量的記憶體開銷 |
stats_persist_period_sec | 如果非空,則每隔 stats_persist_period_sec 自動將統計資訊儲存到隱藏列族___ rocksdb_stats_history___。 |
stats_history_buffer_size | 如果不為零,則定期獲取統計資訊快照並儲存在記憶體中,統計資訊快照的記憶體大小上限為 stats_history_buffer_size |
strict_bytes_per_sync | RocksDB 把資料寫入到硬碟時為了效能考慮,預設沒有同步 Flush,因此異常情況下存在丟失資料的可能,為了對丟失資料數量的可控,需要一些引數來設定重新整理的動作。如果此引數為 true,那麼 RocksDB 將嚴格的按照 wal_bytes_per_sync 和 bytes_per_sync 的設定刷盤,即每次都重新整理完整的一個檔案,如果此引數為 false 則每次只重新整理部分資料:也就是說如果對可能的資料丟失不怎麼 care,就可以設定 false,不過還是推薦為 true |
enable_rocksdb_prefix_filtering | 是否開啟 prefix_bloom_filter,開了之後會根據寫入 key 的前 rocksdb_filtering_prefix_length 位在 memtable 構造 bloom filter |
enable_rocksdb_whole_key_filtering | 在 memtable 建立 bloomfilter,其中對映的 key 是 memtable 的完整 key 名,所以這個配置和 enable_rocksdb_prefix_filtering 衝突,如果 enable_rocksdb_prefix_filtering 為 true,則這個配置不生效 |
rocksdb_filtering_prefix_length | 見 enable_rocksdb_prefix_filtering |
num_compaction_threads | 後臺併發 compaction 執行緒的最大數量,實際是執行緒池的最大執行緒數,compaction 的執行緒池預設為低優先順序 |
rate_limit | 用於記錄在程式碼裡通過 NewGenericRateLimiter 建立速率控制器的引數,這樣重啟的時候可以通過這些引數構建 rate_limiter。rate_limiter 是 RocksDB 用來控制 Compaction 和 Flush 寫入速率的工具,因為過快的寫會影響資料的讀取,我們可以這樣設定:rate_limit = {"id":"GenericRateLimiter"; "mode":"kWritesOnly"; "clock":"PosixClock"; "rate_bytes_per_sec":"200"; "fairness":"10"; "refill_period_us":"1000000"; "auto_tuned":"false";} |
write_buffer_size | memtable 的最大 size,如果超過了這個值,RocksDB 就會將其變成 immutable memtable,並建立另一個新的 memtable |
max_write_buffer_number | 最大 memtable 的個數,包含 mem 和 imm。如果滿了,RocksDB 就會停止後續的寫入,通常這都是寫入太快但是 Flush 不及時造成的 |
level0_file_num_compaction_trigger | Leveled Compaction 專用觸發引數,當 L0 的檔案數量達到 level0_file_num_compaction_trigger 的值時,則觸發 L0 和 L1 的合併。此值越大,寫放大越小,讀放大越大。當此值很大時,則接近 Universal Compaction 狀態 |
level0_slowdown_writes_trigger | 當 level0 的檔案數大於該值,會降低寫入速度。調整此引數與level0_stop_writes_trigger 引數是為了解決過多的 L0 檔案導致的 Write Stall 問題 |
level0_stop_writes_trigger | 當 level0 的檔案數大於該值,會拒絕寫入。調整此引數與level0_slowdown_writes_trigger 引數是為了解決過多的 L0 檔案導致的 Write Stall 問題 |
target_file_size_base | L1 檔案的 SST 大小。增加此值會減少整個 DB 的 size,如需調整可以使target_file_size_base = max_bytes_for_level_base / 10,也就是 level 1 會有 10 個 SST 檔案即可 |
target_file_size_multiplier | 使得 L1 上層(L2...L6)的檔案的 SST 的 size 都會比當前層大 target_file_size_multiplier 倍 |
max_bytes_for_level_base | L1 層的最大容量(所有 SST 檔案大小之和),超過該容量就會觸發 Compaction |
max_bytes_for_level_multiplier | 每一層相比上一層的檔案總大小的遞增引數 |
disable_auto_compactions | 是否禁用自動 Compaction |
交流圖資料庫技術?加入 Nebula 交流群請先填寫下你的 Nebula 名片,Nebula 小助手會拉你進群~~