B 站基於 ClickHouse 的海量用戶行爲分析應用實踐

本期作者

陸志君

數倉平臺資深數據開發工程師

趙卓男

嗶哩嗶哩資深開發工程師

張弛

嗶哩嗶哩高級開發工程師

王智博

嗶哩嗶哩資深開發工程師

01. 背景介紹

數據驅動理念已被各行各業所熟知,核心環節包括數據採集、埋點規劃、數據建模、數據分析和指標體系構建。在用戶行爲數據領域,對常見的多維數據模型進行信息提煉和模型整合,可以形成一套常見的數據分析方法來發現用戶行爲的內在聯繫,能更好洞察用戶的行爲習慣和行爲規律,幫助企業挖掘用戶數據的商業價值。

行業內最早可追溯到 Google Analytics 埋點分析工具,國內較早開始這方面研究的是百度大數據分析平臺;隨着 15 年後國內大數據興起,神策的用戶行爲分析平臺、GrowthingIO 的增長平臺等獨立數據分析平臺公司相繼成立;18 年後一些發展較快的大廠經過幾年數據積累也有了自己的分析平臺,例如美團點評的 Ocean 行爲分析平臺、字節的火山引擎增長分析平臺等等。

只有當數據達到一定規模才更適合用科學化的方法來提升數據分析效率,如前面所述,雖然 Google 和百度在這塊最早探索,但後面一些互聯網公司也是過幾年纔有自己的產品,即數據產品的發展需要與實際數據規模和業務發展相符。B 站最早從 19 年開始關注大數據建設,到現在已經有一套較爲成熟的數據產品——北極星,可以實現對用戶行爲數據進行埋點採集、埋點測試、埋點管理、行爲數據分析等功能。行爲數據分析平臺主要包括下圖所列功能模塊,本文介紹主要模塊原理和相關技術實現。

圖片

02. 技術方案演進

北極星用戶行爲分析(User Behavior Analysis, UBA)模塊自 19 年以來主要有三波迭代。

2019 年~ 2020 年中:【部分模型化聚合 + Spark Jar 任務】

這個階段主要任務是功能實現,根據用戶前端查詢參數,提交 Spark Jar 作業等待返回結果。不同的分析模塊對應不同的 Spark Jar 作業,也對應不同加工好的用戶行爲模型。數據架構如下圖所示:

圖片

雖然在一定程度上可以完成功能實現,但存在明顯弊端:

在實際使用中計算時間太長,單事件分析需要超過 3 分鐘返回結果,漏斗和路徑分析需要超過 30 分鐘返回結果,導致產品可用性極低,查詢穩定性和成功率不是很高,使用人數不是很多。這個階段的埋點管理和上報格式未完全規範化,所以重點還是做後者。

2020 年中~ 2021 年中:【無模型化明細 + Flink+ClickHouse】

ClickHouse 是 Yandex 公司於 2016 年開源的一個列式數據庫管理系統。Yandex 的核心產品是搜索引擎,非常依賴流量和在線廣告業務,因此 ClickHouse 天生就適合用戶流量分析。B 站於 2020 年開始引入 ClickHouse,結合北極星行爲分析場景進行重構,如下圖所示:

圖片

這裏直接從原始數據開始消費,通過 Flink 清洗任務將數據直接洗入 ClickHouse 生成用戶行爲明細,可以稱作無模型化明細數據。Redis 維表被用來做實時用戶屬性關聯,字典服務被用於把 String 類型的實體 ID 轉成 Bigint,利用 ClickHouse 原生的 RoaringBitMap 函數對參與計算的行爲人羣交併差集計算。這一代實現了實時埋點效果查看,上線以來北極星產品周活人數提升了 300% 以上,相對於前代,性能有較大提升:

但本身這種性能提升是以資源消耗爲前提的。以移動端日誌爲例,FLink 消費任務峯值可以達到百萬條每秒,對 Redis 維表關聯和字典服務處理挑戰很大,計算併發度甚至達到 1200core,遇到特殊流量事件往往出現堆積、延遲、斷流,對人工運維成本消耗也較大。此外這種 Lambda 數據流架構,實時和離線清洗邏輯需要保持一致,否則很容易導致數據解釋成本提升。另外本身實時 + 離線維護兩套對存儲上也是極大浪費,即 Kafka、Hive、CK 都需要存儲同一份數據。到 21 年底,隨着業務發展 CK 存儲幾經橫向擴充剩下不到 10%,而集羣的擴展和數據遷移也需要較大精力,本文後面小節會詳細介紹。功能方面,直接對明細數據應用原生 CK 函數查詢的跨天留存分析、路徑分析需要用時分鐘級,體驗不是很好。

