Neo4j 匯入 Nebula Graph 的實踐總結

NebulaGraph發表於2021-02-02

摘要: 主要介紹如何通過官方 ETL 工具 Exchange 將業務線上資料從 Neo4j 直接匯入到 Nebula Graph 以及在匯入過程中遇到的問題和優化方法。

本文首發於 Nebula 論壇:https://discuss.nebula-graph.com.cn/t/topic/2044

Neo4j 匯入 Nebula Graph 的實踐總結

1 背景

隨著業務資料量不斷增長,業務對圖資料庫線上資料實時更新寫入和查詢的效率要求也不斷增加。Neo4j 存在明顯效能不足,Neo4j 社群開源版本只支援單機部署,擴充套件能力存在比較大的問題,無法滿足讀寫效能的線性擴充套件以及讀寫分離的業務需求,並且開源版本 Neo4j 對點和邊的總資料量也有限制;而 Neo4j 企業版因果叢集也存在單機主節點 Cypher 實時寫入的效能瓶頸。

相比於 Neo4j,Nebula Graph 最大的特色便是採用 shared-nothing 分散式的架構,無單主寫入瓶頸問題,讀寫支援線性擴充套件,擅長處理千億節點、萬億條邊的超大規模資料集。

本文主要介紹如何通過官方 ETL 工具 Exchange 將業務線上資料從 Neo4j 直接匯入到 Nebula Graph 以及在匯入過程中遇到的問題和優化方法。其中絕大部分問題都已經通過論壇發帖的方式得到社群的支援和解決,本文會結合問題進行逐一列舉。

2 部署環境

系統環境:

  • CPU name:Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz
  • CPU Cores:40
  • Memory Size:376 GB
  • Disk:HDD
  • System:CentOS Linux release 7.4.1708 (Core)

軟體環境:

  • Neo4j:3.4 版本,五節點因果叢集
  • Nebula Graph:
  • Exchange:nebula-java v1.1.0 原始碼編譯 jar 包
  • 數倉環境:
    • hadoop-2.7.4
    • spark-2.3.1

注意:單臺機器部署 Nebula 多節點的埠分配:每個 storage 還會將使用者配置的埠號 + 1的埠作為內部使用。請參考論壇帖子 nebula從neo4j匯入資料出現Get UUID Failed錯誤

3 全量 & 增量資料匯入

3.1 全量匯入

根據 Neo4j 點和邊的屬性資訊建立 Nebula Graph 的 Tag 和 Edge 結構,這裡需要注意一點,業務可能會根據不同需求只在部分點和邊上增加 Neo4j 點和邊的屬性資訊,其他點和邊對應的屬性為 NULL,所以需要先跟業務明確一下點和邊的全部屬性資訊,避免遺漏屬性。Nebula Graph 的 Schema 資訊類似 MySQL,支援 Create 和 Alter 新增屬性,並且所有的 Tag 和 Edge 的後設資料資訊是一致的。

1、Nebula Graph 建立 Tag 和 Edge

# 示例
# 建立圖空間,10 個分割槽,3 個 storage 副本。
CREATE SPACE test(partition_num=10,replica_factor=3);
# 選擇圖空間 test
USE test;
# 建立標籤 tagA
CREATE TAG tagA(vid string, field-a0 string, field-a1 bool, field-a2 double);
# 建立標籤 tagB
CREATE TAG tagB(vid string, field-b0 string, field-b1 bool, field-b2 double);
# 建立邊型別 edgeAB
CREATE EDGE edgeAB(vid string, field-e0 string, field-e1 bool, field-e2 double);

