百億級圖數(shù)據(jù) JanusGraph 遷移之旅
1. 遷移背景介紹
目前我們的圖數(shù)據(jù)庫數(shù)據(jù)量為 頂點 20 億,邊 200 億的規(guī)模嘉蕾。在遷移之前我們使用的 AgensGraph 數(shù)據(jù)庫
一個主庫四個備庫贺奠,機器的配置都比較高,256G 內(nèi)存 SSD 的磁盤错忱,單機數(shù)據(jù)量為 3T左右儡率。
在數(shù)據(jù)量比較小的情況下 AgensGraph 表現(xiàn)非常穩(wěn)定優(yōu)異,我們之前一主一備的情況下支撐了很長一段時間航背。
但隨著公司業(yè)務(wù)的急速發(fā)展喉悴,圖越來越大棱貌,占用的磁盤越來越多玖媚,對應(yīng)的查詢量也越來越大,隨之這種方案的問題就暴露出來了
- 單機的磁盤空間不夠婚脱,按理說可以一直添加磁盤今魔,但現(xiàn)實情況有很多限制
- AgensGraph 不是分布式結(jié)構(gòu),每次查詢都需要單機處理障贸,單臺機器的處理能力總是有上限的错森,導(dǎo)致查詢耗時增加
- 隨著查詢量的增加,機器磁盤篮洁,網(wǎng)絡(luò) IO 出現(xiàn)瓶頸涩维。按理說可以通過增加備庫來解決,但備庫要求高導(dǎo)致成本增高袁波,并且數(shù)據(jù)冗余嚴重
由于上面的原因?qū)е?AgensGraph 沒辦法繼續(xù)支撐業(yè)務(wù)高速發(fā)展帶來的性能要求瓦阐。AgensGraph 底層基于 PostgreSQL 數(shù)據(jù)庫使它在小數(shù)據(jù)量的情況下非常的穩(wěn)定并且查詢響應(yīng)非常的迅速,在此感謝 AgensGraph 陪我們度過業(yè)務(wù)快速成長階段篷牌。
為了尋找新的圖數(shù)據(jù)庫我們把目光投向了接受度和知名度都比較高的 JanusGraph睡蟋。當然還有收費的圖數(shù)據(jù)庫 TigerGraph,暫時不做考慮
在此貼一張我們圖的應(yīng)用場景枷颊,查詢用戶之間的關(guān)系
由于這不是一篇介紹 JanusGraph 文章戳杀,在此不對 JanusGraph 做過多的介紹该面,大家可以自行了解。這里主要列舉下它的優(yōu)點:
- 分布式圖數(shù)據(jù)庫信卡,支持水平拓展
- 底層存儲基于 Hbase/Cassandra 隔缀,技術(shù)成熟
- 支持 OLAP 對圖進行批量處理,豐富圖的功能
- 支持 TinkerPop Gremlin 查詢語句傍菇,能滿足更復(fù)雜的業(yè)務(wù)查詢需求
2. 數(shù)據(jù)導(dǎo)入方案探索
簡單介紹完 JanusGraph 的優(yōu)點蚕泽,就正式開始遷移數(shù)據(jù)了。不得不說我們嚴重低估的數(shù)據(jù)的遷移難度桥嗤,之前預(yù)估大概兩周就能搞定须妻,結(jié)果花了快兩個月的時間。
方案一:利用 GremlimServer 批量插入
我們最開始采用的數(shù)據(jù)導(dǎo)入方式是連接 GremlinServer 批量插入頂點泛领,然后再插入邊荒吏,在插入邊的同時需要檢索到關(guān)聯(lián)的頂點。
批量插入的優(yōu)化方案主要參考下面這篇 blog 渊鞋。批量插入頂點的時候還是比較慢 20億頂點花了一周才搞定绰更。這里說明下,我們底層存儲用的是 HBase 集群锡宋,80多臺機器儡湾。為了加快導(dǎo)入的速度我們的插入程序是用Spark 編寫的,導(dǎo)入數(shù)據(jù)存放在 HDFS 集群上徐钠。值得注意的地方是數(shù)據(jù)寫入需要使用同步方式尝丐,異步很快就會把GremlinServer 內(nèi)存寫滿爹袁,然后出現(xiàn)連接異常。
導(dǎo)入完頂點導(dǎo)入邊的時候才發(fā)現(xiàn)邊的導(dǎo)入非常的慢盹兢,按照當時的導(dǎo)入速度計算 200 億邊預(yù)計需要 3個月的時間才能導(dǎo)入完成蛤迎,這種速度是不能接受的替裆。
插入邊比較慢辆童,最主要的原因是每插入一條邊都需要檢索兩個頂點故黑。社區(qū)里面建議是維持 name 索引到頂點id的一個 map 存放到內(nèi)存中场晶,我們沒試過诗轻,主要感覺有兩方面問題,第一20億點的需要不少內(nèi)存恨樟,其次因為我們頂點是批量插入的劝术,構(gòu)建這個 map 不是很方便,于是就放棄了這個方案。
方案二:生成 Cassandra SSTable 文件
只能嘗試其他方案陈轿,嘗試過網(wǎng)上生成 Cassandra SSTable 文件的方式導(dǎo)入數(shù)據(jù),最后在建立索引的時候有問題潜秋,聯(lián)系上原作者說不建議這種方式罗售,說代碼有bug數(shù)據(jù)有丟失寨躁。也只能放棄這種方案
方案三:生成 HBase Hfile 文件
想過自己寫程序生成 HBase Hfile的形式快速導(dǎo)入數(shù)據(jù),最大的困難是 JanusGraph 對 Hbase 表結(jié)構(gòu)的介紹文檔基本找不到放钦,只能看源代碼最筒,這個在短時間內(nèi)是比較難的。我們這邊時間也不允許邢锯, AgensGraph 的磁盤很快就滿了丹擎,查詢壓力也越來越大蒂培。另外這個也需要對 Hbase 有深入了解,團隊中缺少這樣的技術(shù)專家媳荒,大家都停留在使用層面。所以這個方案最終也選擇放棄
最終方案:bulkLoader 方式
最終還是把目光放到了JanusGraph 官方提供的 bulkLoader 方式鱼炒。其實最開始想到的就是這個方案指蚁,但是這個方案對導(dǎo)入的數(shù)據(jù)有非常嚴格的要求欣舵,它需要每個頂點一行數(shù)據(jù),再把這個頂點關(guān)聯(lián)的所有邊都關(guān)聯(lián)到這一行糟把,中間用 tab 分隔,第一部分是頂點的屬性,第二部分是頂點的入邊辨液,第三部分是頂點的出邊。當時一看到這種結(jié)構(gòu)就很頭大燎悍,我們的頂點有3榜揖,4種關(guān)系举哟,處理成這種格式感覺不可能,完全不知道怎么處理威兜。以下就是 JanusGraph 官方提供的例子大家感受下
179,song,COSMIC CHARLIE,,0 followedBy,130,1|followedBy,82,1|followedBy,76,1|followedBy,101,1|followedBy,3,1|followedBy,25,1|followedBy,215,1 followedBy,178,1|followedBy,148,1|followedBy,76,1|followedBy,110,3|followedBy,101,1
和團隊成員討論,看有什么辦法能轉(zhuǎn)換成這種格式笔宿,同事提醒說 Spark 有個 cogroup 操作應(yīng)該可以達到這個目的迈勋。深入了解之后發(fā)現(xiàn)這就是我們要找的重归,cogroup 的核心思想就是將多個 RDD 根據(jù)相同的 key 可以 jion成一行,下面是個簡單的例子
val rdd1 = sc.parallelize(Array(("aa",1),("bb",2),("cc",6)))
val rdd2 = sc.parallelize(Array(("aa",3),("dd",4),("aa",5)))
rdd1.cogroup(rdd2).collect()
output:
(aa,(CompactBuffer(1),CompactBuffer(3, 5)))
(dd,(CompactBuffer(),CompactBuffer(4)))
(bb,(CompactBuffer(2),CompactBuffer()))
(cc,(CompactBuffer(6),CompactBuffer()))
我們轉(zhuǎn)換為目標導(dǎo)入格式的代碼已經(jīng)放到 github上了笨腥,大家可以參考士鸥,沒有封裝成通用的工具,大家借鑒按自己的數(shù)據(jù)結(jié)構(gòu)進行修改。
3. 數(shù)據(jù)導(dǎo)入過程
接下來就是按需要的格式生成導(dǎo)入數(shù)據(jù),這中間有個值得注意的地方就是確保頂點 ID 的唯一性窥突,確保數(shù)據(jù)沒有重復(fù)称近,不然會導(dǎo)入失敗煌茬。
我們還是低估了這種 bulkLoader 導(dǎo)入數(shù)據(jù)的難度眠屎,導(dǎo)入花了比較長的時間驯镊,最主要的問題分為兩部分冯乘,一部分是 Hbase 相關(guān)參數(shù)調(diào)整的問題姊氓,另外一部分是 Spark 任務(wù)的內(nèi)存優(yōu)化問題。最痛苦的還是這種 bulkLoader 導(dǎo)入方式如果過程中出現(xiàn)問題喷好,失敗了翔横,只能將數(shù)據(jù)清理掉重新導(dǎo)入。
先說 Hbase 參數(shù)相關(guān)的問題梗搅,JanusGraph 導(dǎo)入的過程中會往Hbase中寫入大量數(shù)據(jù)棕孙,這個時候 Hbase 會有很多的異常情況出現(xiàn)。
下列參數(shù)就是導(dǎo)入過程中和 Hbase 相關(guān)的參數(shù)些膨,這些參數(shù)都是從一次次失敗中提煉總結(jié)出來的蟀俊。當然這些參數(shù)都是根據(jù)我們自己的環(huán)境設(shè)置的,大家應(yīng)該做相應(yīng)調(diào)整
# 這個參數(shù)批量導(dǎo)入需要設(shè)置
storage.batch-loading=true
# 這個參數(shù) 經(jīng)過調(diào)試订雾,這個值比較合理
ids.block-size=20000000
ids.renew-timeout=3600000
storage.buffer-size=20240
# 使插入數(shù)據(jù)更 robust
storage.read-attempts=100
storage.write-attempts=100
storage.attempt-wait=1000
# 分區(qū)數(shù)肢预,最好設(shè)置為 hbase 機器數(shù) 的2到 3倍
storage.hbase.region-count = 150
# hbase 超時時間,這個非常重要洼哎,不然導(dǎo)入會因為超時報錯
# 需要hbase 服務(wù)器端同步設(shè)置烫映,取客服端和服務(wù)器端的最小值
# 這些參數(shù)的只是是 看 janusgraph 源碼才發(fā)現(xiàn)可以設(shè)置的
storage.hbase.ext.hbase.rpc.timeout = 300000
storage.hbase.ext.hbase.client.operation.timeout = 300000
storage.hbase.ext.hbase.client.scanner.timeout.period = 300000
再來說導(dǎo)入過程中 Spark 相關(guān)的問題。JanusGraph 官方集成 Spark的時候只提供了單機模式和 standalone cluster 模式的配置方式噩峦,沒有提供如何集成 Spark on Yarn 的文檔锭沟。這就導(dǎo)致一個問題,我們是有 Spark on Yarn 環(huán)境的并且集群性能和資源都很好∈恫梗現(xiàn)在利用不上這部分資源需要重新申請機器再搭建一個 standalone cluster 的 Spark 集群族淮。好在我們當時有部分機器空閑搭建了 standalone cluster 集群。并且我們也通過其他同事的努力解決了 JanusGraph 如何集成 Spark on Yarn
說回 Spark 導(dǎo)入過程中相關(guān)的問題凭涂,最主要的問題就是如何平衡 executor 內(nèi)存和并行度的問題祝辣。executor 內(nèi)存配置的小能夠增加并行度但是會出現(xiàn) OutOfMemoryError,如果把內(nèi)存調(diào)整的很大并行度又下來了切油,導(dǎo)入時間會很長蝙斜,不確定性增加。另一個問題就是如果并行度過高 Hbase 集群能否支撐的住澎胡。最終需要在這些問題中找到平衡孕荠。
下面是我們生產(chǎn)環(huán)境配置的 Spark 相關(guān)參數(shù)
spark.network.timeout=7600
spark.master=yarn
spark.deploy-mode=client
# spark.executor.memory/spark.executor.cores 保障在8g左右,具體看數(shù)據(jù)量
spark.executor.memory=20g
spark.executor.cores=2
spark.yarn.queue=root.graph
spark.executor.instances=70
spark.executor.extraJavaOptions=-XX:+UseG1GC
spark.shuffle.io.retryWait=120s
spark.serializer=org.apache.spark.serializer.KryoSerializer
spark.kryo.registrator=org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoRegistrator
gremlin.spark.graphStorageLevel=MEMORY_AND_DISK
gremlin.spark.persistContext=true
gremlin.spark.graphWriter=org.apache.tinkerpop.gremlin.spark.structure.io.PersistedOutputRDD
gremlin.spark.persistStorageLevel=DISK_ONLY
以上相關(guān)的參數(shù)在我上面提到的 github倉庫中都有做相關(guān)說明攻谁,大家可以根據(jù)自己的情況自行做相應(yīng)調(diào)整剪返。
4.JanusGraph 查詢優(yōu)化
本來以為經(jīng)歷完漫長的數(shù)據(jù)導(dǎo)入過程从橘,后面會順利很多,但是現(xiàn)實和期望還是有差距。問題是 JanusGraph 在大數(shù)據(jù)量情況下掀亩,查詢性能達不到生產(chǎn)要求,查詢需要幾十秒妒挎。相同的功能在 AgensGraph 查詢都是秒級腰埂。
好在 JanusGraph 查詢語句都可以用 profile 功能進行分析調(diào)試,通過分析的結(jié)果能明確知道那些地方有性能問題那槽。
經(jīng)過分析發(fā)現(xiàn)慢的最主要的原因就是 JanusGraph 獲取頂點屬性特別慢悼沿,默認居然不是并行獲取而是逐條獲取。我們的應(yīng)用場景屬性都是放到頂點上骚灸,例如:如果我要查詢一個用戶的通話關(guān)系糟趾,但是需要過濾只要相關(guān)注冊用戶,查詢語句像下面這樣
g.V().has("name","138xxxx4444").both("CALL").has("is_register","true")
上面的查詢語句假設(shè)這個用戶和 1000 個人有通話關(guān)系,但是我只關(guān)心和他相關(guān)的注冊用戶 100 人义郑。JanusGraph 默認的做法是逐條獲取這個1000 個用戶的所有屬性蝶柿,再在內(nèi)存中做過濾最后獲得這 100 個用戶,這就導(dǎo)致關(guān)聯(lián)的頂點數(shù)量比較大的時候非驮,直接不可用交汤。
好在 JanusGraph 在最新的 0.4 版本中提供了一個 _multiPreFetch
的優(yōu)化功能,能在屬性過濾的時候批量并行獲取所有關(guān)聯(lián)頂點的屬性劫笙,再在內(nèi)存做屬性過濾芙扎,關(guān)于這個功能的詳細介紹可以看這里。個人感覺在沒有這個優(yōu)化功能的情況下 JanusGraph 基本不具備在生產(chǎn)環(huán)境使用的條件填大。并且這個功能并不是很完善戒洼,當你的過濾條件是 hasNot, 或者返回邊的屬性允华,或者語句后有 limit 操作都會使這個優(yōu)化失效圈浇。而你能做的只能是想盡辦法繞開,例如:has("is_exception", neq("true"))
另一個問題就是 JanusGraph 查詢的數(shù)據(jù)如何返回的問題例获,Gremlin 返回數(shù)據(jù)支持多種寫法汉额。最常用的就是使用 valueMap 的方式,但是這里面有兩個比較大的坑榨汤,第一個是返回的屬性值默認是list類型蠕搜,第二個是如果返回結(jié)果使用多個 valueMap 導(dǎo)致特別消耗內(nèi)存。
這兩個問題好在都能找到解決方法收壕,詳細情況不在這里做過多說明請參考這里妓灌。這些問題JanusGraph 都沒有做很好的說明,并且默認也沒做規(guī)避蜜宪,都是一次次痛苦的經(jīng)歷得出來的經(jīng)驗虫埂,所以說 JanusGraph目前還不是特別成熟。
5.未來
雖然經(jīng)過上面的優(yōu)化圃验,我們發(fā)現(xiàn)在數(shù)據(jù)量比較大的情況下掉伏,查詢還是比較慢。經(jīng)過分析發(fā)現(xiàn)主要從 Hbase 獲取大量數(shù)據(jù)比較慢澳窑。分析 Hbase Region Server 的負載情況斧散,發(fā)現(xiàn)磁盤IO 負載比較高。所以我們下一步的策略是搭建 一套基于 SSD 磁盤的 Hbase 集群來加速查詢性能摊聋。
同時也期待 JanusGraph 開源社區(qū)的快速發(fā)展鸡捐,為大家提供性能更高效的圖數(shù)據(jù)庫。同時也希望我們自己能對 JanusGraph 做一些優(yōu)化并且回饋社區(qū)麻裁。希望大家一起為 JanusGraph 圖數(shù)據(jù)庫社區(qū)的發(fā)展助力