2021 年中~ 今:【Iceberg 全模型化聚合 + ClickHouse】

22 年開始公司大力推動降本增效,這就要求以儘可能少的資源最大化行爲分析產品效能。整體核心思路是全模型化聚合加速,底層流量數據鏈路走 kappa 架構,不會再用北極星應用數據和流量表不一致的情況,數據小時級產出。這次改造實時資源可以節約 1400core,節省 Redis 內存 400G、節省 Kafka300 Partiton。每天數據量由千億數據降低爲百億,通過特定的 sharding 方式配合下推參數,利用分區、主鍵、索引等手段支持事件分析 (平均查詢耗時 2.77s)、事件合併去重分析 (平均查詢耗時 1.65s)、單用戶細查 (平均查詢耗時 16.2s)、漏斗分析 (平均查詢耗時 0.58s),留存分析和路徑分析從分鐘級查詢到 10s 內相應。數據架構如圖所示:

圖片

擁有以下特點:

到 22 年中,隨着數據湖的興起,我們將 hive 流量聚合模型遷移到 Iceberg 上,日常事件查詢可以在 10s 內完成,可以作爲 CK 數據的備用鏈路。這條鏈路不光降低了緊急事件運維成本,提升數據可用性保障,還可以支持用戶日常流量關聯其他業務定製化查詢取數。通用的模型結構除了支持流量行爲日誌外,通過映射管理可以快速接入其他服務端日誌,擴展其使用的場景。下圖爲 22 年 12 月份最近一週各功能模塊使用情況:

圖片

從發展歷程來看,用戶行爲數據分析經歷了從強離線引擎驅動到強 OLAP 驅動,離不開業界大數據技術不斷髮展和進步,北極星行爲數據底層明細後面也會切換到 Hudi,可以滿足更加實時的數據消費,讓專業的工具做專業的事。

03. 事件、留存分析

事件分析是指對具體的行爲事件進行相關指標統計、屬性分組、運算、條件篩選等操作,本質上是分析埋點事件的用戶觸發情況以及埋點事件的分析統計情況。留存分析可以根據業務場景以及產品階段的不同,自定義起始行爲和後續行爲做留存計算,協助分析用戶使用產品的粘性,根據留存分析結果有針對性地調整策略,引導用戶發現產品價值,留住用戶,實現用戶真實的增長。

過去北極星分析平臺的分析模塊大多以 B 站的千億明細行爲數據爲基礎,通過 ClickHouse 查詢引擎的指標函數例如 uniq(),可以支持單個事件分析、多個事件的對比分析以及多個事件的複合指標運算,支持指定時間內的行爲留存分析 (參與後續行爲的用戶佔參與初始行爲用戶的比值),通過篩選、分組等組件滿足多樣化分析需求。但是過去的北極星事件分析是基於明細數據,B 站行爲數據每天增量千億級別,存儲日增 10T 以上資源消耗巨大,明細數據分析查詢比較慢,每天用戶慢查詢平均 30s~50s 體驗較差,而且其功能比較單薄,只能支持 30 天的查詢窗口,用戶留存、用戶分羣等複雜分析模塊很難實現。而且海量行爲數據分析也面臨許多挑戰,每天千億行爲數據,高峯期寫入 QPS 百萬以上。如何實現既滿足時效性又滿足海量數據壓力的計算方式?如何滿足複雜分析場景的同時,壓縮存儲提升查詢效率?如何簡化數據鏈路,模塊化插件化降低接入成本,提升擴展性?如何打通標籤、ABTest 等其他業務系統將北極星的行爲分析能力標準化?

北極星事件分析:

圖片

