?作者 | 開發(fā)套件團隊
摘要
字節(jié)的 DataCatalog 系統(tǒng),在 2021 年進行過大規(guī)模重構,新版本的存儲層基于 Apache Atlas 實現(xiàn)。遷移過程中,我們遇到了比較多的性能問題。本文以 Data Catalog 系統(tǒng)升級過程為例,與大家討論業(yè)務系統(tǒng)性能優(yōu)化方面的思考,也會介紹我們關于 Apache Atlas 相關的性能優(yōu)化。
背景
字節(jié)跳動 Data Catalog 產(chǎn)品早期,是基于 LinkedIn Wherehows 進行二次改造,產(chǎn)品早期只支持 Hive 一種數(shù)據(jù)源。后續(xù)為了支持業(yè)務發(fā)展,做了很多修修補補的工作,系統(tǒng)的可維護性和擴展性變得不可忍受。比如為了支持數(shù)據(jù)血緣能力,引入了字節(jié)內(nèi)部的圖數(shù)據(jù)庫 veGraph,寫入時,需要業(yè)務層處理 MySQL、ElasticSearch 和 veGraph 三種存儲,模型也需要同時理解關系型和圖兩種。更多的背景可以參照之前的文章。
新版本保留了原有版本全量的產(chǎn)品能力,將存儲層替換成了 Apache Atlas。然而,當我們把存量數(shù)據(jù)導入到新系統(tǒng)時,許多接口的讀寫性能都有嚴重下降,服務器資源的使用也被拉伸到夸張的地步,比如:
- 寫入一張超過 3000 列的 Hive 表元數(shù)據(jù)時,會持續(xù)將服務節(jié)點的 CPU 占用率提升到 100%,十幾分鐘后觸發(fā)超時
- 一張幾十列的埋點表,上下游很多,打開詳情展示時需要等 1 分鐘以上
為此,我們進行了一系列的性能調(diào)優(yōu),結合 Data Catlog 產(chǎn)品的特點,調(diào)整了 Apache Atlas 以及底層 Janusgraph 的實現(xiàn)或配置,并對優(yōu)化性能的方法論做了一些總結。
業(yè)務系統(tǒng)優(yōu)化的整體思路
在開始討論更多細節(jié)之前,先概要介紹下我們做業(yè)務類系統(tǒng)優(yōu)化的思路。本文中的業(yè)務系統(tǒng),是相對于引擎系統(tǒng)的概念,特指解決某些業(yè)務場景,給用戶直接暴露前端使用的 Web 類系統(tǒng)。
優(yōu)化之前,首先應明確優(yōu)化目標。與引擎類系統(tǒng)不同,業(yè)務類系統(tǒng)不會追求極致的性能體驗,更多是以解決實際的業(yè)務場景和問題出發(fā),做針對性的調(diào)優(yōu),需要格外注意避免過早優(yōu)化與過度優(yōu)化。
準確定位到瓶頸,才能事半功倍。一套業(yè)務系統(tǒng)中,可以優(yōu)化的點通常有很多,從業(yè)務流程梳理到底層組件的性能提升,但是對瓶頸處優(yōu)化,才是 ROI 最高的。
根據(jù)問題類型,挑性價比最高的解決方案。解決一個問題,通常會有很多種不同的方案,就像條條大路通羅馬,但在實際工作中,我們通常不會追求最完美的方案,而是選用性價比最高的。
優(yōu)化的效果得能快速得到驗證。性能調(diào)優(yōu)具有一定的不確定性,當我們做了某種優(yōu)化策略后,通常不能上線觀察效果,需要一種更敏捷的驗證方式,才能確保及時發(fā)現(xiàn)策略的有效性,并及時做相應的調(diào)整。
業(yè)務系統(tǒng)優(yōu)化的細節(jié)
優(yōu)化目標的確定
在業(yè)務系統(tǒng)中做優(yōu)化時,比較忌諱兩件事情:
- 過早優(yōu)化:在一些功能、實現(xiàn)、依賴系統(tǒng)、部署環(huán)境還沒有穩(wěn)定時,過早的投入優(yōu)化代碼或者設計,在后續(xù)系統(tǒng)發(fā)生變更時,可能會造成精力浪費。
- 過度優(yōu)化:與引擎類系統(tǒng)不同,業(yè)務系統(tǒng)通常不需要跑分或者與其他系統(tǒng)產(chǎn)出性能對比報表,實際工作中更多的是貼合業(yè)務場景做優(yōu)化。比如用戶直接訪問前端界面的系統(tǒng),通常不需要將響應時間優(yōu)化到 ms 以下,幾十毫秒和幾百毫秒,已經(jīng)是滿足要求的了。
優(yōu)化范圍選擇
對于一個業(yè)務類 Web 服務來說,特別是重構階段,優(yōu)化范圍比較容易圈定,主要是找出與之前系統(tǒng)相比,明顯變慢的那部分 API,比如可以通過以下方式收集需要優(yōu)化的部分:
- 通過前端的慢查詢捕捉工具或者后端的監(jiān)控系統(tǒng),篩選出 P90 大于 2s 的 API
- 頁面測試過程中,研發(fā)和測試同學陸續(xù)反饋的 API
- 數(shù)據(jù)導入過程中,研發(fā)發(fā)現(xiàn)的寫入慢的 API 等
優(yōu)化目標確立
針對不同的業(yè)務功能和場景,定義盡可能細致的優(yōu)化目標,以 Data Catalog 系統(tǒng)為例:
定位性能瓶頸手段
系統(tǒng)復雜到一定程度時,一次簡單的接口調(diào)用,都可能牽扯出底層廣泛的調(diào)用,在優(yōu)化某個具體的 API 時,如何準確找出造成性能問題的瓶頸,是后續(xù)其他步驟的關鍵。下面的表格是我們總結的常用瓶頸排查手段。
優(yōu)化策略
在找到某個接口的性能瓶頸后,下一步是著手處理。同一個問題,修復的手段可能有多種,實際工作中,我們優(yōu)先考慮性價比高的,也就是實現(xiàn)簡單且有明確效果。
快速驗證
優(yōu)化的過程通常需要不斷的嘗試,所以快速驗證特別關鍵,直接影響優(yōu)化的效率。
Data Catalog 系統(tǒng)優(yōu)化舉例
在我們升級字節(jié) Data Catalog 系統(tǒng)的過程中,廣泛使用了上文中介紹的各種技巧。本章節(jié),我們挑選一些較典型的案例,詳細介紹優(yōu)化的過程。
調(diào)節(jié) JanusGraph 配置
實踐中,我們發(fā)現(xiàn)以下兩個參數(shù)對于 JanusGraph 的查詢性能有比較大的影響:
- query.batch = ture
- query.batch-property-prefetch=true
其中,關于第二個配置項的細節(jié),可以參照我們之前發(fā)布的文章。這里重點講一下第一個配置。
JanusGraph 做查詢的行為,有兩種方式:
針對字節(jié)內(nèi)部的應用場景,元數(shù)據(jù)間的關系較多,且元數(shù)據(jù)結構復雜,大部分查詢都會觸發(fā)較多的節(jié)點訪問,我們將 query.batch 設置成 true 時,整體的效果更好。
調(diào)整 Gremlin 語句減少計算和 IO
一個比較典型的應用場景,是對通過關系拉取的其他節(jié)點,根據(jù)某種屬性做 Count。在我們的系統(tǒng)中,有一個叫“BusinessDomain”的標簽類型,產(chǎn)品上,需要獲取與某個此類標簽相關聯(lián)的元數(shù)據(jù)類型,以及每種類型的數(shù)量,返回類似下面的結構體:
{
"guid": "XXXXXX",
"typeName": "BusinessDomain",
"attributes": {
"nameCN": "直播",
"nameEN": null,
"creator": "XXXX",
"department": "XXXX",
"description": "直播業(yè)務標簽"
},
"statistics": [
{
"typeName": "ClickhouseTable",
"count": 68
},
{
"typeName": "HiveTable",
"count": 601
}
]
}
我們的初始實現(xiàn)轉化為 Gremlin 語句后,如下所示,耗時 2~3s:
g.V().has('__typeName', 'BusinessDomain')
.has('__qualifiedName', eq('XXXX'))
.out('r:DataStoreBusinessDomainRelationship')
.groupCount().by('__typeName')
.profile();
優(yōu)化后的 Gremlin 如下,耗時~50ms:
g.V().has('__typeName', 'BusinessDomain')
.has('__qualifiedName', eq('XXXX'))
.out('r:DataStoreBusinessDomainRelationship')
.values('__typeName').groupCount().by()
.profile();
Atlas 中根據(jù) Guid 拉取數(shù)據(jù)計算邏輯調(diào)整
對于詳情展示等場景,會根據(jù) Guid 拉取與實體相關的數(shù)據(jù)。我們優(yōu)化了部分 EntityGraphRetriever 中的實現(xiàn),比如:
- mapVertexToAtlasEntity 中,修改邊遍歷的讀數(shù)據(jù)方式,調(diào)整為以點以及點上的屬性過濾拉取,觸發(fā) multiPreFetch 優(yōu)化。
- 支持根據(jù)邊類型拉取數(shù)據(jù),在應用層根據(jù)不同的場景,指定不同的邊類型集合,做數(shù)據(jù)的裁剪。最典型的應用是,在詳情展示頁面,去掉對血緣關系的拉取。
- 限制關系拉取的深度,在我們的業(yè)務中,大部分關系只需要拉取一層,個別的需要一次性拉取兩層,所以我們接口實現(xiàn)上,支持傳入拉取關系的深度,默認一層。
配合其他的修改,對于被廣泛引用的埋點表,讀取的耗時從~1min 下降為 1s 以內(nèi)。
對大量節(jié)點依次獲取信息加并行處理
在血緣相關接口中,有個場景是需要根據(jù)血緣關系,拉取某個元數(shù)據(jù)的上下游 N 層元數(shù)據(jù),新拉取出的元數(shù)據(jù),需要額外再查詢一次,做屬性的擴充。
我們采用增加并行的方式優(yōu)化,簡單來說:
- 設置一個 Core 線程較少,但 Max 線程數(shù)較多的線程池:需要拉取全量上下游的情況是少數(shù),大部分情況下幾個 Core 線程就夠用,對于少數(shù)情況,再啟用額外的線程。
- 在批量拉取某一層的元數(shù)據(jù)后,將每個新拉取的元數(shù)據(jù)頂點加入到一個線程中,在線程中單獨做屬性擴充
- 等待所有的線程返回
對于關系較多的元數(shù)據(jù),優(yōu)化效果可以從分鐘級到秒級。
對于寫入瓶頸的優(yōu)化
字節(jié)的數(shù)倉中有部分大寬表,列數(shù)超過 3000。對于這類元數(shù)據(jù),初始的版本幾乎沒法成功寫入,耗時也經(jīng)常超過 15 min,CPU 的利用率會飆升到 100%。
定位寫入的瓶頸
我們將線上的一臺機器從 LoadBalance 中移除,并構造了一個擁有超過 3000 個列的元數(shù)據(jù)寫入請求,使用 Arthas 的 itemer 做 Profile,得到下圖:
從上圖可知,總體 70%左右的時間,花費在 createOrUpdate 中引用的 addProperty 函數(shù)。
耗時分析
- JanusGraph 在寫入一個 property 的時候,會先找到跟這個 property 相關的組合索引,然后從中篩選出 Coordinality 為“Single”的索引
- 在寫入之前,會 check 這些為 Single 的索引是否已經(jīng)含有了當前要寫入的 propertyValue
- 組合索引在 JanusGraph 中的存儲格式為:
- Atlas 默認創(chuàng)建的“guid”屬性被標記為 globalUnique,他所對應的組合索引是__guid。
- 對于其他在類型定義文件中被聲明為“Unique”的屬性,比如我們業(yè)務語義上全局唯一的“qualifiedName”,Atlas 會理解為“perTypeUnique”,對于這個 Property 本身,如果也需要建索引,會建出一個 coordinity 是 set 的完全索引,為“propertyName+typeName”生成一個唯一的完全索引
- 在調(diào)用“addProperty”時,會首先根據(jù)屬性的類型定義,查找“Unique”的索引。針對“globalUnique”的屬性,比如“guid”,返回的是“__guid”;針對“perTypeUnique”的屬性,比如“qualifiedName”,返回的是“propertyName+typeName”的組合索引。
- 針對唯一索引,會嘗試檢查“Unique”屬性是否已經(jīng)存在了。方法是拼接一個查詢語句,然后到圖里查詢
- 在我們的設計中,寫入表的場景,每一列都有被標記為唯一的“guid”和“qualifiedName”,“guid”會作為全局唯一來查詢對應的完全索引,“qualifiedName”會作為“perTypeUnique”的查詢“propertyName+typeName”的組合完全索引,且整個過程是順序的,因此當寫入列很多、屬性很多、關系很多時,總體上比較耗時。
優(yōu)化思路
- 對于“guid”,其實在創(chuàng)建時已經(jīng)根據(jù)“guid”的生成規(guī)則保證了全局唯一性,幾乎不可能有沖突,所以我們可以考慮去掉寫入時對“guid”的唯一性檢查,節(jié)省了一半時間。
- 對于“qualifiedName”,根據(jù)業(yè)務的生成規(guī)則,也是“globalUnique”的,與“perTypeUnique”的性能差別幾乎是一倍:
優(yōu)化實現(xiàn)效果
- 去除 Atlas 中對于“guid”的唯一性的檢查。
- 添加“Global_Unqiue”配置項,在類型定義時使用,在初始化時對“__qualifiedName”建立全局唯一索引。
- 配合其他優(yōu)化手段,對于超多屬性與關系的 Entity 寫入,耗時可以降低為分鐘級。
總結
- 業(yè)務類系統(tǒng)的性能優(yōu)化,通常會以解決某個具體的業(yè)務場景為目標,從接口入手,逐層解決
- 性能優(yōu)化基本遵循思路:發(fā)現(xiàn)問題->定位問題->解決問題->驗證效果->總結提升
- 優(yōu)先考慮“巧”辦法,“土”辦法,比如加機器改參數(shù),不為了追求高大上而走彎路