基于改進(jìn)字典的大數(shù)據(jù)多維分析加速實(shí)踐
一、背景
OLAP場(chǎng)景是大數(shù)據(jù)應(yīng)用中非常重要的一環(huán),能夠快速、靈活地滿足業(yè)務(wù)各種分析需求,提供復(fù)雜的分析操作和決策支持。B站主流湖倉(cāng)使用Iceberg存儲(chǔ),通過建表優(yōu)化可以實(shí)現(xiàn)常規(guī)千萬級(jí)的指標(biāo)統(tǒng)計(jì)秒級(jí)查詢,這樣就能快速搭建可視化報(bào)表,但當(dāng)數(shù)據(jù)量達(dá)到億級(jí)、需要交叉分析維度復(fù)雜多表情況下,想要支持秒級(jí)就變得困難。因此B站數(shù)據(jù)分析或者數(shù)據(jù)開發(fā)同學(xué)為了能有秒級(jí)響應(yīng)的報(bào)表,需要通過ETL grouping sets 提前設(shè)計(jì)要參與多維分析的維度和指標(biāo),然后在ADS層離線計(jì)算好對(duì)應(yīng)的數(shù)據(jù)cube。這有點(diǎn)類似Kylin的預(yù)計(jì)算模式,區(qū)別是查詢效率和查詢SQL復(fù)雜度要更高,畢竟Kylin底層是KV存儲(chǔ)并且做了SQL解釋器,而原始grouping sets模式得讓下游自己選cube切片。比如Push業(yè)務(wù)DWB表幾十億數(shù)據(jù)量,想要快速支持十幾個(gè)維度和十幾個(gè)指標(biāo)秒級(jí)交叉分析,只能開發(fā)提前配置好要參與分析的維度組合,在可視化界面也需要提前說明只支持這幾個(gè)維度組合。
OLAP引擎中Kylin 也逐漸被淘汰了,因?yàn)橹荒茴A(yù)計(jì)算而不能現(xiàn)算,要滿足新的分析維度視角和指標(biāo)得重新配置和數(shù)據(jù)同步成本巨大,當(dāng)然ETL grouping sets這種更原始的方式成本只會(huì)更大,以下列舉了數(shù)據(jù)平臺(tái)ETL任務(wù)中用到grouping sets 預(yù)計(jì)算所使用資源開銷:
ETL種類 | 任務(wù)數(shù) | CPU開銷/cpu_vcore_d | 內(nèi)存開銷/mmr_tb_d |
多維分析任務(wù) | 563(0.7%) | 4448.90(10.8%) | 5.43(33.2%) |
當(dāng)前多維分析的任務(wù)只占總?cè)蝿?wù)數(shù)不到1%,卻貢獻(xiàn)了10.8%的CPU日開銷和33.2%的內(nèi)存日開銷,實(shí)屬浪費(fèi),下游ADS報(bào)表使用極其繁瑣,運(yùn)維成本較大。
B站當(dāng)前主流OLAP引擎是ClickHouse,和主流引擎Doris、StarRocks一樣,ClickHouse同樣采用MPP架構(gòu)和列式存儲(chǔ)格式,可以滿足億級(jí)數(shù)據(jù)量的秒級(jí)甚至亞秒級(jí)查詢,其專注于OLAP場(chǎng)景優(yōu)化,但在join操作和讀QPS上不如Doris、StarRocks高效。報(bào)表場(chǎng)景下對(duì)QPS要求不高,但join能力要能跟上,而小數(shù)據(jù)量join 性能影響就不大了,為此我們計(jì)劃將大表加工成聚合小表,即滿足多維分析場(chǎng)景,也能滿足外部擴(kuò)展join場(chǎng)景。
為了保證指標(biāo)和維度一致性,避免重復(fù)加工和數(shù)據(jù)冗余、歧義,我們將這種ClickHouse聚合加速功能集成在指標(biāo)服務(wù)平臺(tái)(OneService,OS),由OS統(tǒng)一做加速配置、API注冊(cè)、SQL查詢解析。此外,聚合加速的難點(diǎn)在于對(duì)去重指標(biāo)的處理,因此除了涉及聚合數(shù)據(jù)模型設(shè)計(jì)、數(shù)據(jù)同步、OS工程集成,本文還會(huì)詳細(xì)闡述如何將字符串的去重實(shí)體轉(zhuǎn)化為ClickHouse的去重聚合實(shí)體集合 groupBitmap(即RoaringBitmap,RBM),如何生成映射字典去滿足多個(gè)RBM能夠快速交并差運(yùn)算。
二、總體架構(gòu)設(shè)計(jì)圖
圖片
三、數(shù)據(jù)方案設(shè)計(jì)
3.1 模型設(shè)計(jì)
多維分析加速的核心是如何設(shè)計(jì)數(shù)據(jù)聚合方案,滿足多維快速分析場(chǎng)景。按照指標(biāo)是否去重,所有的多維聚合分析指標(biāo)按照其所在數(shù)倉(cāng)模型可以被拆分成兩部分:一、無需去重計(jì)算,即所需要的指標(biāo)直接在需要分析的維度上進(jìn)行聚合處理,比如max、count、sum等,這類對(duì)應(yīng)數(shù)倉(cāng)模型通常無需關(guān)注實(shí)體或者實(shí)體粒度唯一;二、 需要去重計(jì)算,例如UV(Unique Vistor)、PU(Paid User)、RU(Revenue User)、DAU(Daily Active User)等,如果所在模型粒度不是要去重的實(shí)體,則必須使用distinct去重運(yùn)算 。實(shí)際數(shù)倉(cāng)模型設(shè)計(jì)中兩者皆有的情況占大多數(shù),即實(shí)體不唯一,而實(shí)體產(chǎn)生的業(yè)務(wù)指標(biāo)又可累加,因此需要同時(shí)設(shè)計(jì)兩種聚合模式。以播放為例,假如有張播放DWB表,每日1億條記錄,定義如下:
Play_dwb_tbl(
uid bigint comment '用戶ID'
,deviceID string comment '設(shè)備ID'
,area_id bigint comment '分區(qū)ID'
,area_name string comment '分區(qū)名'
,uid_city string comment '用戶自填城市'
,uid_gender string comment '用戶自填性別'
,play_duration bigint commnet '播放時(shí)長(zhǎng)'
,dt string '統(tǒng)計(jì)日期'
)
想要統(tǒng)計(jì)在不同維度組合的播放時(shí)長(zhǎng)、播放用戶數(shù)、播放設(shè)備數(shù)。
直接聚合模式,即對(duì)無需去重的指標(biāo)組合其維度生成聚合表。以上面案例舉例,播放時(shí)長(zhǎng)指標(biāo)不依賴實(shí)體可直接累加,生成如下表:
Play_dwb_tbl_aggr_directly(
area_id bigint comment '分區(qū)ID'
,area_name string comment '分區(qū)名'
,uid_city string comment '用戶自填城市'
,uid_gender string comment '用戶自填性別'
,paly_duration bigint commnet '播放時(shí)長(zhǎng)'
,dt string '統(tǒng)計(jì)日期'
)
結(jié)果每日大概1萬條記錄,將這1萬條記錄導(dǎo)入到OLAP引擎可以快速滿足任意多維分析需求。這種模式比較簡(jiǎn)單和傳統(tǒng),本文重點(diǎn)講另一種模式。
去重聚合模式,即對(duì)需要去重的指標(biāo)生成聚合表。以上面案例舉例,播放用戶數(shù)、播放設(shè)備數(shù)就是分別對(duì)用戶和設(shè)備兩種不同實(shí)體的聚合。分析其ER關(guān)系可知,性別、城市由uid唯一映射,但同一個(gè)設(shè)備可能有多個(gè)uid,因此不能被設(shè)備ID唯一映射;分區(qū)ID和分區(qū)名屬于聯(lián)合維度,一一對(duì)應(yīng)不可分割。根據(jù)ER關(guān)系劃分維度組合,相同維度分析不同指標(biāo)底層的實(shí)現(xiàn)復(fù)雜度相差極大。對(duì)于播放設(shè)備數(shù),uid和分區(qū)是其必要維度,uid雖然是實(shí)體不需要參與分析,但其關(guān)聯(lián)維度需要放入,最終全維度放入聚合維度池;對(duì)于播放用戶數(shù),由于存在實(shí)體的唯一映射,可以劃分3個(gè)維度組合(dt、uid_city)、 (dt、uid_gender)、 (dt、area_id、area_name) 放入聚合維度池,可以利用三個(gè)維度對(duì)應(yīng)實(shí)體集合交并實(shí)現(xiàn)多維分析(這里實(shí)體集合的生成涉及字典服務(wù),后文會(huì)詳解),相對(duì)于全維度組合其組合枚舉值大大減少。去重聚合模式的維度劃分邏輯如下圖所示:
圖片
對(duì)于entityM,根據(jù)ER關(guān)系中有多少個(gè)唯一映射就可以拆多少個(gè)維度集合,剩下不能再唯一映射的維度當(dāng)成實(shí)體關(guān)聯(lián)維度打包成一個(gè)維度集合。
通過以上實(shí)例分析,不管哪種聚合模式都可能存在維度組合爆炸情況,極端情況其聚合模型的記錄數(shù)和原模型一致,就沒有聚合加速的優(yōu)勢(shì)。實(shí)際情況下我們的聚合模型是要面向可視化報(bào)表、面向?qū)嶋H業(yè)務(wù)分析場(chǎng)景,不可能需要到實(shí)體粒度的維度組合展現(xiàn),因此在生產(chǎn)聚合模型的時(shí)候系統(tǒng)會(huì)自動(dòng)檢查參與分析的維度組合數(shù)量是不是超標(biāo)。此外還有一些其他規(guī)則,比如日期維度需要強(qiáng)制選擇,去重實(shí)體在字典服務(wù)中存在映射字典等等。整體數(shù)據(jù)流程見下圖:
圖片
接下來重點(diǎn)講下去重聚合模式(RBM aggr)在ClickHouse中的的設(shè)計(jì)、同步實(shí)現(xiàn)和查詢。
3.2 Schema設(shè)計(jì)
圖片
dim_name和dim_value包含多種維度組合,分別代表維度名和維度值。實(shí)際查詢中必用ind_name,dim_name大部分場(chǎng)景都會(huì)使用, 因此設(shè)計(jì)成LowCardinality(String)意味著它可以有效地壓縮重復(fù)字符串值,從而減少存儲(chǔ)空間的使用,其使用的字典編碼還可以提高查詢的性能。在統(tǒng)計(jì)每日的去重實(shí)體數(shù)時(shí)不需要ind_name,這也是很頻繁的場(chǎng)景,因此我們還添加了Projection操作預(yù)計(jì)算:
PROJECTION projection_logdate (
SELECT dt, ind_name, groupBitmapOrState(ind_rbm) ,count(1)
GROUP BY dt, ind_name
)
這樣就沒必要掃描所有dim_name去重復(fù)統(tǒng)計(jì)(每個(gè)dim_name算出的來的去重實(shí)體數(shù)必然一樣),另外在執(zhí)行'!='操作的時(shí),完全可以拿這個(gè)預(yù)計(jì)算結(jié)果和右邊的維度值對(duì)應(yīng)的ind_rbm做差集馬上可以得到結(jié)果,性能和算'='一樣。此外dim_value添加布隆過濾索引,針對(duì)多維篩選可以加速分析。
3.3 數(shù)據(jù)同步
圖片
完整的數(shù)據(jù)同步圖如上圖所示。dwb_tbl為待同步的原始數(shù)據(jù)模型,sync_tbl為待同步到ClickHouse的數(shù)據(jù)模型,和目標(biāo)表的Schema一樣,整個(gè)過程核心包含三個(gè)步驟,以下詳述S1, S2, S3三個(gè)步驟的內(nèi)容。
S1:產(chǎn)出預(yù)加工模型,其結(jié)構(gòu)如下:
Temp_1(
dt string comment '日期'
,ind_name string comment '指標(biāo)名'
,ind_value string comment '實(shí)體值'
,dims_map map<string,string> comment '{dim_name:dim_value}'
)
目標(biāo)是將不同的原數(shù)據(jù)模型加工成統(tǒng)一模型表以適配后續(xù)流程。用戶選擇要分析的維度和指標(biāo),而這些維度和指標(biāo)在OS上可以二次定義或者衍生,OS自動(dòng)生成產(chǎn)出這些維度和指標(biāo)的SQL,之后統(tǒng)一封裝成預(yù)加工模型。這里ind_value為要去重指標(biāo)的實(shí)體值,這個(gè)實(shí)體值不管是數(shù)值還是字符串,統(tǒng)一存儲(chǔ)為字符串格式。dims_map聚合了后續(xù)需要的dim_name和dim_value,目的是保證ind_value粒度,后續(xù)轉(zhuǎn)化字典的時(shí)候不需要重復(fù)請(qǐng)求,降低計(jì)算成本。
S2:獲取映射字典,即將ind_value轉(zhuǎn)化為預(yù)設(shè)的稠密字典值,實(shí)現(xiàn)字符轉(zhuǎn)數(shù)字。通過OS拿到指標(biāo)實(shí)體所用的字典,這里優(yōu)先會(huì)請(qǐng)求已有的離線映射字典表(若未就緒自動(dòng)取最新分區(qū)數(shù)據(jù)),如果獲取不到再請(qǐng)求字典服務(wù)申請(qǐng)新的字典,新的記錄后續(xù)會(huì)離線存放到映射字典表中,避免后續(xù)再重復(fù)請(qǐng)求,降低字典服務(wù)壓力,讓億級(jí)別數(shù)據(jù)量能分鐘級(jí)生成字典。
S3:產(chǎn)出要同步到ClickHouse的數(shù)據(jù)模型。由于目標(biāo)表是分布式CK聚合表,這里需要根據(jù)實(shí)體的字典值映射到要寫入的shard,按照指標(biāo)名+維度+shard粒度對(duì)這些字典值進(jìn)行RBM聚合操作。
3.4 數(shù)據(jù)查詢
查詢場(chǎng)景有很多,這里主要說明下多維分析在已知篩選條件下的查詢實(shí)現(xiàn)。以上述播放數(shù)據(jù)為例,對(duì)應(yīng)ClickHouse本地表為Play_ck_tbl_local,分布式表為Play_ck_tbl,分別查詢某日的上海用戶在分區(qū)ID為1下的播放指標(biāo):
-- example1.查詢播放用戶數(shù)
select
sum(`播放用戶數(shù)`)
from cluster('XXX', view(
select
bitmapAndCardinality(a.ind_rbm, b.ind_rbm) AS `播放用戶數(shù)`
from(
select
ind_rbm
from Play_ck_tbl_local
where dt='XX' and ind_name='播放用戶數(shù)' and dim_name='uid_city' and dim_value[1]='上海'
)a join (
select
ind_rbm
from Play_ck_tbl_local
where dt='XX' and ind_name='播放用戶數(shù)' and dim_name='area_id' and dim_value[1]='1'
)b on 1=1
)tmp
-- example2.查詢播放設(shè)備數(shù)
select
bitmapCardinality(groupBitmapOrState(ind_rbm)) `播放設(shè)備數(shù)`
from Play_ck_tbl
where dt='XX' and ind_name='播放設(shè)備數(shù)' and dim_name = 'area_id,area_name,uid_city,uid_gender'
and dim_value[1]='1' and dim_value[3]='上海'
兩個(gè)指標(biāo)維度一樣,但查詢邏輯有很大不同。播放用戶數(shù)指標(biāo)對(duì)應(yīng)多個(gè)維度集合存儲(chǔ),需要將涉及到的維度集取出后做關(guān)聯(lián)取交集運(yùn)算,由于本地表是按照實(shí)體字典做了分shard處理,因此只要實(shí)現(xiàn)本地Colocate Join邏輯后求和即可得到結(jié)果。播放設(shè)備數(shù)指標(biāo)只有一個(gè)維度集合,這個(gè)維度集合包含了想要參與多維分析的所有維度值組合,因此只要限制維度值數(shù)組指定位置取值為篩選條件即可。
需要注意的是前面限制維度基數(shù)的原因在這里可以體現(xiàn)。如果有多個(gè)維度集合需要join運(yùn)算,要求每個(gè)維度集合不能太多,我們利用的是ClickHouse小數(shù)據(jù)量join能力,如果太大會(huì)帶來性能損耗,無法滿足秒級(jí)查詢要求。
四、字典服務(wù)方案的優(yōu)化演進(jìn)
4.1 基于Snowflake算法的字典服務(wù)
Snowflake是Twitter開源的一種分布式ID生成算法。基于64位數(shù)實(shí)現(xiàn),下圖為Snowflake算法的ID構(gòu)成圖:
圖片
- 第1位置為0。第2-42位是相對(duì)時(shí)間戳,通過當(dāng)前時(shí)間戳減去一個(gè)固定的歷史時(shí)間戳生成。第43-52位是機(jī)器號(hào)workerID,每個(gè)Server的機(jī)器ID不同。第53-64位是自增ID。通過時(shí)間+機(jī)器號(hào)+自增ID的組合來實(shí)現(xiàn)完全分布式的ID下發(fā)
- Leaf提供了Java版本的實(shí)現(xiàn),同時(shí)對(duì)Zookeeper生成機(jī)器號(hào)做了弱依賴處理,即使Zookeeper有問題,也不會(huì)影響服務(wù)。Leaf在第一次從Zookeeper拿取workerID后,會(huì)在本機(jī)文件系統(tǒng)上緩存一個(gè)workerID文件。即使ZooKeeper出現(xiàn)問題,同時(shí)恰好機(jī)器也在重啟,也能保證服務(wù)的正常運(yùn)行。這樣做到了對(duì)第三方組件的弱依賴,一定程度上提高了SLA。
4.2 用離散數(shù)據(jù)構(gòu)建Bitmap的問題
基于snowflake算法生成的字典值在連續(xù)數(shù)值空間上的分布是相對(duì)離散的,所以基于snowflake字典構(gòu)建的bitmap中的數(shù)據(jù)也是比較離散的。ClickHouse使用RoaringBitmap存儲(chǔ)bitmap數(shù)據(jù),而由于RoaringBitmap的結(jié)構(gòu)設(shè)計(jì),使其并不適合用于存儲(chǔ)和計(jì)算由離散數(shù)據(jù)構(gòu)建而成的bitmap。下面我們從RoaringBitmap的結(jié)構(gòu)原理出發(fā),闡述離散數(shù)據(jù)分布下bitmap存儲(chǔ)和計(jì)算所面臨的問題。
一個(gè)32位RoaringBitmap的內(nèi)部結(jié)構(gòu)如下圖:
圖片
首先以數(shù)字的高16位來確定分桶,然后低16存儲(chǔ)到對(duì)應(yīng)container中。
根據(jù)存儲(chǔ)數(shù)據(jù)的數(shù)量和分布情況,會(huì)使用不同種類的container來使計(jì)算更加高效。
- Array Container:桶內(nèi)數(shù)據(jù)小于4096個(gè),計(jì)算效率低
- Bitmap Container:桶內(nèi)數(shù)據(jù)大于4096個(gè),計(jì)算效率高
- Run Container:對(duì)連續(xù)數(shù)字使用RLE編碼來壓縮空間,最省空間
針對(duì)64位的bitmap,則是先將64位整型的高32位分桶,低32位則使用RoaringBitmap32表示。
通過上述對(duì)RoaringBitmap結(jié)構(gòu)的分析,我們可以看出,存入bitmap的數(shù)據(jù)連續(xù)性越高,則bitmap的存儲(chǔ)空間就越小,計(jì)算效率也越高。
基于Snowflake算法生成的字典值連續(xù)性低,高位稠密,低位稀疏,導(dǎo)致生成的roaring bitmap中包含大量的array container,從而導(dǎo)致bitmap計(jì)算消耗大量?jī)?nèi)存資源,且計(jì)算效率非常低。
4.3 ClickHouse稠密字典編碼服務(wù)體系
為了解決RoaringBitmap因數(shù)據(jù)連續(xù)性低而導(dǎo)致存儲(chǔ)計(jì)算效率低的問題,我們圍繞ClickHouse建設(shè)了一套稠密字典編碼服務(wù)體系。
術(shù)語說明:
- 正向查詢:用原始key值查詢對(duì)應(yīng)的字典值value。
- 反向查詢:用字典值value查詢對(duì)應(yīng)的原始key。
4.3.1 架構(gòu)設(shè)計(jì)圖
圖片
4.3.2 功能模塊簡(jiǎn)介
稠密字典編碼服務(wù)體系包括以下幾個(gè)模塊:
- ClickHouse字典編碼模塊:在ClickHouse內(nèi)部新增BidirectionalDictionary功能支持,同時(shí)支持了各種dictionary functions,從支持?jǐn)?shù)據(jù)在寫入過程中在ClickHouse內(nèi)部完成字典編碼與轉(zhuǎn)換,最終生成連續(xù)字典值的bitmap數(shù)據(jù)。
- Dict Service:提供字典管理(新建,刪除等),字典keyToValue,valueToKey查詢代理,查詢緩存,批量查詢等功能。既可供ClickHouse內(nèi)部生成字典值bitmap數(shù)據(jù)使用,也可通過Dict Client, Dict UDF供外部數(shù)據(jù)同步組件用于生成bitmap數(shù)據(jù)后導(dǎo)入ClickHouse。
- Dict Client:Dict Service的客戶端,用于連接Dict Service完成字典正向查詢/更新和字典反向查詢,同時(shí)提供查詢緩存,批量查詢等功能。
- Dict UDF:為數(shù)據(jù)同步組件提供Spark UDF/UDAF用于字典值轉(zhuǎn)換及bitmap數(shù)據(jù)生成,以便在ClickHouse之外預(yù)先生成字典編碼的bitmap數(shù)據(jù)。
- Distribute KV:基于RocksDB的分布式KV存儲(chǔ),用于存儲(chǔ)正反向字典數(shù)據(jù),同時(shí)提供全局自增器生成自增的字典值。
4.3.3 Dict Service實(shí)現(xiàn)方案詳述
Dict Service維護(hù)了所有的字典元數(shù)據(jù),客戶端連接Dict Service做字典正向和反向查詢。Dict Service的運(yùn)行實(shí)例是無狀態(tài)無鎖的,所以Dict Service可以做分布式可擴(kuò)展部署,狀態(tài)同步完全依靠分布式KV存儲(chǔ)進(jìn)行,通過各種容錯(cuò)機(jī)制保證字典數(shù)據(jù)的正確性。
字典正向查詢與更新流程圖:
圖片
字典正向查詢與更新流程步驟說明:
- 輸入的keys,首先會(huì)查詢正向字典,如果能夠全部命中,則向客戶端返回values。
- 對(duì)于未命中的keys,首先將其進(jìn)行排序去重,然后使用全局自增器分配映射值。
- 將完成映射的values → keys批量寫入反向字典。
- 使用CAS操作,將keys → values寫入正向字典,若發(fā)生CAS沖突,則將該key的value替換為old value。
- 向客戶端返回values。
- 回收掉由于CAS沖突而未使用到的values,用于下次分配。
對(duì)于字典值,我們?cè)贙V存儲(chǔ)中采用大端方式表示,這樣可以使得反向查詢效率更高,原因如下:
Dict Service應(yīng)用于bitmap計(jì)算場(chǎng)景,用戶在反向查詢時(shí),從bitmap讀取數(shù)值序列并請(qǐng)求Dict Service做批量查詢,取出的查詢目標(biāo)數(shù)值序列是有序的,而使用大端方式存儲(chǔ)字典值能夠保證字典值在KV存儲(chǔ)中也是按照數(shù)值順序排序的,這樣查詢目標(biāo)數(shù)值序列和存儲(chǔ)的字典值序列就都是按同樣順序排好序的,這對(duì)于使用LSM結(jié)構(gòu)的RocksDB而言查詢會(huì)更加高效。
4.4 bitmap計(jì)算的優(yōu)化效果
基于B站真實(shí)用戶行為數(shù)據(jù),我們分別基于Snowflake算法字典服務(wù)和ClickHouse稠密字典編碼服務(wù)對(duì)用戶設(shè)備id做字典編碼后,對(duì)比了兩者在bitmap計(jì)算場(chǎng)景下的性能差異:
說明:測(cè)試數(shù)據(jù)為用戶行為表連續(xù)七天某一時(shí)間段的所有數(shù)據(jù),用戶設(shè)備id基數(shù)約為1000萬。
五、工程應(yīng)用接入
5.1 前置工作
邏輯模型定義
用戶期望對(duì)于已有的明細(xì)表進(jìn)行物化加速, 首先需預(yù)先定義好數(shù)據(jù)模型, 包括模型中包含的指標(biāo)維度, 其中
- 指標(biāo)定義: 對(duì)某實(shí)體進(jìn)行去重計(jì)數(shù)來計(jì)算指標(biāo), 則較為適用于rbm加速方案。反之則使用原有的聚合計(jì)算加速方式即可, 如sum, max, min, count等計(jì)算指標(biāo)等。
- 如對(duì)[播放記錄表]中設(shè)備與用戶進(jìn)行去重計(jì)數(shù)計(jì)算, 可產(chǎn)出[播放設(shè)備數(shù)]及[播放用戶數(shù)]指標(biāo)。
圖片
- 實(shí)體定義: 一般來說會(huì)對(duì), 例如: 設(shè)備id, 用戶id等一系列id類信息進(jìn)行去重計(jì)數(shù), 此類信息字段即為計(jì)算實(shí)體, 并會(huì)在后續(xù)流程中, 通過字典服務(wù)將實(shí)體轉(zhuǎn)換為稠密的bitmap結(jié)構(gòu)(rbm)。
- 維度定義: 在模型中定義, 按某些維度對(duì)相關(guān)指標(biāo)進(jìn)行分析。簡(jiǎn)單理解就是需要對(duì)于哪些維度進(jìn)行分組聚合(group by)。
- 主鍵維定義: 對(duì)于需要計(jì)算的實(shí)體字段, 必須是模型定義中的[主鍵]或[與某些維度組合后為主鍵]進(jìn)行rbm數(shù)據(jù)生產(chǎn), 否則會(huì)出現(xiàn)指標(biāo)計(jì)算出現(xiàn)誤差。
- 此處設(shè)計(jì)是基于上文[數(shù)據(jù)設(shè)計(jì)]部分所闡述的[維度組合]方案的一種讓步, 考慮到用戶對(duì)于維度組合的理解成本過高, 且產(chǎn)品交互設(shè)計(jì)復(fù)雜, 進(jìn)行了一定的取舍。通過主鍵維定義的方式解決計(jì)算誤差問題, 雖會(huì)犧牲一定的查詢性能與更多的數(shù)據(jù)存儲(chǔ), 但對(duì)于用戶來說設(shè)置主鍵維更好理解, 操作也更便捷。未來支持維度組設(shè)置, 也可依據(jù)一定的策略規(guī)則自動(dòng)識(shí)別維度組, 減少用戶配置成本。
- 用戶開啟rbm加速開關(guān), 并設(shè)置合理的主鍵信息, 即可使用rbm的加速特性
圖片
主鍵維定義case
假設(shè)我們有如下數(shù)據(jù), 其中buvid在pid維度下唯一, 并對(duì) pid, is_new 2個(gè)維度字段產(chǎn)出dau指標(biāo)(對(duì)buvid進(jìn)行去重計(jì)數(shù)計(jì)算)
當(dāng)我們要分析, pid = 'android' and is_new = 'n' 維度下的dau時(shí)
- 常規(guī)sql分析方式:
select pid, is_new, count(distinct buvid) as dau
from tbl where pid = 'android' and is_new = 'n'
- 產(chǎn)出結(jié)果為 dau=1, 其中命中此條件的buvid為'b'
- 未定義主鍵維方式:
- 對(duì) pid = 'android' 與 is_new = 'n' 的2個(gè)rbm交集計(jì)算后得出的rbm為 [a=1, b=1]
- 產(chǎn)出的結(jié)果為 dau=2, 可見這個(gè)結(jié)果是錯(cuò)誤的
- 定義主鍵維方式:
- 定位到 [pid, is_new] = [android, n] 的rbm為 [a=0, b=1]
- 產(chǎn)出的結(jié)果為 dau=1, 符合預(yù)期
示例:
假設(shè)我們已經(jīng)定義好一個(gè)模型
圖片
5.2 數(shù)據(jù)加速
在進(jìn)行完模型定義后, 就可對(duì)其數(shù)據(jù)進(jìn)行加速處理, 我們選擇將加速數(shù)據(jù)寫入olap引擎clickhouse中。
去重計(jì)數(shù)指標(biāo)
首先對(duì)于需要去重計(jì)數(shù)的指標(biāo), 通過以上模型定義, 系統(tǒng)自動(dòng)翻譯出如下SQL, 提供至[數(shù)據(jù)同步]S1部分闡述的任務(wù)。
其中需要查詢出每個(gè)實(shí)體命中各維度的維度值有哪些。
SELECT
log_date AS log_date,
CASE WHEN grouping(`dau`) = 0 THEN 'dau' END AS ind_name, -- 指標(biāo)名
CASE WHEN grouping(`dau`) = 0 THEN `dau` END AS ind_value, -- 指標(biāo)對(duì)應(yīng)實(shí)體值,如此例中的buvid
map_group(dim_map) AS dims_map -- 分組聚合所有map中的元素,即為實(shí)體命中各維度的維度值
FROM
(
SELECT
log_date AS log_date,
COALESCE(CAST(dau AS string), '') AS dau,
map(
'pid',
COALESCE(CAST(pid AS string), ''), -- 實(shí)體命中的pid
'pid,device_brand', -- 主鍵維列表 拼接 單個(gè)非主鍵維
CONCAT_WS(',',
COALESCE(CAST(pid AS string), ''), -- 實(shí)體命中的pid
COALESCE(CAST(device_brand AS string), '') -- 實(shí)體命中的設(shè)備品牌
),
... -- 主鍵維列表 拼接 其余單個(gè)非主鍵維
) AS dim_map
FROM
( ... ) l1 -- 模型定義標(biāo)準(zhǔn)化
) l2
GROUP BY
log_date,
dau
GROUPING SETS((log_date, `dau`)) -- 按不同指標(biāo)計(jì)算grouping sets
SQL查詢出的結(jié)果樣例如下, 其中ind_name表示指標(biāo)名, ind_value是產(chǎn)出此指標(biāo)的某個(gè)實(shí)體, dims_map則是該實(shí)體命中各維度的維度值。
圖片
例如: 第一行中的buvid命中的pid=1124, device_brand=official, is_new_user=0。
又因主鍵維定義, 實(shí)際會(huì)將device_brand, is_new_user維度分別與pid主鍵維進(jìn)行組合處理, log_date本身是單獨(dú)的分區(qū)字段, 則無需再進(jìn)行組合。
[數(shù)據(jù)同步]任務(wù)獲取到數(shù)據(jù)后, 即會(huì)進(jìn)行實(shí)體字段轉(zhuǎn)換并將數(shù)據(jù)寫入ck聚合表中, 具體結(jié)構(gòu)見上文[Schema 設(shè)計(jì)], 此處不再贅述。
ck聚合表數(shù)據(jù)樣例如下, 通過bitmapCardinality函數(shù)可計(jì)算出rbm的基數(shù)值, 業(yè)務(wù)含義上即為該維度下DAU指標(biāo)。后續(xù)進(jìn)行多維分析時(shí), 針對(duì)rbm進(jìn)行交并計(jì)算即可得到期望值。
圖片
非去重計(jì)數(shù)指標(biāo)
本樣例中即為vv指標(biāo), 通過模型定義中的維度, 簡(jiǎn)單分組聚合計(jì)算加速即可, SQL樣例如下:
SELECT
log_date, pid, device_brand, is_new_user, -- 分析維度
SUM(vv) AS vv -- sum計(jì)算指標(biāo)
FROM
( ... ) l1 -- 模型定義標(biāo)準(zhǔn)化
GROUP BY
log_date, pid, device_brand, is_new_user -- 分析維度
物化模型
數(shù)據(jù)服務(wù)對(duì)于邏輯模型加速后的數(shù)據(jù), 會(huì)對(duì)應(yīng)生成物化模型的定義信息。rbm加速與聚合加速會(huì)分別創(chuàng)建2個(gè)物化模型定義, 兩者包含的維度一致, 而指標(biāo)不同。定義對(duì)比如下:
邏輯模型 | 聚合物化模型 | rbm物化模型 | |
維度 | pid, device_brand, is_new_user | ||
指標(biāo) | dau, vv - 包含所有指標(biāo) | vv - 包含非去重計(jì)數(shù)指標(biāo) | dau - 僅包含去重 |
圖片
5.3 查詢分析
基于以上物化加速后的數(shù)據(jù), 數(shù)據(jù)服務(wù)即可通過物化模型定義, 翻譯出對(duì)應(yīng)的查詢SQL。其中應(yīng)用了[星座模型]的查詢特性, 將2個(gè)物化模型, 分別作為2個(gè)事實(shí)表模型對(duì)待, 進(jìn)行處理。復(fù)用了OneService原有的多模型查詢能力。
SQL對(duì)比
rbm物化表對(duì)于查詢SQL的翻譯與普通數(shù)據(jù)表存在差異, 以下對(duì)比了2種不同SQL的處理特點(diǎn):
圖片
以下以最復(fù)雜的 [主鍵維+多維非主鍵維分析dau與vv指標(biāo)] 查詢模式舉例:
維度過濾 log_date = '20240609' and pid = 13 and is_new_user = 1
SELECT
xxx, -- 分析維度
0 AS vv, sum(dau) AS dau -- rbm物化模型不產(chǎn)出vv指標(biāo),產(chǎn)出去重計(jì)數(shù)指標(biāo)dau,此處應(yīng)用ck的shard特性,對(duì)多shard間的rbm基數(shù)進(jìn)行sum計(jì)算
FROM
cluster('xxx', view( -- ck多shard查詢優(yōu)化
SELECT
cast(device_brand AS String) AS device_brand, xxx, -- 由于rbm物化表中維度值均為string類型,需將其轉(zhuǎn)換回原始類型
ind_name AS ind_name, sum(bitmapAndCardinality(device_brand_tbl.ind_rbm, is_new_user_tbl.ind_rbm)) AS ind_rbm, -- ck特性,先統(tǒng)一取交計(jì)算好各非主鍵維下rbm,提升查詢性能
if(ind_name = 'dau', ind_rbm, 0) AS dau, if(ind_name = 'xxx', ind_rbm, 0) AS xxx -- 若有,可計(jì)算其他去重計(jì)數(shù)指標(biāo)
FROM
( SELECT
dim_value[2] AS device_brand, dim_value[1] AS pid, -- 對(duì)應(yīng)dim_name='pid,device_brand'順序,數(shù)組第二位為device_brand,數(shù)組第一位為pid
ind_name AS ind_name, groupBitmapOrState(ind_rbm) AS ind_rbm -- 同維度rbm進(jìn)行并集計(jì)算
FROM rbm_physics_tbl_local -- 查詢r(jià)bm_physics_tbl分shard后的local表,性能優(yōu)化
WHERE (ind_name in ('dau')) -- 指標(biāo)選擇
AND (log_date = '20240609') AND (dim_name = 'pid,device_brand') AND (dim_value[1] = '13') -- 維度過濾,此處等價(jià)于log_date=20240609 and pid=13
GROUP BY ind_name, dim_value[2], dim_value[1] -- 等價(jià)于ind_name, device_brand, pid -- 多維分組
) AS device_brand_tbl -- 定義device_brand維度邏輯視圖
-- pid與is_new_user維度對(duì)應(yīng)邏輯視圖
INNER JOIN ( xxx ) AS is_new_user_tbl ON is_new_user_tbl.ind_name = device_brand_tbl.ind_name -- 多邏輯視圖必須按ind_name條件join,否則會(huì)出現(xiàn)笛卡爾積
AND is_new_user_tbl.pid = device_brand_tbl.pid -- 多邏輯視圖公共維join條件
-- 若有其他非主鍵維邏輯視圖,以相同規(guī)則進(jìn)行join
INNER JOIN ( xxx ) ON xxx
GROUP BY device_brand, is_new_user, pid, ind_name -- 按分析維+ind_name分組
)) v
GROUP BY device_brand, is_new_user, pid -- 多維分組
sql邏輯說明:
1.最內(nèi)層處理
a. 維度過濾需要通過對(duì)dim_value數(shù)組進(jìn)行轉(zhuǎn)換, 保證語義與普通where過濾一致, 并對(duì)映射后的分析維+指標(biāo)名進(jìn)行分組, 聚合同維度下rbm進(jìn)行并集計(jì)算
b. 對(duì)于非主鍵維, 需定義不同子查詢, 同時(shí)進(jìn)行inner join操作, join鍵為指標(biāo)名+分析維度中的公共維, 需要分析N個(gè)非主鍵維就需要join N-1次。若分析維度中僅包含主鍵維, 則無需進(jìn)行join操作, 定位到dim_name='主鍵維'即可
c. 由于經(jīng)過了底層一系列的join操作, 若分析維度或過濾條件設(shè)置不當(dāng), 可能造成join后維度基數(shù)爆炸, 會(huì)引起較為嚴(yán)重的影響, 具體在后文[穩(wěn)定性保障]部分進(jìn)行說明
2.第二層處理
a. 由于rbm加速物化表中的維度值均是以string類型進(jìn)行存儲(chǔ), 而轉(zhuǎn)換前的原表字段有對(duì)應(yīng)的實(shí)際類型, 在join邏輯后需要進(jìn)行反轉(zhuǎn)
b. 在此層, 利用ck特性, 先統(tǒng)一取交計(jì)算好各非主鍵維下rbm基數(shù)和, 可有效提升查詢性能。若直接獲取rbm基數(shù)進(jìn)行指標(biāo)轉(zhuǎn)換, 也可達(dá)成相同效果, 但查詢耗時(shí)會(huì)隨著非主鍵維數(shù)量, 乘倍上升
3.第三層處理
a. 利用ck分片特性, 進(jìn)行l(wèi)ocal本地計(jì)算rbm基數(shù)合后, 再對(duì)各分片基數(shù)求和, 最終產(chǎn)出對(duì)應(yīng)指標(biāo)值
4.跨模型指標(biāo)處理-可選
a. 應(yīng)用[星座模型]查詢實(shí)現(xiàn), 分別將 [聚合物化模型]中非rbm加速的普通計(jì)算指標(biāo)vv 與 [rbm物化模型]中rbm加速的去重計(jì)數(shù)指標(biāo)dau, 通過union方式聚合到相同維度下, 最終返回完整結(jié)果集
性能對(duì)比
以下統(tǒng)計(jì)的響應(yīng)時(shí)間不包含數(shù)據(jù)傳輸?shù)膇o耗時(shí)
維度基數(shù)對(duì)于性能的影響
如下可觀察到, 耗時(shí)隨維度基數(shù)增加而線性增加
圖片
圖片
穩(wěn)定性保障
加速保障: 對(duì)維度基數(shù)過大的數(shù)據(jù)進(jìn)行加速, 會(huì)生成非常多的rbm結(jié)構(gòu)數(shù)據(jù)。不僅加速過程中需要消耗平臺(tái)極多的資源, 加速時(shí)間很長(zhǎng), 同時(shí)查詢時(shí)的性能提升也極為有限。此類情況在用戶配置加速時(shí), 會(huì)進(jìn)行數(shù)據(jù)探查校驗(yàn), 并在準(zhǔn)入階段進(jìn)行攔截。
查詢保障: 為保障對(duì)rbm加速數(shù)據(jù)查詢的穩(wěn)定性, 避免單次查詢中涉及交并計(jì)算的rbm過多, 加載大量數(shù)據(jù)至內(nèi)存引起集群穩(wěn)定性下降。我們?cè)趯?shí)際查詢數(shù)據(jù)前, 先對(duì)數(shù)據(jù)進(jìn)行了一次基數(shù)探查, 對(duì)于基數(shù)大于某閾值的查詢, 直接進(jìn)行攔截阻斷, 避免對(duì)集群造成過多壓力, 此類查詢本身消耗資源較多, 且多數(shù)情況也無法在要求的時(shí)間內(nèi)響應(yīng)數(shù)據(jù)?;鶖?shù)探查是通過count計(jì)算出部分維度組合的數(shù)量, 查詢性能較好, 一般在1s內(nèi)能響應(yīng), 實(shí)際數(shù)據(jù)查詢通常是秒級(jí)返回, 1s內(nèi)的耗時(shí)損耗還是在接受范圍內(nèi), 且用戶體感也不十分明顯, 是必要的前置校驗(yàn)保障。
六、未來展望
工程化支持[維度組合]方式加速rbm結(jié)構(gòu), 將能定位唯一粒度實(shí)體維度組合以最小組合方式建立, 可有效減少rbm結(jié)構(gòu)數(shù)據(jù)的數(shù)量。節(jié)省存儲(chǔ)的同時(shí), 也提升查詢性能。
目前bitmap數(shù)據(jù)主要是在離線鏈路生成完后寫入clickhouse,后續(xù)將基于ClickHouse 字典服務(wù)構(gòu)建bitmap數(shù)據(jù)生成與更新的實(shí)時(shí)鏈路,提升整體產(chǎn)品的實(shí)時(shí)分析場(chǎng)景能力。
引用
- Roaring Bitmaps: Implementation of an Optimized Software Library(https://arxiv.org/pdf/1709.07821)
- WeOLAP 亞秒級(jí)實(shí)時(shí)數(shù)倉(cāng) —— BitBooster 10倍查詢優(yōu)化實(shí)踐(https://mp.weixin.qq.com/s/tJQoNRZ5UDJ_IASZLlhB4Q)