爲了解決以上痛點和海量數據分析的挑戰,新的事件、留存分析通過準實時方式建模分層,用戶、事件、時間等粒度的預聚合壓縮,不僅統一了離線口徑,而且自研拉寬匯聚 spark 腳本可以承載千億數據壓力,搭配多種聚合模型實現豐富的分析模塊。同時釋放實時資源離線小時任務保證時效性,維表壓力採用 join 離線維表 + 屬性字典維度服務的方式解決,並且早於平臺自研可指定 shard 的 BulkLoad 出倉工具,配合下推參數可加速查詢,數據鏈路可擴展易運維。相比較以往的處理千億明細數據,準實時在 DWB 層實現了對數據的壓縮,將每天千億數據壓縮到每天百億級別。OLAP 層也通過彙總後的數據替代了原先的明細數據,大大縮小存儲的同時也提高了查詢性能,每天用戶慢查詢可降到 10s 以內,時間窗口可擴大到 45 天甚至更長。並且對高複雜的查詢比如用戶留存,用戶分羣等分析場景可以更好的支持。

事件分析數據開發流程

圖片

具體實現包括以下核心部分:
1. 流量聚合模型創建。首先準實時清洗 DWD 層 B 站千億明細行爲數據,流量數據都是分爲私有參數和公有參數,其中公有參數在用戶粒度下是不會經常改變的,我們會用一般聚合函數取一定時間內指定設備和行爲事件下最新保留的不變公有參數,而將同等粒度下變化比較頻繁的私有參數維度名寫入 Array 結構,利用 map 索引原理,把私參維度值組合通過 spark 自定義邏輯計數併入 map 的 key 中,map 的 value 則用來寫入各種公共指標聚合結果,整個過程均通過 spark 腳本實現,最終寫入到 Iceberg 引擎中。因爲 Iceberg 可以關聯其他任何已有 hive 表,通過快速業務表關聯也可以支持到其他多項業務應用,也可以作爲不出倉的北極星降級備用方案支持大部分查詢分析功能。

流量聚合模型數據方案

圖片

  1. 流量聚合模型在 iceberg 下查詢。 如下圖所示,聚合之後的數據形成 DWB 層落地到 iceberg 表 (即圖中 iceberg_bdp.dwb_flow_ubt_app_group_buvid_eventid_v1_l_hr),可以在 hive 和 spark 上計算大部分查詢維度下的指標。利用 Trino 基於連接器實現了存儲與計算分離,通過 map_filter、array_position 等 trino 條件函數和 map_values、reduce 等 trino 指標函數可以實現一系列複雜事件分析,當然我們也配套開發了一些簡單易用的 UDF 可以繞開較複雜的 trino 函數組合供用戶查詢使用,性能上相差不大。

圖片

  1. 公參和私參篩選器創建。接下來我們利用 BulkLoad 出倉腳本將 iceberg 數據導入 ClickHouse 表 (即圖中 polagrou.polaris_dwb_flow_ubt_group_buvid_eventid_pro_i_d_v1),即保證了時效性又兼容了特殊的數據結構。從 ClickHouse 表結構設計上支持了 SAMPLE BY murmurHash3_64(buvid) 的抽樣功能,由於 buvid(設備 id)分 shard 寫入可以保證單節點的數據隨機分配,只要在單節點上做抽樣配合 ReplicatedReplacingMergeTree 引擎就可以實現了 ck to ck 的物化篩選器,直接爲北極星分析平臺提供公參維度聚合、私參枚舉排序的維度篩選功能。整個過程直接在可支持調度的 python 腳本上實現,可支持到近小時更新。

圖片

  1. 流量聚合模型在 ClickHouse 下查詢。在 ClickHouse 查詢上設計特定的 CK-UDF 來解析嵌套 map 結構,保證複雜分析場景的同時用於加速了查詢,相比用 ClickHouse 原生多個函數組合解析要快 30% 左右,比原先明細模型的查詢要快更多。而且通過腳本實現了多維度的 ClickHouse 小時級別的機器人監控告警,早於平臺對此定製化監控告警的支持。

目前北極星分析平臺平均查詢耗時 3.4s,通過通用聚合模型,下游可以對行爲人羣進行交併計算實現標籤畫像和人羣圈選等轉化分析功能,也可以利用 Retention 函數實現了 N 日的事件留存分析。最終相比前代方案節省計算資源 1400C、節省存儲資源 40%,提升查詢效率 60% 以上,利用 RBM 實現了北極星、標籤、ABTest 等多業務打通。

04. 漏斗、路徑分析

