DataLeap數(shù)據(jù)資產(chǎn)實戰(zhàn):如何實現(xiàn)存儲優(yōu)化?

背景
- DataLeap 作為一站式數(shù)據(jù)中臺套件,匯集了字節(jié)內(nèi)部多年積累的數(shù)據(jù)集成、開發(fā)、運維、治理、資產(chǎn)、安全等全套數(shù)據(jù)中臺建設的經(jīng)驗,助力企業(yè)客戶提升數(shù)據(jù)研發(fā)治理效率、降低管理成本。
 - Data Catalog 是一種元數(shù)據(jù)管理的服務,會收集技術元數(shù)據(jù),并在其基礎上提供更豐富的業(yè)務上下文與語義,通常支持元數(shù)據(jù)編目、查找、詳情瀏覽等功能。目前 Data Catalog 作為火山引擎大數(shù)據(jù)研發(fā)治理套件 DataLeap 產(chǎn)品的核心功能之一,經(jīng)過多年打磨,服務于字節(jié)跳動內(nèi)部幾乎所有核心業(yè)務線,解決了數(shù)據(jù)生產(chǎn)者和消費者對于元數(shù)據(jù)和資產(chǎn)管理的各項核心需求。
 - Data Catalog 系統(tǒng)的存儲層,依賴 Apache Atlas,傳遞依賴 JanusGraph。JanusGraph 的存儲后端,通常是一個 Key-Column-Value 模型的系統(tǒng),本文主要講述了使用 MySQL 作為 JanusGraph 存儲后端時,在設計上面的思考,以及在實際過程中遇到的一些問題。
 
起因
實際生產(chǎn)環(huán)境,我們使用的存儲系統(tǒng)維護成本較高,有一定的運維壓力,于是想要尋求替代方案。在這個過程中,我們試驗了很多存儲系統(tǒng),其中 MySQL 是重點投入調(diào)研和開發(fā)的備選之一。
另一方面,除了字節(jié)內(nèi)部外,在 ToB 場景,MySQL 的運維成本也會明顯小于其他大數(shù)據(jù)組件,如果 MySQL 的方案跑通,我們可以在 ToB 場景多一種選擇。
基于以上兩點,我們投入了一定的人力調(diào)研和實現(xiàn)基于 MySQL 的存儲后端。
方案評估
在設計上,JanusGraph 的存儲后端是可插拔的,只要做對應的適配即可,并且官方已經(jīng)支持了一批存儲系統(tǒng)。結(jié)合字節(jié)的技術棧以及我們的訴求,做了以下的評估。
各類存儲系統(tǒng)比較

因投入成本過高,我們不接受自己運維有狀態(tài)集群,排除了 HBase 和 Cassandra;
- 從當前數(shù)據(jù)量與將來的可擴展性考慮,單機方案不可選,排除了 BerkeleyDB;
 - 同樣因為人力成本,需要做極大量開發(fā)改造的方案暫時不考慮,排除了 Redis。
 
最終我們挑選了 MySQL 來推進到下一步。
MySQL 的理論可行性
- 可以支持 Key-Value(后續(xù)簡稱 KV 模型)或者 Key-Column-Value(后續(xù)簡稱 KCV 模型)的存儲模型,聚集索引 B+ 樹排序訪問,支持基于 Key 或者 Key-Column 的 Range Query,所有查詢都走索引,且避免內(nèi)存中重排序,效率初步判斷可接受。
 - 中臺內(nèi)的其他系統(tǒng),最大的 MySQL 單表已經(jīng)到達億級別,且 MySQL 有成熟的分庫分表解決方案,判斷數(shù)據(jù)量可以支持。
 - 在具體使用場景中,對于寫入的效率要求不高,因為大量的數(shù)據(jù)都是離線任務完成,判斷 MySQL 在寫入上的效率不會成為瓶頸。
 
總體設計

- 維護一張 Meta 表做 lookup 用,Meta 表中存儲租戶與 DataSource(庫)之間的映射關系,以及 Shards 等租戶級別的配置信息。
 - StoreManager 作為入口,在 openTransaction 的時候?qū)⒆鈶粜畔⒆⑷氲?StoreTransaction 中,并返回租戶級別的 DataSource。
 - StoreManager 中以 name 為 Key,維護一組 Store,Store 與存儲的數(shù)據(jù)類型有關,具有跨租戶能力
 
