導讀:?Apache Doris 是小米集團內(nèi)部應用最為廣泛的 OLAP 引擎之一,本文主要從數(shù)據(jù)的角度分析 A/B 實驗場景查詢的性能現(xiàn)狀,探討基于 Apache Doris 的性能優(yōu)化的解決方案。經(jīng)過一系列基于 Doris 的性能優(yōu)化和測試,A/B 實驗場景查詢性能的提升超過了我們的預期。希望本次分享可以給有需要的朋友提供一些參考。
作者|小米集團大數(shù)據(jù)工程師 樂濤
一、業(yè)務背景
A/B實驗是互聯(lián)網(wǎng)場景中對比策略優(yōu)劣的重要手段。為了驗證一個新策略的效果,需要準備原策略A和新策略B兩種方案。 隨后在總體用戶中取出一小部分,將這部分用戶完全隨機地分在兩個組中,使兩組用戶在統(tǒng)計角度無差別。將原策略A和新策略B分別展示給不同的用戶組,一段時間后,結(jié)合統(tǒng)計方法分析數(shù)據(jù),得到兩種策略生效后指標的變化結(jié)果,并以此判斷新策略B是否符合預期。
小米A/B實驗平臺是一款通過A/B實驗的方式,借助實驗分組、流量拆分與科學評估來輔助完成科學的業(yè)務決策,最終實現(xiàn)業(yè)務增長的一款運營工具產(chǎn)品。其廣泛的應用于產(chǎn)品研發(fā)生命周期中各個環(huán)節(jié):
本文主要從數(shù)據(jù)的角度分析A/B實驗場景查詢的性能現(xiàn)狀,探討一下性能優(yōu)化的解決方案。
二、數(shù)據(jù)平臺架構(gòu)
A/B實驗平臺的架構(gòu)如下圖所示:
平臺使用的數(shù)據(jù)主要包含平臺自用的實驗配置數(shù)據(jù)、元數(shù)據(jù),以及業(yè)務方上報的日志數(shù)據(jù)。
由于業(yè)務方引入 SDK,并與分流服務進行交互,日志數(shù)據(jù)中包含其參與的實驗組 ID 信息。
用戶在實驗平臺上配置、分析、查詢,以獲得報告結(jié)論滿足業(yè)務訴求。
鑒于AB實驗報告各個業(yè)務方上報數(shù)據(jù)的鏈路都大體類似,我們就拿頭部業(yè)務方廣告業(yè)務舉例
數(shù)據(jù)流程如下圖所示:
整個數(shù)據(jù)鏈路并不復雜,日志數(shù)據(jù)傳入后,經(jīng)過必要的數(shù)據(jù)處理和清洗工作進入 Talos(小米自研消息隊列),通過 Flink 任務以明細數(shù)據(jù)的形式實時寫入到 Doris 表中,同時 Talos 數(shù)據(jù)也會同步到 Hive 表進行備份,以便問題排查和數(shù)據(jù)修復。
出于對高效寫入以及字段增減需求的考慮,Doris 明細表以 Duplicate 模型來建模:
CREATE TABLE `dwd_xxxxxx` (
? `olap_date` int(11) NULL COMMENT "分區(qū)日期",
? `user_id` varchar(256) NULL COMMENT "用戶id",
? `exp_id` varchar(512) NULL COMMENT "實驗組ID",
? `dimension1` varchar(256) NULL COMMENT "",
? `dimension2` varchar(256) NULL COMMENT "",
? ......
? `dimensionN` bigint(20) NULL COMMENT "",
? `index1` decimal(20, 3) NULL COMMENT "",
? ......
? `indexN` int(11) NULL COMMENT "",
) ENGINE=OLAP
DUPLICATE KEY(`olap_date`, `user_id`)
COMMENT "OLAP"
PARTITION BY RANGE(`olap_date`)
(
PARTITION p20221101 VALUES [("20221101"), ("20221102")),
PARTITION p20221102 VALUES [("20221102"), ("20221103")),
PARTITION p20221103 VALUES [("20221103"), ("20221104"))
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 300
;
三、數(shù)據(jù)現(xiàn)狀分析
在提速之前,小米A/B實驗平臺完成實驗報告查詢的 P95 時間為小時級,實驗報告使用數(shù)據(jù)的方式存在諸多的性能問題,直接影響業(yè)務部門做運營和決策的效率。
3.1 報告查詢基于明細
當前報告查詢的數(shù)據(jù)來源為明細表,而明細表的數(shù)據(jù)量巨大:
而且,實驗報告的查詢條件中時間范圍常常橫跨多天?;跉v史查詢報告統(tǒng)計,查詢條件中時間范圍大于一天的報告占比69.1%,具體的時間跨度占比分布如下:
明細數(shù)據(jù)的巨大掃描量給集群帶來了不小的壓力,且由于報告查詢存在并發(fā)以及 SQL 的拆分,如果一個 SQL 請求不能快速的返回結(jié)果釋放資源,也會影響到請求的排隊狀況。因此在工作時間段內(nèi) Doris 集群BE節(jié)點 CPU 負載狀況基本是持續(xù)滿載,磁盤 IO 也持續(xù)處于高負荷狀態(tài),如下圖所示:
BE節(jié)點CPU使用率
BE節(jié)點磁盤IO
個人思考
當前報告所有查詢基于明細數(shù)據(jù),且平均查詢時間跨度為 4 天,查詢掃描數(shù)據(jù)量上百億。
由于掃描數(shù)據(jù)量級大,計算成本高,給集群造成較大壓力,導致數(shù)據(jù)查詢效率不高。
如果通過對數(shù)據(jù)進行預聚合處理,控制 Scan Rows 和 Scan Bytes,減小集群的壓力,查詢性能會大幅提升。
3.2 字段查詢熱度分層分布
由于之前流程管控機制相對寬松,用戶添加的埋點字段都會進入到明細表中,導致字段冗余較多。
統(tǒng)計歷史查詢報告發(fā)現(xiàn),明細表中常用的維度和指標只集中在部分字段,且查詢熱度分層分布:
參與計算的指標也集中在部分字段,且大部分都是聚合計算(sum)或可以轉(zhuǎn)化為聚合計算(avg):
個人思考
明細表中參與使用的維度只占54.3%,高頻使用的維度只占15.2%,維度查詢頻次分層分布。
數(shù)據(jù)聚合需要對明細表中維度字段做取舍,選擇部分維度進行上卷從而達到合并的目的,但舍棄部分字段必然會影響聚合數(shù)據(jù)對查詢請求的覆蓋情況。而維度查詢頻次分層分布的場景非常適合根據(jù)維度字段的熱度做不同層次的數(shù)據(jù)聚合,同時兼顧聚合表的聚合程度和覆蓋率。
3.3 實驗組ID匹配效率低
當前明細數(shù)據(jù)的格式為:
明細數(shù)據(jù)中的實驗組ID以逗號分隔的字符串形式聚攏在一個字段中,而實驗報告的每條查詢語句都會使用到exp_id過濾,查詢數(shù)據(jù)時使用LIKE方式匹配,查詢效率低下。
個人思考
將實驗組ID建模成一個單獨的維度,可使用完全匹配代替LIKE查詢,且可利用到Doris索引,提高數(shù)據(jù)查詢效率。
將逗號分隔的實驗組ID直接打平會引起數(shù)據(jù)量的急劇膨脹,因此需要設計合理的方案,同時兼顧到數(shù)據(jù)量和查詢效率。
3.4 進組人數(shù)計算有待改進
進組人數(shù)查詢是實驗報告的必查指標,因此其查詢速度很大程度上影響實驗報告的整體查詢效率,當前主要問題如下:
當進組人數(shù)作為獨立指標計算時,使用近似計算函數(shù)APPROX_COUNT_DISTINCT處理,是通過犧牲準確性的方式提升查詢效率。
當進組人數(shù)作為復合指標的分母進行計算時,使用COUNT DISTINCT處理,此方式在大數(shù)據(jù)量計算場景效率較低。
個人思考
AB實驗報告的數(shù)據(jù)結(jié)論會影響到用戶決策,犧牲準確性的方式提升查詢效率是不可取的,特別是廣告這類涉及金錢和業(yè)績的業(yè)務場合,用戶不可能接受近似結(jié)果。
進組人數(shù)使用的COUNT DISTINCT計算需要依賴明細信息,這也是之前查詢基于明細數(shù)據(jù)的重要因素。必須為此類場景設計新的方案,使進組人數(shù)的計算在保證數(shù)據(jù)準確的前提下提高效率。
四、數(shù)據(jù)優(yōu)化方案
基于以上的數(shù)據(jù)現(xiàn)狀,我們優(yōu)化的核心點是將明細數(shù)據(jù)預聚合處理,通過壓縮數(shù)據(jù)來控制Doris查詢的Scan Rows和Scan Bytes。與此同時,使聚合數(shù)據(jù)盡可能多的覆蓋報告查詢。從而達到,減小集群的壓力,提高查詢效率的目的。
新的數(shù)據(jù)流程如下圖所示:
整個流程在明細鏈路的基礎上增加聚合鏈路,Talos數(shù)據(jù)一方面寫入Doris明細表,另一方面增量落盤到Iceberg表中,Iceberg表同時用作回溯明細數(shù)據(jù)以及生成聚合數(shù)據(jù),我們通過工場Alpha(小米自研數(shù)據(jù)開發(fā)平臺)的實時集成和離線集成保證任務的穩(wěn)定運行和數(shù)據(jù)的一致性。
4.1 選取高頻使用維度聚合
在生成數(shù)據(jù)聚合的過程中,聚合程度與請求覆蓋率是負相關的。使用的維度越少,能覆蓋的請求就越少,但數(shù)據(jù)聚合程度越高;使用的維度越多,覆蓋的請求也越多,但數(shù)據(jù)粒度就越細,聚合程度也越低。因此需要在聚合表建模的過程中取得一個平衡。
我們的具體做法是:拉取歷史(近半年)查詢?nèi)罩具M行分析,根據(jù)維度字段的使用頻次排序確認進入聚合表的優(yōu)先級。在此基礎上得出聚合表的覆蓋率和數(shù)據(jù)量隨著建模字段增加而變化的曲線,如下圖所示:
其中覆蓋率根據(jù)歷史請求日志代入聚合表計算得出。
我們的原則是:針對OLAP查詢,聚合表的數(shù)據(jù)量應盡可能的控制在單日1億條以內(nèi),請求覆蓋率盡可能達到80%以上。
因此不難得出結(jié)論:選擇14個維度字段對聚合表建模比較理想,數(shù)據(jù)量能控制到單日8千萬條左右,且請求覆蓋率約為83%。
4.2 使用物化視圖
在分析報告歷史查詢?nèi)罩緯r,我們發(fā)現(xiàn)不同的維度字段查詢頻次有明顯的分層:
Top7維度字段幾乎出現(xiàn)在所有報告的查詢條件之中,對于如此高頻的查詢,值得做進一步的投入,使查詢效率盡可能的提升到最佳。
Doris的物化視圖能夠很好的服務于此類場景。
什么是物化視圖?
物化視圖是一種特殊的物理表,其中保存基于基表(base table)部分字段進一步上卷聚合的結(jié)果。
雖然在物理上獨立存儲,但它是對用戶透明的。為一張基表配置好物化視圖之后,不需要為其寫入和查詢做任何額外的工作:
當向基表寫入和更新數(shù)據(jù)時,集群會自動同步到物化視圖,并通過事務方式保證數(shù)據(jù)一致性。
當對基表進行查詢時,集群會自動判斷是否路由到物化視圖獲取結(jié)果。當查詢字段能被物化視圖完全覆蓋時,會優(yōu)先使用物化視圖。
因此我們的查詢路由如下圖所示:
用戶的查詢請求會盡可能的路由到聚合表物化視圖,然后是聚合表基表,最后才是明細表。
如此使用多梯度的聚合模型的配合來應對熱度分層的查詢請求,使聚合數(shù)據(jù)的效能盡可能的發(fā)揮到最大。
4.3 精確匹配取代LIKE查詢
既然物化視圖這么好用,為什么我們不是基于Doris明細表配置物化視圖,而是單獨開發(fā)聚合表呢?
是因為明細數(shù)據(jù)中的實驗組ID字段存儲和查詢方式并不合理,聚合數(shù)據(jù)并不適合通過明細數(shù)據(jù)直接上卷來得到。
3.3節(jié)已經(jīng)提到,exp_id(實驗組ID)在明細表中以逗號分隔的字符串進行存儲,查詢數(shù)據(jù)時使用LIKE方式匹配。作為AB實驗報告查詢的必查條件,這種查詢方式無疑是低效的。
我們希望的聚合方式如下圖所示:
我們需要將exp_id字段拆開,把數(shù)據(jù)打平,使用精確匹配來取代LIKE查詢,提高查詢的效率。
控制聚合表數(shù)據(jù)量
如果只做拆分打平的處理必然會導致數(shù)據(jù)量的激增,未必能達到正向優(yōu)化的效果,因此我們還需要想辦法來壓縮exp_id打平后的數(shù)據(jù)量:
聚合表選取維度字段建模的時候,除了4.1節(jié)提到的,以字段的使用頻次熱度作為依據(jù)之外,也要關注字段的取值基數(shù),進行綜合取舍。如果取值基數(shù)過高的維度字段進入聚合表,必然會對控制聚合表的數(shù)據(jù)量造成阻礙。因此,我們在保證聚合表請求覆蓋量的前提下,酌情舍棄部分高基數(shù)(取值有十萬種以上)的維度。
從業(yè)務的角度盡可能過濾無效數(shù)據(jù)(比如一個實驗組的流量為0%或者100%,業(yè)務上就沒有對照的意義,用戶也不會去查,這樣的數(shù)據(jù)就不需要進入聚合表)。
經(jīng)過這一系列步驟,最終聚合表的數(shù)據(jù)量被控制在單日約8000萬條,并沒有因為exp_id打平而膨脹。
值得一提的是,exp_id字段拆分后,除了查詢從LIKE匹配變?yōu)榫_匹配,還額外帶來了兩項收益:
字段從String類型變?yōu)镮nt類型,作為查詢條件時的比對效率變高。
能利用Doris的前綴索引和布隆過濾器等能力,進一步提高查詢效率。
4.4 使用BITMAP去重代替COUNT DISTINCT
要提速實驗報告查詢,針對進組人數(shù)(去重用戶數(shù))的優(yōu)化是非常重要的一個部分。作為一個對明細數(shù)據(jù)強依賴的指標,我們?nèi)绾卧诓粊G失明細信息的前提下,實現(xiàn)像Sum,Min,Max等指標一樣高效的預聚合計算呢?
BITMAP去重計算可以很好的滿足我們的需求。
什么是BITMAP去重?
BITMAP去重簡單來說就是建立一種數(shù)據(jù)結(jié)構(gòu),表現(xiàn)形式為內(nèi)存中連續(xù)的二進制位(bit),參與去重計算的每個元素(必須為整型)都可以映射成這個數(shù)據(jù)結(jié)構(gòu)的一個bit位的下標,如下圖所示:
計算去重用戶數(shù)時,數(shù)據(jù)以bit_or的方式進行合并,以bit_count的方式得到結(jié)果。更重要的是,如此能實現(xiàn)去重用戶數(shù)的預聚合。BITMAP性能優(yōu)勢主要體現(xiàn)在兩個方面:
空間緊湊:通過一個bit位是否置位表示一個數(shù)字是否存在,能節(jié)省大量空間。以Int32為例,傳統(tǒng)的存儲空間為4個字節(jié),而在BITMAP計算時只需為其分配1/8字節(jié)(1個bit位)的空間。
計算高效:BITMAP去重計算包括對給定下標的bit置位,統(tǒng)計BITMAP的置位個數(shù),分別為O(1)和O(n)的操作,并且后者可使用CLZ,CTZ等指令高效計算。此外,BITMAP去重在Doris等MPP執(zhí)行引擎中還可以并行加速處理,每個節(jié)點各自計算本地子BITMAP,而后進行合并。
當然,以上只是一個簡化的介紹,這項技術(shù)發(fā)展至今已經(jīng)做了很多優(yōu)化實現(xiàn),比如RoaringBitmap,感興趣的同學可以看看:GitHub - RoaringBitmap/RoaringBitmap
全局字典
要實現(xiàn)BITMAP去重計算,必須保證參與計算的元素為UInt32 / UInt64,而我們的user_id為String類型,因此我們還需設計維護一個全局字典,將user_id映射為數(shù)字,從而實現(xiàn)BITMAP去重計算。
由于聚合數(shù)據(jù)目前只服務于離線查詢,我們選擇基于Hive表實現(xiàn)全局字典,其流程如下:
指標聚合
生成Doris聚合表時,將user_id作為查詢指標以BITMAP類型來存儲,其他常規(guī)查詢指標則通過COUNT / SUM / MAX / MIN等方式聚合:
如此明細表和聚合表的指標計算對應關系如下:
五、優(yōu)化效果
SQL視角
查詢請求轉(zhuǎn)換成SQL之后,在明細表和聚合表的表現(xiàn)對比如下:
常規(guī)聚合指標查詢的性能提升自不必說(速度提升50~60倍)
進組人數(shù)查詢性能的提升也非常可觀(速度提升10倍左右)
集群視角
SQL查詢的快進快出,使查詢占用的資源能快速釋放,對集群壓力的緩解也有正向的作用。
Doris集群BE節(jié)點CPU使用情況和磁盤IO狀況的改變效果顯著:
需要說明的是,集群狀況的改善(包括實驗報告查詢P95提升)并不全歸功于數(shù)據(jù)預聚合優(yōu)化工作,這是各方合力協(xié)作(如產(chǎn)品業(yè)務形態(tài)調(diào)整,后端查詢引擎排隊優(yōu)化,緩存調(diào)優(yōu),Doris集群調(diào)優(yōu)等)的綜合結(jié)果。
六、小技巧
由于業(yè)務查詢需求的多樣,在查詢明細表時,會出現(xiàn)一個字段既作為維度又作為指標來使用的情況。
如廣告業(yè)務表中的targetConvNum(目標轉(zhuǎn)化個數(shù))字段,此字段的取值為0和1,查詢場景如下:
--作為維度
selecttargetConvNum,count(distinct user_id)
from analysis.doris_xxx_event?
where olap_date = 20221105
and event_name='CONVERSION'
and exp_id like '%154556%'group bytargetConvNum;
--作為指標
select sum(targetConvNum)
from analysis.doris_xxx_event?
where olap_date = 20221105
and event_name='CONVERSION'and exp_id like '%154556%';
如果這個字段被選取進入聚合表,應該如何處理呢?
我們的處理方式是:
在聚合表中把這類字段建模成維度
聚合表中需要一個計數(shù)指標cnt,表示聚合表中一條數(shù)據(jù)由明細表多少條數(shù)據(jù)聚合得到
當這類字段被作為指標查詢時,可將其與cnt指標配合計算得到正確結(jié)果
明細表查詢:
select sum(targetConvNum)
from analysis.doris_xxx_event
where olap_date = 20221105
and event_name='CONVERSION'
and exp_id like '%154556%';
對應的聚合表查詢:
select sum(targetConvNum * cnt)
from agg.doris_xxx_event_agg
where olap_date = 20221105
and event_name = 'CONVERSION'
and exp_id = 154556;
七、結(jié)束語
經(jīng)過這一系列基于Doris的性能優(yōu)化和測試,A/B實驗場景查詢性能的提升超過了我們的預期。值得一提的是,Doris較高的穩(wěn)定性和完備的監(jiān)控、分析工具也為我們的優(yōu)化工作提效不少。希望本次分享可以給有需要的朋友提供一些參考。
最后,感謝SelectDB公司和Apache Doris社區(qū)對我們的鼎力支持。Apache Doris是小米集團內(nèi)部應用最為廣泛的OLAP引擎之一,目前集團內(nèi)部正在推進最新的向量化版本升級工作。未來一段時間我們將會把業(yè)務優(yōu)化工作和Doris最新的向量化版本進行適配,進一步助力業(yè)務的正向發(fā)展。