流量業務分析場景上會查看一羣用戶在客戶端或者網頁上的路徑流轉信息,路徑分析將用戶在產品中的使用路徑用桑吉圖呈現,展現用戶在頁面與頁面流轉中的流量走向。通過路徑分析可以幫助驗證產品運營策略,優化產品設計思路。漏斗是用戶在產品使用中完成的一系列行爲轉化。漏斗分析可以幫助瞭解用戶在行爲步驟中的轉化或流失情況,進而通過優化產品或者開展運營活動提升轉化率,達成業務目標。

在業務日益增長的情況下,對用戶漏斗、路徑精細化分析訴求逐漸增加,爲此北極星分析平臺增加此類型支持,用於分析一羣用戶在某一頁面、某一模塊前後的流量流轉變化。漏斗分析業界常見解決此類場景利用 ClickHouse 提供了一個名叫 windowFunnel 的函數來實現對明細數據的漏斗分析。而路徑分析技術一般分爲兩種,一種爲明細數據結合 sequenceCount(pattern)(timestamp, cond1, cond2, ...) 做簡單的路徑分析,而複雜的路徑分析又叫智能路徑分析可以通過 ClickHouse 提供的高階數組函數進行曲線救國。

路徑分析背景挑戰:

圖片

但是過去的流量漏斗、路徑分析都是基於明細數據進行的。存儲資源消耗大、分析查詢慢、功能比較單薄等。爲了解決以上痛點,新的漏斗、路徑分析通過離線方式的建模分層、用戶路徑粒度的預聚合、存儲引擎 ClickHouse 的 RBM 物化視圖等技術,將每天千億數據壓縮到每天幾十億。查詢效率也從分鐘級優化到秒級,更是通過關聯標籤和人羣支持到了各種轉化查詢分析。大大縮小存儲的同時查詢性能大大提升,最終實現了關聯標籤和人羣圈選等功能。

路徑分析功能頁面:

圖片

具體實現包括以下核心部分:

  1. 路徑聚合 DWB 模型創建。首先離線處理 B 站的千億明細行爲數據,經過維度裁剪變化比較頻繁的私有參數,保留用戶粒度下的公有參數,並且通過 buvid(設備 id) 粒度進行聚合,將同一個 buvid 的所有事件根據時間線串聯聚合到一個字段中,聚合之後的數據形成 DWB 層落地到 hive 表。

路徑分析數據方案:

圖片

  1. 路徑聚合 DWS 模型創建。在上一步的基礎上,對 DWB 層的數據進行路徑的彙總,將同一個路徑的 buvid(設備 id) 彙總聚合到數組結構中,這個過程出現很多幹擾事件,比如某些路徑會頻繁出現,會亂序而干擾真正的用戶行爲,所以我們會通過去重等手段進行干擾事件過濾路徑補位拼接形成桑基圖節點,當然我們還引入了 RBM 數據結構存儲聚合後的設備編碼,最終落到 hive 表。整個過程都是通過 spark 腳本利用代碼和算法實現的。

漏斗分析查詢方案:

圖片

  1. 路徑聚合模型 Clickhouse 表設計。接下來我們利用平臺工具將 hive 數據出倉到 ClickHouse,在 ClickHouse 表結構設計上,採用了 ClickHouse 的物化視圖技術和 RBM 數據結構,進一步壓縮 buvid(設備 id) 集合爲 RBM 編碼,利用數組物化 RBM 的方式大大壓縮了存儲,可通過 Bitmap 交併計算路徑相關指標,千億數據壓縮到幾十億做到了秒級查詢。

路徑分析數據協議:

圖片

數據結構形成的樹型圖:

圖片

  1. 路徑聚合模型漏斗分析查詢。在功能上漏斗分析通過 windowFunnel 函數進行計算,將計算週期內每個用戶的行爲明細按時間順序聚合爲對應事件鏈,然後搜索滑動時間窗口滿足漏斗條件的事件鏈,並計算從鏈中發生的最大事件數 level,最後對各級 level 計算 uv 獲得結果。

右側節點上的數字表示從中心事件 e0 至自身的路徑 uv:

圖片

在樹型圖中的對應關係:表示路徑 e0->e4→e1→e3→e2 在窗口期內的總 uv 爲 1。左側同理,方向相反。

