提速10倍+,StarRocks 指標平臺在攜程火車票的實踐
作者簡介
Kane,攜程高級數倉經理,專注數倉建設、數據應用和分析;
Wn,大數據平臺開發專家,專注大數據領域。
攜程火車票事業群運營著鐵友、攜程火車票和去哪兒火車票等重要的業務和品牌,目前正在積極地拓展海外市場。火車票的指標平臺旨在為業務人員提供便捷的指標查詢服務,讓業務人員能夠快速靈活地獲得這些業務和品牌相關的指標數據。
一、早期 OLAP 架構與痛點
火車票事業群的業務涵蓋了火車票、國際火車票、汽車票(含船票)等產品,錯綜復雜的業務也產生了多種多樣訂單和行為數據,通過對這些數據的分析可以揭示當前業務的發展現狀,也可以為未來的發展提供方向指引。
早些時候事業群開發過一套指標平臺,根據不同的指標類型使用了 3 套數據庫引擎,分別是 ClickHouse,Apache Kylin (以下簡稱 Kylin)和 Presto,如下圖所示。
圖片
圖 1
在舊版的指標平臺中,為了提升查詢性能使用了 ClickHouse、 Kylin 和 Presto 等多種存儲和查詢引擎,數據層混合使用了明細層和輕度匯總層,由此帶來的問題有:
- 指標數據源混亂,容易造成口徑不一致,維護成本大。
- 學習成本高,BI 同學錄入指標不僅需要了解不同存儲的區別,還需要掌握不同引擎的數據同步方法。
- 架構不合理,指標平臺將查詢的中間結果通過 jdbc 寫入 mysql 后再到服務端用 java 做匯總計算,處理鏈路過長,整體性能非常差,導致部分指標查詢需要半小時以上的等待時間。
鑒于這些原因,無論是用戶(運營人員)還是指標開發人員,都面臨著使用極差的問題。在這種情況下,我們決定使用基于一種查詢速度快和使用簡單的分布式數據庫來重構指標平臺。
二、指標平臺重構整體設計
首先,重構指標平臺我們首先考慮的是將多套存儲合并成一套,雖說 ClickHouse 和 Kylin 已經足夠強大,但是不足也很明顯。比如 ClickHouse 的 join 性能不盡如人意,并發性能差,SQL 語法是非標準的,使用起來不方便,大量的查詢很容易將 CPU 打滿;Kylin 是一個分析引擎,不支持增刪改操作,修改數據需要重新導入,修改 schema 需要重建 Cube(ETL成本很高),其次 Kylin 需要預先創建模型加載數據到 Cube 后才可進行查詢,使用上需要具備一定的數倉知識。
于是我們將目光投向 StarRocks,StarRocks 是一款全場景的 MPP 數據庫,相比 ClickHouse 等具有以下優點:
- 性能強悍:查詢速度快,多張億級表 join 也能秒級響應;
- 使用簡單:兼容 MySQL 協議,用戶使用門檻低;
- 支持高并發:滿足大量用戶同時查詢;
- 支持多種數據模型:明細、聚合、更新和主鍵模型,可靈活配置 ETL 任務;
- 支持物化視圖:可以自動路由到命中的物化視圖,用戶無感知;
- 支持多種導入方式:StreamLoad、SparkLoad、RoutineLoad,便于實時離線快速導入 StarRocks,流批一體。
圖 2
因此,重構后的結構如下:
圖 3
重構后的指標平臺只有一個數據庫,查詢時利用 StarRocks 內部 ETL 將明細數據轉存到臨時表,后續的匯總從臨時表查詢,避免了反復掃描大表。
2.1 指標查詢過程
當一個指標查詢請求發起時,由于指標屬性和用戶想查看的信息不同,我們根據查詢參數將查詢拆解成若干子查詢,子查詢分為明細和匯總兩類。
1)明細類子查詢
a. 可累加的指標查詢時間范圍內的明細數據,以及去年和 2019 年同期的明細數據,這部分的明細會存儲到臨時表,后續查詢都從這張表掃描,以避免對大表的頻繁掃描;該表每天生成 T+1 分區,防止增加分區失敗導致當天的指標查詢無法進行。
-tarpresqls "
ALTER TABLE ${table} ADD PARTITION if not exists p${partition}
VALUES [('${zdt.addDay(1).format("yyyy-MM-dd")}'),('${zdt.addDay(2).format("yyyy-MM-dd")}'));
" \
b. 如果指標不可累加或 count(distinct)類,僅存儲查詢時間范圍內的明細,不存儲用戶計算同環比的明細;
c. 當多個指標同時對相同維度進行查詢時,將多個指標的數據 join 后以寬表模式存儲。
2)匯總類子查詢
這一類 sql 主要在明細的基礎上根據用戶的需要做相應的計算,相比舊版本在服務內部用 java 做匯總計算,這里全部借助了 StarRocks,主要的匯總功能有:
a. 指標卡匯總和同環比;
b. 折線圖和維度下鉆。
3) “緩存”
多維度特別是包含出發/到達城市組合的查詢數據量非常大,耗時較長,同時避免相同的查詢反復訪問大表,我們增加了“緩存”功能,實現原理如下:
a. 記錄初次查詢的指標信息,主要包括維度和維度值,時間范圍,指標原始計算 sql 的 MD5 值,以及是否查詢成功;
b. 新的查詢進入后,我們會在當天的記錄中查找是否存在相同的查詢。如果存在相同的查詢,我們使用唯一的查詢標識(groupkey)將當前查詢指向上次已經執行過的查詢。這樣,我們可以直接讀取上次查詢的詳細數據和匯總結果,從而提高查詢效率。
因此這里的緩存非真實意義上的緩存,而是直接調用相同查詢的結果。
2.2 數據同步
首先我們梳理了舊平臺的數據源,從 300+ 指標的邏輯 sql 中提取了公共的 dwd 和 dim 表 51 張,并將這些數據統一同步至 StarRocks,但是對于一些指標使用的 dwd 表只出現一次的,依然將 dws 同步過來。
對于不同的 hive 表,我們使用了不同的 StarRocks 建表模型和同步方式,有以下幾種:
a. 全量同步:主要針對一些數據量小的表,例如 shareout_trn.dim_ibu_alliance,大小為 608k;
b. 增量分區同步:每天同步 hive 表中 T-1 的分區,各分區之間獨立;
c. 更新同步:火車票 BU 的一些訂單數據由于涉及到預售和訂單狀態的變更,變更的數據時間跨度比較大,將跨度范圍內的數據全部更新代價比較高,因此使用更新模型。
數據導入更新模型直接需要計算 T-1 和 T-2 分區有差異的數據,這里將所有字段使用 concat_ws('|',***)拼接后取 hash 值,之后 join 找到 hash 值不一致的數據。
模型KEY設置:
UNIQUE KEY(`order_id`)
取兩天有差異的數據:
select
t1.*
from
(select … where d='${cur_day}') as t1
left join
(select … where d=’${pre_day}’) as t2
on t1.business_pk_id=t2.business_pk_id
where t1.hash_code!=t2.hash_code or t2.order_id is null
d. 每天同步當月數據:如國際火車的訪問數據量較小,每天一個分區會導致 StarRocks 集群有很多小的 bucket,分桶數太多會導致元數據壓力比較大,數據導入導出時也會受到一些影響,因此我們按月設置分區,每天同步當月的數據。
時間范圍:
startdate='${zdt.format("yyyy-MM-01")}'
endDate='${zdt.add(2,1).format("yyyy-MM-01")}'
表設計:
PARTITION BY RANGE(dt)(Start("2019-01-01") End("2023-03-01") Every(Interval 1 month))
DISTRIBUTED BY HASH(分桶字段) BUCKETS 桶的數量
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.prefix" = "p",
"dynamic_partition.time_unit" = "month",
"dynamic_partition.end" = "1");
datax配置:
-temporary_partitions "tp${partition}" \
-tarpresqls "
ALTER TABLE ${table} DROP TEMPORARY PARTITION if exists tp${partition};
ALTER TABLE ${table} ADD PARTITION if not exists p${partition} VALUES [('${startdate}'),('${endDate}'));
ALTER TABLE ${table} ADD TEMPORARY PARTITION tp${partition} VALUES [('${startdate}'),('${endDate}'));
" \
-tarpostsqls "
ALTER TABLE ${table} REPLACE PARTITION (p${partition}) WITH TEMPORARY PARTITION (tp${partition});"
此外,對于 UBT 類數據,數據量級非常大,并且常見用于查詢 PV,UV 和停留時長等比較固定的場景,于是我們從中抽取出三張表:
ubt_for_pv: 每天按維度匯總 count(uid),每天數據大小只有幾十 K;
ubt_for_duration: 每天按維度匯總 sum(duration),如需要計算平均停留時長除以對應的 pv 即可;
ubt_for_uv: 每天按維度去重,盡最大可能減少數據量。
最后,鑒于上游表的迭代可能帶來的數據的不穩定,我們對需要同步的表的數據量做了監控,若發現當天的數據量波動超過 3sigma,監控任務自動發出郵件告警,這些 job 的同步都在 15 分鐘內完成。
三、Starrocks使用經驗分享
在指標平臺重構的過程中我們也遇到了一些問題,與數據和查詢相關的有以下幾個:
3.1 建表經驗
首先是 buckets 設置不合理,多數是設置過多,通常一個桶的數據量在 500MB~1GB 為好,個別表設置的桶數量太少,導致查詢時間長;其次是分區不合理,有些表沒有設置分區,有些設置的分區后每個分區數據量很小,優化建議是將不常訪問的數據按月分區,經常訪問的數據按日分區。
3.2 數據查詢
由于指標的查詢sql之前是針對不同引擎編寫,很多引擎是沒有索引的,比如 Presto。StarRocks 有豐富的索引功能,統一至 StarRocks 希望利用索引加速查詢,因此過濾條件中最好不要加函數,比如 select c1 from t1 where upper(employeeid) = upper(' s1')修改成select c1 from t1 where employeeid in(upper(' s1'), lower(' s1'))。
另外很多 sql 沒有使用分區,在 StarRocks 中將會全表掃描造成資源浪費。
3.3 函數問題
StarRocks 的 split 函數結果的下標從 1 開始,而 sparksql 等引擎對應的是從 0 開始,導致 sql 在 StarRocks 執行查詢的時候不報錯但是結果錯誤。
select split('a,b,c',',')[0] StarRocks查詢結果為空,其他引擎查詢結果為‘a’
select split('a,b,c',',')[1] StarRocks查詢結果為‘a’,其他引擎查詢結果為‘b’
四、查詢性能大幅提升
指標平臺的重構主要是為了解決查詢性能的問題,并且重構后也基本達到了預期。重構之前,復雜查詢需要數分鐘的時間才能完成。特別對于火車票相關指標,諸如出票票量指標,如果帶上出發和到達城市查詢,可能需要等待 30 分鐘以上,并且查詢失敗率較高。而在重構后,查詢時間大大縮短,復雜查詢在 10s 左右,并且 P99 在 2 秒之內,因此整體體驗得到顯著提升,用戶查詢次數相比改造前也有了翻倍的增長。
此外,現在新指標系統還豐富了更多功能,比如同環比和維度下鉆計算。得益于 StarRocks 的并發能力,我們可以在生成子查詢 SQL 后并發提交,從而大幅度減少響應時間,使得用戶在進行維度下鉆時幾乎無需等待即可快速獲取所需數據。
五、 后續優化方向
a. 目前,UV 類的 Count Distinct 查詢是基于存儲了大量明細數據的方式進行的。然而,對于部分指標,我們可以嘗試使用 Bitmap 來減少不必要的明細數據存儲空間,并且更重要的是可以提高查詢速度。在接下來的工作中,我們計劃嘗試這種方案,以進一步優化 UV 類指標的查詢性能。
b. 對于全量或增量更新的表使用聚合模型,聚合模型會對導入后具有相同維度的數據做預聚合,查詢的時候減少掃描數據的行數達到提升查詢速度的目的。
c. 當前的指標平臺計算過程將所需的數據寫入臨時表,后續改成使用物化視圖,在達到同樣效果的情況下減少了復雜度。