2、Exchange 匯入配置檔案

  • Exchange 配置目前不支援 bolt+routing 的方式連線neo4j,如果是因果叢集,可以選擇一個從節點進行 bolt 方式直連讀取資料,減少叢集壓力。
  • 我們業務的 Neo4j 資料點和邊的 vid 是 string 型別,Nebula v1.x 版本還不支援 string 直接當做 vid(v2.0支援),考慮到官方文件中的描述:“當點數量到達十億級別時,用 hash 函式生成 vid 有一定的衝突概率。因此 Nebula Graph 提供 UUID 函式來避免大量點時的 vid 衝突。” 選擇了uuid() 作為轉化函式,但是匯入效率要比 hash 低,而且 uuid() 在未來版本可能存在相容問題。
  • partition: 是指 Exchange 從 Neo4j 拉取資料的分頁個數。
  • batch: 是指批量插入 Nebula 的 batch 大小。
{
  # Spark relation config
  spark: {
    app: {
      name: Spark Writer
    }

    driver: {
      cores: 1
      maxResultSize: 1G
    }

    cores {
      max: 16
    }
  }

  # Nebula Graph relation config
  nebula: {
    address:{
      graph:["xxx.xxx.xxx.xx:3699"]
      meta:["xxx.xxx.xxx.xx:45500"]
    }
    user: user
    pswd: password
    space: test

    connection {
      timeout: 3000
      retry: 3
    }

    execution {
      retry: 3
    }

    error: {
      max: 32
      output: /tmp/errors
    }

    rate: {
      limit: 1024
      timeout: 1000
    }
  }
  
  # Processing tags
  tags: [
    # Loading tag from neo4j
    {
      name: tagA
      type: {
        source: neo4j
        sink: client
      }
      server: "bolt://xxx.xxx.xxx.xxx:7687"
      user: neo4j
      password: neo4j
      exec: "match (n:tagA) where id(n) < 300000000 return n.vid as vid, n.field-a0 as field-a0, n.field-a1 as field-a1, n.field-a2 as field-a2 order by id(n)"
      fields: [vid, field-a0, field-a1, field-a2]
      nebula.fields: [vid, field-a0, field-a1, field-a2]
      vertex: {
        field: vid
        policy: "uuid"
      }
      partition: 10
      batch: 1000
      check_point_path: /tmp/test
    }
    # Loading tag from neo4j
    {
      name: tagB
      type: {
        source: neo4j
        sink: client
      }
      server: "bolt://xxx.xxx.xxx.xxx:7687"
      user: neo4j
      password: neo4j
      exec: "match (n:tagB) where id(n) < 300000000 return n.vid as vid, n.field-b0 as field-b0, n.field-b1 as field-b1, n.field-b2 as field-b2 order by id(n)"
      fields: [vid, field-b0, field-b1, field-b2]
      nebula.fields: [vid, field-b0, field-b1, field-b2]
      vertex: {
        field: vid
        policy: "uuid"
      }
      partition: 10
      batch: 1000
      check_point_path: /tmp/test
    }
  ]

  # Processing edges
  edges: [
   # Loading edges from neo4j
    {
      name: edgeAB
      type: {
        source: neo4j
        sink: client
      }
      server: "bolt://xxx.xxx.xxx.xxx:7687"
      user: neo4j
      password: neo4j
      exec: "match (a:tagA)-[r:edgeAB]->(b:tagB) where id(r) < 300000000 return n.vid as vid, n.field-e0 as field-e0, n.field-e1 as field-e1, n.field-e2 as field-e2 order by id(r)"
      fields: [vid, field-e0, field-e1, field-e2]
      nebula.fields: [vid, field-e0, field-e1, field-e2]
      source: {
        field: a.vid
        policy: "uuid"
      }
      target: {
        field: b.vid
        policy: "uuid"
      }
      partition: 10
      batch: 1000
      check_point_path: /tmp/test
    }
  ]
}

3、執行匯入命令

nohup spark-submit --class com.vesoft.nebula.tools.importer.Exchange --master "local" exchange-1.1.0.jar -c test.conf > test.log &

4、檢視匯入 Nebula Graph 的資料量

./bin/db_dump --space=test --db_path=./data/storage/nebula/ --meta_server=127.0.0.1:45500 -limit 0 --mode=stat --tags=tagA,tagB --edges=edgeAB