- 常見的 Store 有?
?system_properies???,??tx_log???,??graphindex???,??edgestore??等 
- 對于 MySQL 最終的讀寫,都收斂在 Store,方法簽名中傳入 StoreTransaction,Store 從中取出租戶信息和數(shù)據(jù)庫連接,進行數(shù)據(jù)讀寫。
 - 對于單租戶來說,數(shù)據(jù)可以分表(shards),對于某個特定的 key 來說,存儲和讀取某個 shard,是根據(jù) ShardManager 來決定
 - 典型的 ShardManager 邏輯,是根據(jù)總 shard 數(shù)對 key 做 hash 決定,默認單分片。
 - 對于每個 Store,表結(jié)構(gòu)是 4 列(id, g_key, g_column, g_value),除自增 ID 外,對應 key-column-value model 的數(shù)據(jù)模型,key+column 是一個聚集索引。
 - Context 中的租戶信息,需要在操作某個租戶數(shù)據(jù)之前設置,并在操作之后清除掉。
 
細節(jié)設計與疑難問題
細節(jié)設計
存儲模型
JanusGraph 要求 column-family 類型存儲(如 Cassandra, HBase),也就是說,數(shù)據(jù)存儲由一系列行組成,每行都由一個鍵(key)唯一標識,每行由多個列值(column-value)對組成,也會對列進行排序和過濾,如果是非 column-family 的類型存儲,則需要另行適配,適配時數(shù)據(jù)模型有兩種方式:Key-Column-Value 和 Key-Value。

KCV 模型:
- 會將key\column\value在存儲中區(qū)分開來。
 - 對應的接口為:?
?KeyColumnValueStoreManager??。 
KV 模型:
- 在存儲中僅有 key 和 value 兩部分,此處的 key 相當于 KVC 模型中的 key+column;
 - 如果要根據(jù) column 進行過濾,需要額外的適配工作;
 - 對應的接口為:?
