伴魚事件分析平臺設(shè)計
背景
在伴魚,服務(wù)器每天收集的用戶行為日志達到上億條,我們希望能夠充分利用這些日志,了解用戶行為模式,回答以下問題:
最近三個月,來自哪個渠道的用戶注冊量最高?
最近一周,北京地區(qū)的,發(fā)生過繪本瀏覽行為的用戶,按照年齡段分布的情況如何?
最近一周,注冊過伴魚繪本的用戶,7 日留存率如何?有什么變化趨勢?
最近一周,用戶下單的轉(zhuǎn)化路徑上,各環(huán)節(jié)的轉(zhuǎn)化率如何?
為了回答這些問題,事件分析平臺應(yīng)運而生。本文將首先介紹平臺的功能,隨后討論平臺在架構(gòu)上的一些思考。
功能
總的來說,為了回答各種商業(yè)分析問題,事件分析平臺支持基于事件的指標(biāo)統(tǒng)計、屬性分組、條件篩選等功能的查詢。其中,事件指用戶行為,例如登錄、瀏覽伴魚繪本、購買付費繪本等。更具體一些,事件分析平臺支持三類分析:「事件分析」,「漏斗分析」,和「留存分析」。
事件分析
事件分析是指,用戶指定一系列條件,查詢目的指標(biāo),用于回答一個具體的分析問題。這些條件包括:
事件類型:指用戶行為,采集自埋點數(shù)據(jù);例如登錄伴魚繪本,購買付費繪本
指標(biāo):指標(biāo)分為兩類,基礎(chǔ)指標(biāo)和自定義指標(biāo)基礎(chǔ)指標(biāo):總次數(shù)(pv),總用戶數(shù)(uv),人均次數(shù)(pv/uv)自定義指標(biāo):事件屬性 + 計算類型,例如 「用戶下單金額」的「總和/均值/最大值」
過濾條件:用于篩選查詢所關(guān)心的用戶群體
維度分組:基于分組,可以進行分組之間的對比
時間范圍:指定事件發(fā)生的時間范圍
讓我們舉個具體的例子。我們希望回答「最近一周,在北京地區(qū),不同年齡段的用戶在下單一對一課程時,下單金額的平均數(shù)對比」這個問題。這個問題可以很直觀地拆解為下圖所示的事件分析,其中:
事件類型 = 下單一對一課程
指標(biāo) = 下單金額的平均數(shù)
過濾條件 = 北京地區(qū)
維度分組 = 按照年齡段分組
時間范圍 = 最近一周

圖注:事件分析創(chuàng)建流程

圖注:事件分析界面
漏斗分析
漏斗分析用于分析多步驟過程中,每一步的轉(zhuǎn)化與流失情況。
例如,伴魚繪本用戶的完整購買流程可能包含以下步驟:登錄 app -> 瀏覽繪本 -> 購買付費繪本。我們可以將這個流程設(shè)置為一個漏斗,分析整體以及每一步轉(zhuǎn)化情況。
此外,漏斗分析還需要定義「窗口期」,整個流程必須發(fā)生在窗口期內(nèi),才算一次成功轉(zhuǎn)化。和事件分析類似,漏斗分析也支持選擇維度分組和時間范圍。

圖注:漏斗分析創(chuàng)建流程

圖注:漏斗分析界面
留存分析
在留存分析中,用戶定義初始事件和后續(xù)事件,并計算在發(fā)生初始事件后的第 N 天,發(fā)生后續(xù)事件的比率。這個比率能很好地衡量伴魚用戶的粘性高低。
在下圖的例子中,我們希望了解伴魚繪本 app 是否足夠吸引用戶,因此我們設(shè)置初始事件為登錄 app,后續(xù)事件為瀏覽繪本,留存周期為 7 天,進行留存分析。

圖注:留存分析創(chuàng)建流程

圖注:留存分析界面
架構(gòu)
在架構(gòu)上,事件分析平臺分為兩個模塊,如下圖所示:
數(shù)據(jù)寫入:埋點日志從客戶端或者服務(wù)端被上報后,經(jīng)過 Kafka 消息隊列,由 Flink 完成 ETL,然后寫入 ClickHouse。
分析查詢:用戶通過前端頁面,進行事件、條件、維度的勾選,后端將它們拼接為 SQL 語句,從 ClickHouse 中查詢數(shù)據(jù),展示給前端頁面。