注意:Nebula 1.x 版本目前還只能用 db_dump 統計,2.0 會支援 nGQL 命令的方式統計數量。

3.2 增量匯入

增量資料匯入主要是通過 Neo4j 內部點和邊的自增 id() 進行切割,在匯入配置檔案 exec 項執行 Neo4j Cypher 語句時增加 id() 範圍限制,但前提是需要業務停掉刪資料操作,因為增量匯入時,如果之前的資料被刪除後 Neo4j 會複用 id(),這會導致複用 id() 的增量資料匯入時查詢不到造成資料丟失。當然業務如果有條件支援 Neo4j Nebula 雙寫的話,增量匯入就不會出現這種問題

exec: "match (n:user) where id(n) >= 300000000 and id(n) < 400000000 return xxx order by id(n)"

請參考論壇帖子 neo4j到nebula如何做增量匯入

3.3 匯入問題及解決

使用 Exchange 匯入過程中遇到兩個問題,及時的得到官方 @nicole 的支援和解決,具體請參考下面兩個帖子:

問題 1:Exchange 不支援「換行回車」等特殊字元的轉義。如下 string 資料中帶有回車,在拼接 insert 語句插入時會因為換行導致插入失敗。

Neo4j 匯入 Nebula Graph 的實踐總結

PR:https://github.com/vesoft-inc/nebula-java/pull/203 已經合入 exchange v1.0 分支

問題 2:Exchange 不支援屬性為 NULL 的資料匯入。前文 3.1 中提到,業務可能會根據不同需求為某些點和邊增加屬性,這時其他點和邊屬性則是 NULL,這樣在使用 Exchange 匯入時會報錯。

Neo4j 匯入 Nebula Graph 的實踐總結

參考帖子 2 給出的修改建議解決:修改 com.vesoft.nebula.tools.importer.processor.Processor#extraValue,增加 NULL 型別的轉化值。

case NullType => {
  fieldTypeMap(field) match {
    case StringType => ""
    case IntegerType => 0
    case LongType => 0L
    case DoubleType => 0.0
    case BooleanType => false
  }
}

4 匯入效率優化

關於匯入效率的優化,請參考下面兩個帖子:

優化 1:通過適當增加匯入配置中的 partition 和 batch 值,提升匯入效率。
優化 2:如果是 string 型別做 vid 的話,1.x 版本儘量使用 hash() 函式轉化,2.0 版本會支援 string id 型別;如果是int型別做vid的話,可以直接使用,不用轉化效率更高。
優化 3:官方建議 spark-submit 提交命令 master 配置改為 yarn-cluster, 若不使用 yarn,可配置成 spark://ip:port;我們是通過 spark-submit --master "local[16]" 的方式增加 spark 併發,匯入效率比使用 "local" 提升 4 倍+,測試環境單機三節點 HDD 盤 IO 峰值能到 200-300 MB/s。但在指定 --master "local[16]" 併發匯入時遇到 hadoop 快取問題,採用增加 hdfs 配置 fs.hdfs.impl.disable.cache=true 後重啟 hadoop 解決。具體請參考第二個帖子。

5 總結

使用 Exchange 從 Neo4j 匯入 Nebula Graph 過程中遇到一些問題,通過積極與社群進行溝通得到了官方 @nicole 及其他小夥伴的快速響應和大力支援,這一點在 Neo4j 匯入 Nebula Graph 的實踐過程中起到了十分關鍵的作用,感謝社群的大力支援。期待支援 openCypher 的 Nebula Graph 2.0。

6 參考連結

  1. https://nebula-graph.com.cn/posts/how-to-import-data-from-neo4j-to-nebula-graph/
  2. https://github.com/vesoft-inc/nebula-java/tree/v1.0
  3. https://docs.nebula-graph.com.cn/manual-CN/2.query-language/2.functions-and-operators/uuid/
  4. http://arganzheng.life/hadoop-filesystem-closed-exception.html

推薦閱讀

相關文章