DataLeap數(shù)據(jù)資產(chǎn)實(shí)戰(zhàn):如何實(shí)現(xiàn)存儲(chǔ)優(yōu)化?
背景
- DataLeap 作為一站式數(shù)據(jù)中臺(tái)套件,匯集了字節(jié)內(nèi)部多年積累的數(shù)據(jù)集成、開(kāi)發(fā)、運(yùn)維、治理、資產(chǎn)、安全等全套數(shù)據(jù)中臺(tái)建設(shè)的經(jīng)驗(yàn),助力企業(yè)客戶(hù)提升數(shù)據(jù)研發(fā)治理效率、降低管理成本。
- Data Catalog 是一種元數(shù)據(jù)管理的服務(wù),會(huì)收集技術(shù)元數(shù)據(jù),并在其基礎(chǔ)上提供更豐富的業(yè)務(wù)上下文與語(yǔ)義,通常支持元數(shù)據(jù)編目、查找、詳情瀏覽等功能。目前 Data Catalog 作為火山引擎大數(shù)據(jù)研發(fā)治理套件 DataLeap 產(chǎn)品的核心功能之一,經(jīng)過(guò)多年打磨,服務(wù)于字節(jié)跳動(dòng)內(nèi)部幾乎所有核心業(yè)務(wù)線(xiàn),解決了數(shù)據(jù)生產(chǎn)者和消費(fèi)者對(duì)于元數(shù)據(jù)和資產(chǎn)管理的各項(xiàng)核心需求。
- Data Catalog 系統(tǒng)的存儲(chǔ)層,依賴(lài) Apache Atlas,傳遞依賴(lài) JanusGraph。JanusGraph 的存儲(chǔ)后端,通常是一個(gè) Key-Column-Value 模型的系統(tǒng),本文主要講述了使用 MySQL 作為 JanusGraph 存儲(chǔ)后端時(shí),在設(shè)計(jì)上面的思考,以及在實(shí)際過(guò)程中遇到的一些問(wèn)題。
起因
實(shí)際生產(chǎn)環(huán)境,我們使用的存儲(chǔ)系統(tǒng)維護(hù)成本較高,有一定的運(yùn)維壓力,于是想要尋求替代方案。在這個(gè)過(guò)程中,我們?cè)囼?yàn)了很多存儲(chǔ)系統(tǒng),其中 MySQL 是重點(diǎn)投入調(diào)研和開(kāi)發(fā)的備選之一。
另一方面,除了字節(jié)內(nèi)部外,在 ToB 場(chǎng)景,MySQL 的運(yùn)維成本也會(huì)明顯小于其他大數(shù)據(jù)組件,如果 MySQL 的方案跑通,我們可以在 ToB 場(chǎng)景多一種選擇。
基于以上兩點(diǎn),我們投入了一定的人力調(diào)研和實(shí)現(xiàn)基于 MySQL 的存儲(chǔ)后端。
方案評(píng)估
在設(shè)計(jì)上,JanusGraph 的存儲(chǔ)后端是可插拔的,只要做對(duì)應(yīng)的適配即可,并且官方已經(jīng)支持了一批存儲(chǔ)系統(tǒng)。結(jié)合字節(jié)的技術(shù)棧以及我們的訴求,做了以下的評(píng)估。
各類(lèi)存儲(chǔ)系統(tǒng)比較
因投入成本過(guò)高,我們不接受自己運(yùn)維有狀態(tài)集群,排除了 HBase 和 Cassandra;
- 從當(dāng)前數(shù)據(jù)量與將來(lái)的可擴(kuò)展性考慮,單機(jī)方案不可選,排除了 BerkeleyDB;
- 同樣因?yàn)槿肆Τ杀?,需要做極大量開(kāi)發(fā)改造的方案暫時(shí)不考慮,排除了 Redis。
最終我們挑選了 MySQL 來(lái)推進(jìn)到下一步。
MySQL 的理論可行性
- 可以支持 Key-Value(后續(xù)簡(jiǎn)稱(chēng) KV 模型)或者 Key-Column-Value(后續(xù)簡(jiǎn)稱(chēng) KCV 模型)的存儲(chǔ)模型,聚集索引 B+ 樹(shù)排序訪問(wèn),支持基于 Key 或者 Key-Column 的 Range Query,所有查詢(xún)都走索引,且避免內(nèi)存中重排序,效率初步判斷可接受。
- 中臺(tái)內(nèi)的其他系統(tǒng),最大的 MySQL 單表已經(jīng)到達(dá)億級(jí)別,且 MySQL 有成熟的分庫(kù)分表解決方案,判斷數(shù)據(jù)量可以支持。
- 在具體使用場(chǎng)景中,對(duì)于寫(xiě)入的效率要求不高,因?yàn)榇罅康臄?shù)據(jù)都是離線(xiàn)任務(wù)完成,判斷 MySQL 在寫(xiě)入上的效率不會(huì)成為瓶頸。
總體設(shè)計(jì)
- 維護(hù)一張 Meta 表做 lookup 用,Meta 表中存儲(chǔ)租戶(hù)與 DataSource(庫(kù))之間的映射關(guān)系,以及 Shards 等租戶(hù)級(jí)別的配置信息。
- StoreManager 作為入口,在 openTransaction 的時(shí)候?qū)⒆鈶?hù)信息注入到 StoreTransaction 中,并返回租戶(hù)級(jí)別的 DataSource。
- StoreManager 中以 name 為 Key,維護(hù)一組 Store,Store 與存儲(chǔ)的數(shù)據(jù)類(lèi)型有關(guān),具有跨租戶(hù)能力
- 常見(jiàn)的 Store 有?
?system_properies?
??,??tx_log?
??,??graphindex?
??,??edgestore?
?等
- 對(duì)于 MySQL 最終的讀寫(xiě),都收斂在 Store,方法簽名中傳入 StoreTransaction,Store 從中取出租戶(hù)信息和數(shù)據(jù)庫(kù)連接,進(jìn)行數(shù)據(jù)讀寫(xiě)。
- 對(duì)于單租戶(hù)來(lái)說(shuō),數(shù)據(jù)可以分表(shards),對(duì)于某個(gè)特定的 key 來(lái)說(shuō),存儲(chǔ)和讀取某個(gè) shard,是根據(jù) ShardManager 來(lái)決定
- 典型的 ShardManager 邏輯,是根據(jù)總 shard 數(shù)對(duì) key 做 hash 決定,默認(rèn)單分片。
- 對(duì)于每個(gè) Store,表結(jié)構(gòu)是 4 列(id, g_key, g_column, g_value),除自增 ID 外,對(duì)應(yīng) key-column-value model 的數(shù)據(jù)模型,key+column 是一個(gè)聚集索引。
- Context 中的租戶(hù)信息,需要在操作某個(gè)租戶(hù)數(shù)據(jù)之前設(shè)置,并在操作之后清除掉。
細(xì)節(jié)設(shè)計(jì)與疑難問(wèn)題
細(xì)節(jié)設(shè)計(jì)
存儲(chǔ)模型
JanusGraph 要求 column-family 類(lèi)型存儲(chǔ)(如 Cassandra, HBase),也就是說(shuō),數(shù)據(jù)存儲(chǔ)由一系列行組成,每行都由一個(gè)鍵(key)唯一標(biāo)識(shí),每行由多個(gè)列值(column-value)對(duì)組成,也會(huì)對(duì)列進(jìn)行排序和過(guò)濾,如果是非 column-family 的類(lèi)型存儲(chǔ),則需要另行適配,適配時(shí)數(shù)據(jù)模型有兩種方式:Key-Column-Value 和 Key-Value。
KCV 模型:
- 會(huì)將key\column\value在存儲(chǔ)中區(qū)分開(kāi)來(lái)。
- 對(duì)應(yīng)的接口為:?
?KeyColumnValueStoreManager?
?。
KV 模型:
- 在存儲(chǔ)中僅有 key 和 value 兩部分,此處的 key 相當(dāng)于 KVC 模型中的 key+column;
- 如果要根據(jù) column 進(jìn)行過(guò)濾,需要額外的適配工作;
- 對(duì)應(yīng)的接口為:?
?KeyValueStoreManager?
??,該接口有子類(lèi)??OrderedKeyValueStoreManager?
?,提供了保證查詢(xún)結(jié)果有序性的接口; - 同時(shí)提供了?
?OrderedKeyValueStoreManagerAdapter?
?接口,用于對(duì) Key-Column-Value 模型進(jìn)行適配,將其轉(zhuǎn)化為 Key-Value 模型。
MySQL 的存儲(chǔ)實(shí)現(xiàn)采用了 KCV 模型,每個(gè)表會(huì)有 4 列,一個(gè)自增的 ID 列,作為主鍵,同時(shí)還有 3 列分別對(duì)應(yīng)模型中的 key\column\value,數(shù)據(jù)庫(kù)中的一條記錄相當(dāng)于一個(gè)獨(dú)立的 KCV 結(jié)構(gòu),多行數(shù)據(jù)庫(kù)記錄代表一個(gè)點(diǎn)或者邊。
表中 key 和 column 這兩列會(huì)組成聯(lián)合索引,既保證了根據(jù) key 進(jìn)行查詢(xún)時(shí)的效率,也支持了對(duì) column 的排序以及條件過(guò)濾。
多租戶(hù)
存儲(chǔ)層面:默認(rèn)情況下,JanusGraph 會(huì)需要存儲(chǔ)??edgestore?
??, ??graphindex?
??, ??system_properties?
??, ??txlog?
??等多種數(shù)據(jù)類(lèi)型,每個(gè)類(lèi)型在 MySQL 中都有各自對(duì)的表,且表名使用租戶(hù)名作為前綴,如??tenantA_edgestore?
?,這樣即使不同租戶(hù)的數(shù)據(jù)在同一個(gè)數(shù)據(jù)庫(kù),在存儲(chǔ)層面租戶(hù)之間的數(shù)據(jù)也進(jìn)行了隔離,減少了相互影響,方便日常運(yùn)維。(理論上每個(gè)租戶(hù)可以單獨(dú)分配一個(gè)數(shù)據(jù)庫(kù))
具體實(shí)現(xiàn):每個(gè)租戶(hù)都會(huì)有各自的 MySQL 連接配置,啟動(dòng)之后會(huì)為各個(gè)租戶(hù)分別初始化數(shù)據(jù)庫(kù)連接,所有和 JanusGraph 的請(qǐng)求都會(huì)通過(guò) Context 傳遞租戶(hù)信息,以便在操作數(shù)據(jù)庫(kù)時(shí)選擇該租戶(hù)對(duì)應(yīng)的連接。
具體代碼:
- Mysql KcvTx:實(shí)現(xiàn)了?
?AbstractStoreTransaction?
??,對(duì)具體的 MySQL 連接進(jìn)行了封裝,負(fù)責(zé)和數(shù)據(jù)庫(kù)的交互,它的??commit?
??和??rollback?
?方法由封裝的 MySQL 連接真正完成。 - MysqlKcvStore:實(shí)現(xiàn)了?
?KeyColumnValueStore?
??,是具體執(zhí)行讀寫(xiě)操作的入口,每一個(gè)類(lèi)型的 Store 對(duì)應(yīng)一個(gè)??MysqlKcvStore?
??實(shí)例,??MysqlKcvStore?
??處理讀寫(xiě)邏輯時(shí),根據(jù)租戶(hù)信息完全自主組裝 SQL 語(yǔ)句,SQL 語(yǔ)句會(huì)由??MysqlKcvTx?
?真正執(zhí)行。 - MysqlKcvStoreManager:實(shí)現(xiàn)了?
?KeyColumnValueStoreManager?
??,作為管理所有 MySQL 連接和租戶(hù)的入口,也維護(hù)了所有 Store 和??MysqlKcvStore?
??對(duì)象的映射關(guān)系。在處理不同租戶(hù)對(duì)不同 Store 的讀寫(xiě)請(qǐng)求時(shí),根據(jù)租戶(hù)信息,創(chuàng)建??MysqlKcvTx?
??對(duì)象,并將其分配給對(duì)應(yīng)的??MysqlKcvStore?
?去執(zhí)行。
事務(wù)
幾乎所有與 JanusGraph 的交互都會(huì)開(kāi)啟事務(wù),而且事務(wù)對(duì)于多個(gè)線(xiàn)程并發(fā)使用是安全的,但是 JanusGraph 的事務(wù)并不都支持 ACID,是否支持會(huì)取決于底層存儲(chǔ)組件,對(duì)于某些存儲(chǔ)組件來(lái)說(shuō),提供可序列化隔離機(jī)制或者多行原子寫(xiě)入代價(jià)會(huì)比較大。
JanusGraph 中的每個(gè)圖形操作都發(fā)生在事務(wù)的上下文中,根據(jù) TinkerPop 的事務(wù)規(guī)范,每個(gè)線(xiàn)程執(zhí)行圖形上的第一個(gè)操作時(shí)便會(huì)打開(kāi)針對(duì)圖形數(shù)據(jù)庫(kù)的事務(wù),所有圖形元素都與檢索或者創(chuàng)建它們的事務(wù)范圍相關(guān)聯(lián),在使用??commit?
??或者??rollback?
?方法顯式的關(guān)閉事務(wù)之后,與該事務(wù)關(guān)聯(lián)的圖形元素都將過(guò)時(shí)且不可用。
JanusGraph 提供了??AbstractStoreTransaction?
??接口,該接口包含??commit?
??和??rollback?
??的操作入口,在 MySQL 存儲(chǔ)的實(shí)現(xiàn)中,??MysqlKcvTx?
??實(shí)現(xiàn)了??AbstractStoreTransaction?
??,對(duì)具體的 MySQL 連接進(jìn)行了封裝,在其??commit?
??和??rollback?
??方法中調(diào)用 SQL 連接的??commit?
??和??rollback?
?方法,以此實(shí)現(xiàn)對(duì)于 JanusGraph 事務(wù)的支持。
數(shù)據(jù)庫(kù)連接池
Hikari 是 SpringBoot 內(nèi)置的數(shù)據(jù)庫(kù)連接池,快速、簡(jiǎn)單,做了很多優(yōu)化,如使用 FastList 替換 ArrayList,自行研發(fā)無(wú)所集合類(lèi) ConcurrentBag,字節(jié)碼精簡(jiǎn)等,在性能測(cè)試中表現(xiàn)的也比其他競(jìng)品要好。
Druid 是另一個(gè)也非常優(yōu)秀的數(shù)據(jù)庫(kù)連接池,為監(jiān)控而生,內(nèi)置強(qiáng)大的監(jiān)控功能,監(jiān)控特性不影響性能。功能強(qiáng)大,能防 SQL 注入,內(nèi)置 Loging 能診斷 Hack 應(yīng)用行為。
關(guān)于兩者的對(duì)比很多,此處不再贅述,雖然 Hikari 的性能號(hào)稱(chēng)要優(yōu)于 Druid,但是考慮到 Hikari 監(jiān)控功能比較弱,最終在實(shí)現(xiàn)的時(shí)候還是選擇了 Druid。
疑難問(wèn)題
連接超時(shí)
現(xiàn)象:在進(jìn)行數(shù)據(jù)導(dǎo)入測(cè)試時(shí),服務(wù)報(bào)錯(cuò)" The last packet successfully received from the server was X milliseconds ago",導(dǎo)致數(shù)據(jù)寫(xiě)入失敗。
原因:存在超大 table(有 8000 甚至 10000 列),這些 table 的元數(shù)據(jù)處理非常耗時(shí)(10000 列的可能需要 30 分鐘),而且在處理過(guò)程中有很長(zhǎng)一段時(shí)間和數(shù)據(jù)庫(kù)并沒(méi)有交互,數(shù)據(jù)庫(kù)連接一直空閑。
解決辦法:
- 調(diào)整 mysql server 端的 wait_timeout 參數(shù),已調(diào)整到 3600s。
- 調(diào)整 client 端數(shù)據(jù)庫(kù)配置中連接的最小空閑時(shí)間,已調(diào)整到 2400s。
分析過(guò)程:
- 懷疑是 mysql client 端沒(méi)有增加空閑清理或者保活機(jī)制,conneciton 在線(xiàn)程池中長(zhǎng)時(shí)間沒(méi)有使用,mysql 服務(wù)端已經(jīng)關(guān)閉該鏈接導(dǎo)致。嘗試修改客戶(hù)端 connection 空閑時(shí)間,增加 validationQuery 等常見(jiàn)措施,無(wú)果;
- 根據(jù)打點(diǎn)發(fā)現(xiàn)單條消息處理耗時(shí)過(guò)高,疑似線(xiàn)程卡死;
- 新增打點(diǎn)發(fā)現(xiàn)線(xiàn)程沒(méi)卡死,只是在執(zhí)行一些非常耗時(shí)的邏輯,這時(shí)候已經(jīng)獲取到了數(shù)據(jù)庫(kù)連接,但是在執(zhí)行那些耗時(shí)邏輯的過(guò)程中和數(shù)據(jù)庫(kù)沒(méi)有任何交互,長(zhǎng)時(shí)間沒(méi)有使用數(shù)據(jù)庫(kù)連接,最終導(dǎo)致連接被回收;
- 調(diào)高了 MySQL server 端的 wait_timeout,以及 client 端的最小空閑時(shí)間,問(wèn)題解決。
并行寫(xiě)入死鎖
現(xiàn)象:線(xiàn)程 thread-p-3-a-0 和線(xiàn)程 thread-p-7-a-0 在執(zhí)行過(guò)程中都出現(xiàn) Deadlock。
具體日志如下:
原因:
- 結(jié)合日志分析,兩個(gè)線(xiàn)程并發(fā)執(zhí)行,需要對(duì)同樣的多個(gè)記錄加鎖,但是順序不一致,進(jìn)而導(dǎo)致了死鎖。
- ?
?55A0?
?這個(gè) column 對(duì)應(yīng)的 property 是"__modificationTimestamp",該屬性是atlas的系統(tǒng)屬性,當(dāng)對(duì)圖庫(kù)中的點(diǎn)或者邊有更新時(shí),對(duì)應(yīng)點(diǎn)或者邊的"__modificationTimestamp"屬性會(huì)被更新。在并發(fā)導(dǎo)入數(shù)據(jù)的時(shí)候,加劇了資源競(jìng)爭(zhēng),所以會(huì)偶發(fā)死鎖問(wèn)題。
解決辦法:
業(yè)務(wù)中并沒(méi)有用到"__modificationTimestamp"這個(gè)屬性,通過(guò)修改 Atlas 代碼,僅在創(chuàng)建點(diǎn)和邊的時(shí)候?yàn)樵搶傩再x值,后續(xù)更新時(shí)不再更新該屬性,問(wèn)題得到解決。
性能測(cè)試
環(huán)境搭建
在字節(jié)內(nèi)部 JanusGraph 主要用作 Data Catalog 服務(wù)的存儲(chǔ)層,關(guān)于 MySQL 作為存儲(chǔ)的性能測(cè)試并沒(méi)有在 JanusGraph 層面進(jìn)行,而是模擬 Data Catalog 服務(wù)的業(yè)務(wù)使用場(chǎng)景和數(shù)據(jù),使用業(yè)務(wù)接口進(jìn)行測(cè)試,主要會(huì)關(guān)注接口的響應(yīng)時(shí)間。
接口邏輯有所裁剪,在不影響核心讀寫(xiě)流程的情況下,屏蔽掉對(duì)其他服務(wù)的依賴(lài)。
模擬單租戶(hù)表單 分片情況下,庫(kù)表元數(shù)據(jù)創(chuàng)建、更新、查詢(xún),表之間血緣關(guān)系的創(chuàng)建、查詢(xún),以此反映在圖庫(kù)單次讀寫(xiě)和多次讀寫(xiě)情況下 MySQL 的表現(xiàn)。
整個(gè)測(cè)試環(huán)境搭建在火山引擎上,總共使用 6 臺(tái) 8C32G 的機(jī)器,硬件條件如下:
測(cè)試場(chǎng)景如下:
測(cè)試結(jié)論
總計(jì) 10 萬(wàn)個(gè)表(庫(kù)數(shù)量為個(gè)位數(shù),可忽略)
在 10 萬(wàn)個(gè)表且模擬了表之間血緣關(guān)系的情況下,??graphindex?
??表的數(shù)據(jù)量已有 7000 萬(wàn),??edgestore?
?表的數(shù)據(jù)量已有 1 億 3000 萬(wàn),業(yè)務(wù)接口的響應(yīng)時(shí)間基本在預(yù)期范圍內(nèi),可滿(mǎn)足中小規(guī)模 Data Catalog 服務(wù)的存儲(chǔ)要求。
總結(jié)
MySQL 作為 JanusGraph 的存儲(chǔ),有部署簡(jiǎn)單,方便運(yùn)維等優(yōu)勢(shì),也能保持良好的擴(kuò)展性,在中小規(guī)模的 Data Catalog 存儲(chǔ)服務(wù)中也能保持較好的性能水準(zhǔn),可以作為一個(gè)存儲(chǔ)選擇。
市面上也有比較成熟的 MySQL 分庫(kù)分表方案,未來(lái)可以考慮將其引入,以滿(mǎn)足更大規(guī)模的存儲(chǔ)需求。
火山引擎 Data Catalog 產(chǎn)品是基于字節(jié)跳動(dòng)內(nèi)部平臺(tái),經(jīng)過(guò)多年業(yè)務(wù)場(chǎng)景和產(chǎn)品能力打磨,在公有云進(jìn)行部署和發(fā)布,期望幫助更多外部客戶(hù)創(chuàng)造數(shù)據(jù)價(jià)值。目前公有云產(chǎn)品已包含內(nèi)部成熟的產(chǎn)品功能同時(shí)擴(kuò)展若干 ToB 核心功能,正在逐步對(duì)齊業(yè)界領(lǐng)先 Data Catalog 云產(chǎn)品各項(xiàng)能力。