?KeyValueStoreManager???,該接口有子類??OrderedKeyValueStoreManager??,提供了保證查詢結(jié)果有序性的接口; - 同時提供了?
?OrderedKeyValueStoreManagerAdapter??接口,用于對 Key-Column-Value 模型進行適配,將其轉(zhuǎn)化為 Key-Value 模型。 
MySQL 的存儲實現(xiàn)采用了 KCV 模型,每個表會有 4 列,一個自增的 ID 列,作為主鍵,同時還有 3 列分別對應模型中的 key\column\value,數(shù)據(jù)庫中的一條記錄相當于一個獨立的 KCV 結(jié)構(gòu),多行數(shù)據(jù)庫記錄代表一個點或者邊。
表中 key 和 column 這兩列會組成聯(lián)合索引,既保證了根據(jù) key 進行查詢時的效率,也支持了對 column 的排序以及條件過濾。
多租戶
存儲層面:默認情況下,JanusGraph 會需要存儲??edgestore???, ??graphindex???, ??system_properties???, ??txlog???等多種數(shù)據(jù)類型,每個類型在 MySQL 中都有各自對的表,且表名使用租戶名作為前綴,如??tenantA_edgestore??,這樣即使不同租戶的數(shù)據(jù)在同一個數(shù)據(jù)庫,在存儲層面租戶之間的數(shù)據(jù)也進行了隔離,減少了相互影響,方便日常運維。(理論上每個租戶可以單獨分配一個數(shù)據(jù)庫)
具體實現(xiàn):每個租戶都會有各自的 MySQL 連接配置,啟動之后會為各個租戶分別初始化數(shù)據(jù)庫連接,所有和 JanusGraph 的請求都會通過 Context 傳遞租戶信息,以便在操作數(shù)據(jù)庫時選擇該租戶對應的連接。
具體代碼:
- Mysql KcvTx:實現(xiàn)了?
?AbstractStoreTransaction???,對具體的 MySQL 連接進行了封裝,負責和數(shù)據(jù)庫的交互,它的??commit???和??rollback??方法由封裝的 MySQL 連接真正完成。 - MysqlKcvStore:實現(xiàn)了?
?KeyColumnValueStore???,是具體執(zhí)行讀寫操作的入口,每一個類型的 Store 對應一個??MysqlKcvStore???實例,??MysqlKcvStore???處理讀寫邏輯時,根據(jù)租戶信息完全自主組裝 SQL 語句,SQL 語句會由??MysqlKcvTx??真正執(zhí)行。 - MysqlKcvStoreManager:實現(xiàn)了?
?KeyColumnValueStoreManager???,作為管理所有 MySQL 連接和租戶的入口,也維護了所有 Store 和??MysqlKcvStore???對象的映射關系。在處理不同租戶對不同 Store 的讀寫請求時,根據(jù)租戶信息,創(chuàng)建??MysqlKcvTx???對象,并將其分配給對應的??MysqlKcvStore??去執(zhí)行。 
事務
幾乎所有與 JanusGraph 的交互都會開啟事務,而且事務對于多個線程并發(fā)使用是安全的,但是 JanusGraph 的事務并不都支持 ACID,是否支持會取決于底層存儲組件,對于某些存儲組件來說,提供可序列化隔離機制或者多行原子寫入代價會比較大。
JanusGraph 中的每個圖形操作都發(fā)生在事務的上下文中,根據(jù) TinkerPop 的事務規(guī)范,每個線程執(zhí)行圖形上的第一個操作時便會打開針對圖形數(shù)據(jù)庫的事務,所有圖形元素都與檢索或者創(chuàng)建它們的事務范圍相關聯(lián),在使用??commit???或者??rollback??方法顯式的關閉事務之后,與該事務關聯(lián)的圖形元素都將過時且不可用。
JanusGraph 提供了??AbstractStoreTransaction???接口,該接口包含??commit???和??rollback???的操作入口,在 MySQL 存儲的實現(xiàn)中,??MysqlKcvTx???實現(xiàn)了??AbstractStoreTransaction???,對具體的 MySQL 連接進行了封裝,在其??commit???和??rollback???方法中調(diào)用 SQL 連接的??commit???和??rollback??方法,以此實現(xiàn)對于 JanusGraph 事務的支持。
數(shù)據(jù)庫連接池
Hikari 是 SpringBoot 內(nèi)置的數(shù)據(jù)庫連接池,快速、簡單,做了很多優(yōu)化,如使用 FastList 替換 ArrayList,自行研發(fā)無所集合類 ConcurrentBag,字節(jié)碼精簡等,在性能測試中表現(xiàn)的也比其他競品要好。
Druid 是另一個也非常優(yōu)秀的數(shù)據(jù)庫連接池,為監(jiān)控而生,內(nèi)置強大的監(jiān)控功能,監(jiān)控特性不影響性能。功能強大,能防 SQL 注入,內(nèi)置 Loging 能診斷 Hack 應用行為。
關于兩者的對比很多,此處不再贅述,雖然 Hikari 的性能號稱要優(yōu)于 Druid,但是考慮到 Hikari 監(jiān)控功能比較弱,最終在實現(xiàn)的時候還是選擇了 Druid。
疑難問題
連接超時
現(xiàn)象:在進行數(shù)據(jù)導入測試時,服務報錯" The last packet successfully received from the server was X milliseconds ago",導致數(shù)據(jù)寫入失敗。
原因:存在超大 table(有 8000 甚至 10000 列),這些 table 的元數(shù)據(jù)處理非常耗時(10000 列的可能需要 30 分鐘),而且在處理過程中有很長一段時間和數(shù)據(jù)庫并沒有交互,數(shù)據(jù)庫連接一直空閑。
解決辦法:
- 調(diào)整 mysql server 端的 wait_timeout 參數(shù),已調(diào)整到 3600s。
 - 調(diào)整 client 端數(shù)據(jù)庫配置中連接的最小空閑時間,已調(diào)整到 2400s。
 