圖注:總架構(gòu)圖
不難看出,ClickHouse 是構(gòu)成事件分析平臺的核心組件。我們?yōu)榱舜_保平臺的性能,圍繞 ClickHouse 的使用進行了細(xì)致的調(diào)研,回答了以下三個問題:
如何使用 ClickHouse 存儲事件數(shù)據(jù)?
如何高效寫入 ClickHouse?
如何高效查詢 ClickHouse?
如何使用 ClickHouse 存儲事件數(shù)據(jù)?
事件分析平臺的數(shù)據(jù)來源有兩大類:來源于埋點日志的用戶行為數(shù)據(jù),和來源于「用戶畫像平臺」的用戶屬性數(shù)據(jù)。本文只介紹埋點日志數(shù)據(jù)的存儲,對「用戶畫像平臺」感興趣的同學(xué),可以期待一下我們后續(xù)的技術(shù)文章。
在進行埋點日志的存儲選型前,我們首先明確了幾個核心需求:
支持海量數(shù)據(jù)的存儲。當(dāng)前,伴魚每天產(chǎn)生的埋點日志在億級別。
支持實時聚合查詢。由于產(chǎn)品和運營同學(xué)會使用事件分析平臺來探索多種用戶行為模式,分析引擎必須能靈活且高效地完成各種聚合。
ClickHouse 在海量數(shù)據(jù)存儲場景被廣泛使用,高效支持各類聚合查詢,配套有成熟和活躍的社區(qū),促使我們最終選擇 ClickHouse 作為存儲引擎。
根據(jù)我們對真實埋點數(shù)據(jù)的測試,億級數(shù)據(jù)的簡單查詢,例如 PV 和 UV,都能在 1 秒內(nèi)返回結(jié)果;對于留存分析、漏斗分析這類的復(fù)雜查詢,可以在 10 秒內(nèi)返回結(jié)果。
「存在哪」的問題解決后,接下來回答「怎么存」的問題。ClickHouse 的列式存儲結(jié)構(gòu)非常適合存儲大寬表,以支持高效查詢。但是,在事件分析平臺這個場景下,我們還需要支持「自定義屬性」的存儲,這時「大寬表」的存儲方式就不盡理想。
所謂「自定義屬性」,即埋點日志中一些事件所獨有的屬性,例如:「下單一對一課程」這一事件在上報時,會帶上「訂單金額」這個很多其它事件所沒有的屬性。如果為了支持「下單一對一課程」這個事件的存儲,就需要改變 ClickHouse 的表結(jié)構(gòu),新增一列,這將使得表結(jié)構(gòu)的維護成本極高,因為每個新事件都可能附帶多個「自定義屬性」。
為了解決這個問題,我們將頻繁變動的自定義屬性統(tǒng)一存儲在一個 Map 中,將基本不變的公共屬性存為列,使之兼具大寬表方案的高效性,和 Map 方案的靈活性。
如何高效寫入 ClickHouse?
在設(shè)計 ClickHouse 的部署方案時,我們采用了業(yè)界常用的讀寫分離模式:寫本地表,讀分布式表。在寫入側(cè),分為 3 個分片,每個分片都有雙重備份。
由于事件分析的絕大多數(shù)查詢,都是以用戶為單位,為了提高查詢效率,我們在寫入時,將數(shù)據(jù)按照 user_id 均勻分片,寫入到不同的本地表中。如下圖所示:

圖注:將埋點數(shù)據(jù)寫入到 ClickHouse
之所以不寫分布式表,是因為我們使用大量數(shù)據(jù)對分布式表進行寫入測試時,遇到過幾個問題:
Too many parts error:分布式表所在節(jié)點接收到數(shù)據(jù)后,需要按照 sharding_key 將數(shù)據(jù)拆分為多個 parts,再轉(zhuǎn)發(fā)到其它節(jié)點,導(dǎo)致短期內(nèi) parts 過多,并且增加了 merge 的壓力;
寫放大:分布式表所在節(jié)點,如果在短時間內(nèi)被寫入大量數(shù)據(jù),會產(chǎn)生大量臨時數(shù)據(jù),導(dǎo)致寫放大。
如何高效查詢 ClickHouse?
我們可以使用 ClickHouse 的內(nèi)置函數(shù),輕松實現(xiàn)事件分析平臺所需要提供的事件分析、漏斗分析和留存分析三個功能。
事件分析可以用最樸素的 SQL 語句實現(xiàn)。例如,最近一周,北京地區(qū)的,發(fā)生過繪本瀏覽行為的用戶,按照年齡段的分布,可以表述為:
SELECT count(1) as cnt, toDate(toStartOfDay(toDateTime(event_ms))) as date, ageFROM event_analyticsWHERE event = "view_picture_book_home_page" AND city = "beijing" AND event_ms >= 1613923200000 AND event_ms <= 1614528000000GROUP BY (date, age);
留存分析使用 ClickHouse 提供的 retention 函數(shù)。例如,注冊伴魚繪本后,計算瀏覽繪本的次日留存、7 日留存可以表述為:
SELECT sum(ret[1]) AS original, sum(ret[2]) AS next_day_ret, sum(ret[3]) AS seven_day_retFROM(SELECT user_id, retention( event = "register_picture_book" AND toDate(event_ms) = toDate('2021-03-01'), event = "view_picture_book" AND toDate(event_ms) = toDate('2021-03-02'), event = "view_picture_book" AND toDate(event_ms) = toDate('2021-03-08') ) as retFROM event_analyticsWHERE event_ms >= 1614528000000 AND event_ms <= 1615132800000GROUP BY user_id);
漏斗分析使用 ClickHouse 提供的 windowFunnel 函數(shù)。例如,在 瀏覽繪本 -> 購買繪本,窗口期為 2 天的這個轉(zhuǎn)化路徑上,轉(zhuǎn)化率的計算可以被表達為:
SELECTarray( sumIf(count, level >= 1), sumIf(count, level >= 2) ) AS funnel_uv,FROM (SELECTlevel,count() AS countFROM (SELECT uid, windowFunnel(172800000)( event_ms, event = "view_picture_book" AND event_ms >= 1613923200000 AND event_ms <= 1614009600000, event = "buy_picture_book") AS levelFROM event_analyticsWHERE event_ms >= 1613923200000 AND event_ms <= 1614182400000GROUP BY uid )GROUP BY level)