圖片

  1. 路徑聚合模型路徑分析查詢。同理路徑分析在 ClickHouse 數據基礎上利用數據協議和複雜 sql 繪製出路徑樹狀圖進而拼接出桑基圖,可直觀的展現用戶主流流程,幫助確定轉化漏斗中的關鍵步驟,迅速發現被用戶忽略的產品價值點,修正價值點曝光方式並發現用戶的流失點,同時通過 Bitmap 的交併計算實現了標籤畫像和人羣圈選等轉化分析功能。

05. 標籤、人羣圈選

B 站的北極星行爲分析平臺、標籤畫像平臺、AB 實驗人羣包都是基於 ClickHouse 的 RBM(RoaringBitMap) 實現,此外 RBM 還有其他多項應用,比如事件分析標籤人羣圈選、預計算的路徑分析、創建用戶行爲的用戶分羣等,具體可查看之前文章 [1]。

圖片

下圖是基於北極星 CK 底層數據生成一個滿足指定行爲結果的人羣包邏輯:

圖片

RBM 固然好用,但是隻支持 int 或者 long 類型,如果去重字段不是 int 或者 long 怎麼辦呢?海量數據應用層的維度服務如何做到高可用高併發?依賴的鏈路出問題如何快速恢復,數據如何保障?

屬性字典維度服務就是可解碼編碼多業務屬性、可輸出管理多業務維度,具有分佈式、高可用、高併發等特性的服務系統,通過屬性字典維度服務可實現多維度管理多業務打通,爲海量數據應用層定製化提供技術支持。

屬性字典維度服務架構設計:

圖片

高可用方面 Grpc+LoadCache+Redis + 公司自研 rockdbKV 存儲,多級緩存分佈式架構支持平滑擴容和滾動發佈,可做到日常緩存命中率 70% 以上,底層 ID 生成算法基於 Leaf-SnowFlake 快速生成,壓測可支持 50w 以上 QPS 高併發。所有請求通過公司的日誌傳輸通道可以小時級同步到 hive 做備份,事故情況下配合 BulkLoad 讀寫分離可 40 分鐘內恢復 20 億 + 屬性字典。

最終利用屬性字典對 buvid(設備 id) 等業務屬性編碼和解碼,對用戶標籤和 AB 人羣進行創建,並且通過 RBM 交併計算實現了北極星分析平臺、用戶畫像平臺、AB 實驗平臺的多業務打通。

人羣圈選 sql 示例:

圖片

06. ClickHouse 數據導入方案演進

如上文所述,北極星是基於 ClickHouse 構建的一套海量 UBA 技術解決方案,底層 ClickHouse 集羣的穩定性 、讀寫性能、資源使用率均會影響上層業務的使用體驗。與此同時,海量數據如何導入 ClickHouse,以及數據導入過程的穩定性、導入效率、資源消耗在很大程度上決定了 ClickHouse 集羣的整體穩定性和使用效率。所以,一個穩定高效的數據導入方案對於一套 UBA 解決方案來說是必不可少的。

在 B 站,UBA 場景的數據導入方案大致經歷了三個階段的演進:

6.1 JDBC 寫入方案

在 B 站內部,針對數據寫入到各個數據庫 / 引擎主要有兩套 pipeline:一套是基於 Spark 的離線導入鏈路,大部分數據來源於 Hive;另一套是基於 FLink 的實時導入鏈路,大部分數據源來源於 kafka。這兩套鏈路都支持 clickhouse 作爲 data sink,UBA 場景最開始也是基於這兩套鏈路來做數據導入的,主要使用的是實時導入鏈路,在歷史數據初始導入和故障補數等少數情況下也用到離線導入鏈路。

圖片

如上圖所示,離線和實時導入最終都使用 ClickHouse JDBC 向 ClickHouse 發送數據,這種寫入方式實現起來比較簡單,使用開源的 ClickHouse JDBC Driver 就可以使用標準 JDBC 接口向 ClickHouse 寫入數據。同時,flink 實時寫入的數據延遲比較低,端到端延遲可控制在秒級。但這個方案存在以下問題:

  1. ClickHouse Server 端的資源消耗比較大(因爲數據的排序,索引生成,數據壓縮等步驟均是在 server 端完成),在高峯時會影響查詢性能。

  2. 實時任務寫入頻次較高,數據會在寫入後觸發大量 merge 操作,造成 “寫放大”,消耗更多的磁盤 IO 和 CPU 資源,可能導致 too many parts 錯誤。

  3. 實時 Flink 任務需要長時間佔用大量資源,且在故障情況下容易出現數據堆積、延遲、斷流等問題,運維成本較高。