分析過程:
- 懷疑是 mysql client 端沒有增加空閑清理或者?;顧C制,conneciton 在線程池中長時間沒有使用,mysql 服務端已經(jīng)關閉該鏈接導致。嘗試修改客戶端 connection 空閑時間,增加 validationQuery 等常見措施,無果;
 - 根據(jù)打點發(fā)現(xiàn)單條消息處理耗時過高,疑似線程卡死;
 - 新增打點發(fā)現(xiàn)線程沒卡死,只是在執(zhí)行一些非常耗時的邏輯,這時候已經(jīng)獲取到了數(shù)據(jù)庫連接,但是在執(zhí)行那些耗時邏輯的過程中和數(shù)據(jù)庫沒有任何交互,長時間沒有使用數(shù)據(jù)庫連接,最終導致連接被回收;
 - 調(diào)高了 MySQL server 端的 wait_timeout,以及 client 端的最小空閑時間,問題解決。
 
并行寫入死鎖
現(xiàn)象:線程 thread-p-3-a-0 和線程 thread-p-7-a-0 在執(zhí)行過程中都出現(xiàn) Deadlock。
具體日志如下:
原因:
- 結(jié)合日志分析,兩個線程并發(fā)執(zhí)行,需要對同樣的多個記錄加鎖,但是順序不一致,進而導致了死鎖。
 - ?
?55A0??這個 column 對應的 property 是"__modificationTimestamp",該屬性是atlas的系統(tǒng)屬性,當對圖庫中的點或者邊有更新時,對應點或者邊的"__modificationTimestamp"屬性會被更新。在并發(fā)導入數(shù)據(jù)的時候,加劇了資源競爭,所以會偶發(fā)死鎖問題。 
解決辦法:
業(yè)務中并沒有用到"__modificationTimestamp"這個屬性,通過修改 Atlas 代碼,僅在創(chuàng)建點和邊的時候為該屬性賦值,后續(xù)更新時不再更新該屬性,問題得到解決。
性能測試
環(huán)境搭建
在字節(jié)內(nèi)部 JanusGraph 主要用作 Data Catalog 服務的存儲層,關于 MySQL 作為存儲的性能測試并沒有在 JanusGraph 層面進行,而是模擬 Data Catalog 服務的業(yè)務使用場景和數(shù)據(jù),使用業(yè)務接口進行測試,主要會關注接口的響應時間。
接口邏輯有所裁剪,在不影響核心讀寫流程的情況下,屏蔽掉對其他服務的依賴。
模擬單租戶表單 分片情況下,庫表元數(shù)據(jù)創(chuàng)建、更新、查詢,表之間血緣關系的創(chuàng)建、查詢,以此反映在圖庫單次讀寫和多次讀寫情況下 MySQL 的表現(xiàn)。
整個測試環(huán)境搭建在火山引擎上,總共使用 6 臺 8C32G 的機器,硬件條件如下:

測試場景如下:

測試結(jié)論
總計 10 萬個表(庫數(shù)量為個位數(shù),可忽略)

在 10 萬個表且模擬了表之間血緣關系的情況下,??graphindex???表的數(shù)據(jù)量已有 7000 萬,??edgestore??表的數(shù)據(jù)量已有 1 億 3000 萬,業(yè)務接口的響應時間基本在預期范圍內(nèi),可滿足中小規(guī)模 Data Catalog 服務的存儲要求。
總結(jié)
MySQL 作為 JanusGraph 的存儲,有部署簡單,方便運維等優(yōu)勢,也能保持良好的擴展性,在中小規(guī)模的 Data Catalog 存儲服務中也能保持較好的性能水準,可以作為一個存儲選擇。
市面上也有比較成熟的 MySQL 分庫分表方案,未來可以考慮將其引入,以滿足更大規(guī)模的存儲需求。
火山引擎 Data Catalog 產(chǎn)品是基于字節(jié)跳動內(nèi)部平臺,經(jīng)過多年業(yè)務場景和產(chǎn)品能力打磨,在公有云進行部署和發(fā)布,期望幫助更多外部客戶創(chuàng)造數(shù)據(jù)價值。目前公有云產(chǎn)品已包含內(nèi)部成熟的產(chǎn)品功能同時擴展若干 ToB 核心功能,正在逐步對齊業(yè)界領先 Data Catalog 云產(chǎn)品各項能力。















 
 
 




 
 
 
 