以上問題在資源充沛的情況下不會影響業務使用,但當集羣資源接近瓶頸時,查詢性能受寫入影響,寫入性能和穩定性受 merge 影響,最終導致集羣整體穩定性下降,影響業務使用。

6.2 基於中間存儲的 BulkLoad 導入方案

UBA 場景的多個分析模塊對數據延遲要求不盡相同,大部分數據實時性要求並不高,小時級延遲在大部分模塊下是可接受的。因此,爲了解決上述 JDBC 寫入方案的問題,我們針對大部分對時效性要求不高的數據導入需求,構建了一套基於中間存儲的 BulkLoad 導入方案:

圖片

  1. 首先,將 clickhouse 格式的 data part 文件的生成過程轉移到 Spark Application 中完成,這樣就可以利用 Yarn 集羣的資源來完成數據排序,索引生成,數據壓縮等步驟。

  2. data part 文件的生成我們藉助 clickhouse-local 工具實現,在 Spark Executor 中調用 clickhouse-local 寫入數據到本地磁盤,生成 clickhouse data part 文件。

  3. 然後,將 Spark Executor 生成的 data part 文件上傳到 HDFS 文件系統的特定目錄中。

  4. 接着,從 Spark Executor 端發送 "ALTER TABLE ... FETCH PART/PARTITION" SQL 語句到 clickhouse server 執行。

  5. 最後,ClickHouse Server 執行 "ALTER TABLE ... FETCH PART/PARTITION",從 HDFS 拉取 data part 文件並完成 attach 操作。其中,我們對 ClickHouse 代碼做了一些改造,使得 FETCH 語句支持從 HDFS 拉取文件。

由於 Bulkload 導入將數據寫入 data part 文件這個過程移到了 Spark 端執行,大大降低了 ClickHouse Server 數據寫入對資源的消耗。與此同時,由於在 Spark 端數據批量寫入之前已經完成了 repartition 和攢批,到達 ClickHouse Server 的 data part 數量相較 JDBC 寫入要少很多,所以 clickhouse 的 merge 壓力也大幅降低。該方案上線後,數據寫入對 clickhouse 查詢的影響基本消除,集羣穩定性得到大幅提升。

但這個方案依然存在一些問題:

  1. 以 HDFS 作爲文件傳輸的中間存儲,增加了數據傳輸的耗時和網絡開銷,同時會佔用 HDFS 的存儲資源。

  2. HDFS 的負載情況可能影響 ClickHouse Bulkload 數據導入的性能與穩定性。

6.3 直達 ClickHouse 的 BulkLoad 導入方案

爲了進一步優化數據導入的性能和穩定性,我們參照 ClickHouse 副本間數據同步的 DataExchange 服務,開發了 ClickHouse 的 DataReceive 服務,以支持 Spark Executor 直接將 data part 文件傳輸到 ClickHouse Server,繞開 HDFS 中間存儲。

圖片

DataReceive 服務允許使用 HTTP 客戶端直接將數據文件發送到 ClickHouse,ClickHouse 端會進行鑑權、數據校驗、流量控制、併發控制、磁盤負載均衡等操作。該方案相較於基於 HDFS 中間存儲的 Bulkload 方案,大致有一倍的性能提升。

07. ClickHouse 數據重平衡

B 站每天的用戶行爲數據量達數千億行,UBA 場景需要分析最近半年以上的歷史數據,所以底層 ClickHouse 需要存儲 PB 級的已壓縮數據。同時,隨着 B 站活躍用戶日益增長,需要存儲的數據量也在不斷增長,所以集羣擴容的需求是必不可少的。

然而,由於受限於存算一體的架構設計,ClickHouse 集羣目前無法做到彈性擴容,數據需要在新集羣中完成重分配。因此,ClickHouse 如何高效穩定地完成數據重平衡(Data Rebalance)是 ClickHouse 集羣管理人員必須面對和解決的的問題。

我們在 UBA 場景集羣擴容的準備和實施過程中,經歷了從手動化,到半自動化,再到服務化的演進。在此期間,我們將在海量數據重平衡實踐過程中遇到的問題與解決方法轉化成爲了一套自動化工具服務。下面,我們就來介紹一下這套工具服務的功能與實現原理。

7.1 平衡度

集羣中表的大小差異很大,有些達到幾百 TB, 有些只有幾 GB,如何度量數據的平衡程度,篩選出需要平衡的表?我們引入了一些數學公式來解決這個問題。

變異係數:當需要比較兩組數據離散程度大小的時候,如果兩組數據的測量尺度相差太大,或者數據量綱的不同,直接使用標準差來進行比較不合適,此時就應當消除測量尺度和量綱的影響,而變異係數可以做到這一點,它是原始數據標準差與原始數據平均數的比,取值範圍 0~1,值越小,離散程度越小。

表的平衡度 = 變異係數 (取值範圍 0~1,值越大,表越不平衡)

舉例:  表 A 的平衡度

集羣共有 4 個節點,表 A 在不同節點的大小分別爲 4GB, 10GB, 5GB, 3GB

平均值:  (4 + 10 + 5 + 3) / 4 = 5.5
方差:  (x - 平均值) ^ 2 / 4 = 7.25
標準差:  root(方差) = 2.69
變異係數: 標準差 / 平均值 = 0.49

表 A 的平衡度 = 0.49

7.2 平衡算法

對於待平衡的表,有些業務期望最大程度的平衡,提升並行度,發揮集羣的最大算力,而有些表容量過大,業務期望以最小的遷移成本,快速平衡數據,達到相對較優的平衡。

對於不同的業務需求,提供了兩種平衡算法,裝箱算法和貪心算法。

期望達到極致的均衡,數據量較小時,推薦使用裝箱算法。期望以最小的遷移成本,達到較優的均衡,推薦使用貪心算法。

裝箱算法

算法整體採用 Best Fit(最優裝箱算法) + AVL 樹的設計。每個 ClickHouse 節點即爲一個 Node,每個 Node 有初始閾值 capacity,代表 ClickHouse 節點的容納量。將準備平衡的 part 按照大小順序排序,並根據 Best Fit 算法依次填充到 Node 中,Node 根據 remain_capacity(剩餘容量),左旋右旋組成一棵 AVL 樹,以此提升查詢效率,方便快速完成平衡。

設計如下圖所示。

圖片

裝箱算法細節在此不做贅述,感興趣的讀者可參考這裏 [2]。

貪心算法

算法整體採用不斷輪詢 + 局部最優的設計。將 ClickHouse 節點按照大小排序,找出最大和最小的節點,如果將某個 part 從最大的節點搬遷至最小的節點,遷出的節點仍然大於遷入節點,則搬遷該 part,直到最大節點無法遷出。依此類推,繼續按照大小排序 ClickHouse 節點,每次找到最大最小節點,平衡 part 至局部最優,直到輪詢 ClickHouse 節點結束。

設計如下圖所示:

圖片

7.3 平衡計劃

根據平衡算法,可以得出集羣中節點計劃的遷入、遷出情況。平衡單位爲表級別,遷移粒度到 part,可以理解爲表內部 part 平衡。

如下圖所示,可以看到表平衡前後的平衡度,以及節點 1 計劃的遷入、遷出情況。平衡計劃生成完成後,可以根據需要選擇執行特定的平衡計劃。

圖片

圖片

7.4 重平衡執行流程

在執行平衡計劃的過程中,如何準確、高效地將 part 遷入和遷出?如何保證原子性,避免數據出現丟失或重複的問題?如何限流,避免因平衡佔用過多的資源,影響集羣的穩定性?

整體設計如下圖所示。

圖片

08. ClickHouse 應用優化實踐

在支持 UBA 場景各項功能模塊的過程中,我們針對 ClickHouse 的查詢,存儲等方面做了大量應用優化工作。下面選取其中幾個優化點做簡單介紹。

8.1 查詢下推

ClickHouse 中的針對分佈式表的查詢會被改寫成對 local 表的查詢併發送到集羣各個 shard 執行,然後將各個 shard 的中間計算結果收集到查詢節點做合併。當中間計算結果很大時,比如 countDistinct、 windowFunnel 函數等,查詢節點的數據收集和數據合併可能成爲整個查詢的性能瓶頸。

查詢下推的思路就是儘量將計算都下推到各個 shard 執行,查詢節點僅收集合並少量的最終計算結果。不過,也不是所有查詢都適合做下推優化,滿足以下兩個條件的查詢可以考慮做下推優化:

下面,我們以上文中提到的漏斗分析爲例,闡述一下如何做查詢下推。

圖片

上圖是用 windowFunnel 函數實現漏斗分析的一個 SQL,如圖中 “執行步驟” 所示,該查詢需要從各 shard 收集大量數據並在查詢節點完成計算,會產生大量數據傳輸和單點計算量。

我們先使用配置 distributed_group_by_no_merge 做了一版下推優化:

圖片

優化 SQL-V1 將 windowFunnel 的計算下推到各個 shard 執行,僅在查詢節點對 windowFunnel 的最終結果做聚合計算。在我們的場景下,該版本較上一版本性能提升了 5 倍以上。

爲了更進一步做查詢下推,我們利用 cluster + view 的函數組合,將聚合查詢進一步下推:

圖片

優化 SQL-V2 的性能較優化 SQL-V1 進一步提升 30+%.

8.2 Array 和 Map 的跳數索引支持

UBA 場景中的事件數據有很多公共屬性和私有屬性,公共屬性被設計爲表的固定字段,而私有屬性因爲各個事件不盡相同,所以採用 Array/Map 來存儲。最初的設計是採用兩個數組分別存儲屬性名和屬性值,ClickHouse 支持 Map 結構後,則在後續模塊中採用 Map 來滿足類似需求。無論是 Array 還是 Map,最初都不支持創建跳數索引,所以在其他索引字段過濾效果有限的情況下,針對 Array 和 Map 的操作可能會成爲查詢的性能瓶頸。

針對這個問題,我們給 Array 和 Map 加上了 Bloom filter 等跳數索引支持,針對 Map 僅對其 key 構建索引。在某些出現頻率較低的私有屬性過濾場景下,Array/Map 的跳數索引可以收穫數倍的性能提升。

8.3 壓縮算法優化

ClickHouse 常用的數據壓縮方式有三種,分別爲 LZ4、LZ4HC 以及 ZSTD。針對不同的數據類型,數據分佈方式來使用特定的編碼方式可以大大提高數據壓縮率,以減少存儲成本。

針對 UBA 場景,我們測試了不同壓縮算法的壓縮率,寫入性能,查詢性能。相較默認的 LZ4,ZSTD(1) 在壓縮率上普遍可以節省 30% 以上的存儲空間,查詢性能方面未見明顯差異,不過寫入性能在某些場景下有 20% 左右的下降。由於 UBA 場景數據存儲壓力較大,同時對數據時效性要求不是很高,因此我們最終選擇了 ZSTD(1) 作爲主要的壓縮方式。

09. 下一步工作

9.1 多業務通用模型支持

UBA 場景的泛化形態實際是人 + 內容 + 行爲,例如用戶可以在觀看場景產出彈幕行爲或者點贊行爲,這類數據不同於傳統的 SDK 日誌數據具有通用的埋點格式,但我們可以通過抽象映射到通用行爲聚合模型上來,來實現對服務端日誌的行爲分析。目前我們正在對社區服務端日誌和其他非埋點規範的業務 SDK 日誌進行泛化支持,儘可能複用已有能力提高用戶查詢和分析效率。

9.2 Clickhouse 增強多維過濾場景支持

在 UBA 場景下,同一張表可能在多個模塊中使用到,比如,用戶行爲事件數據在事件分析等分析模塊中使用,同時在單用戶行爲明細查詢中會使用到。這兩種使用場景下對錶的查詢是基於不同過濾維度的,但 clickhouse 目前的主鍵索引很難同時對多個維度過濾都有較好過濾效果,因此很難同時滿足多個場景下的查詢性能要求。我們已經完成了 ZOrder 索引的開發,目前正在開發相應的編碼類型,使得 UBA 場景下的數據可以使用 ZOrder index 同時支持多個維度的高效查詢。

參考鏈接:

[1]https://mp.weixin.qq.com/s/hZsZoaMfEo3G51OLv2keyQ

[2]https://www.ics.uci.edu/~goodrich/teach/cs165/notes/BinPacking.pdf

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/F6VREV99NcXD0lU_RUMuTA