1. 程式人生 > >技術內參 | 神策分析架構演進:“變”與“不變” 中的思索與創新

技術內參 | 神策分析架構演進:“變”與“不變” 中的思索與創新

作者:付力力,神策資料聯合創始人&技術 VP 

畢業於北京理工大學軟體工程專業,2008 年至 2013 年期間歷任百度新產品研發部、網頁搜尋部、基礎架構部工程師。2013 年 9 月年至 2014 年 8 月擔任豌豆莢資料部門資深研發工程師。2014 年 9 月至 2015 年 4 月擔任黃金錢包技術合夥人。2018 年 8 月,榮登“2018 福布斯中國 30 歲以下精英榜”。

2015 年 9 月正式釋出了神策分析 1.0 版本,在隨後的 3 年裡,我們的產品研發團隊一直在不斷地進行版本迭代,到目前為止一共釋出了 12 個大版本。

相比於最初的 1.0 版本,現在的神策分析無論是在產品體驗還是在底層架構上都已經發生了很大的變化:

從最初只能使用 3 個單薄的基礎分析功能,到現在支援 10 個分析模型聯合構建的場景化分析能力;從最初只能支援每天數萬日活的小 App,到現在可以輕鬆應對一天產生數百億的資料量巨型 App。

而另一方面,3 年內,神策分析裡也有很多地方沒有改變:

例如,從第一版的設計裡就確定了模型的 Event-User,該模型現在依然是整個神策分析裡最基礎和重要的概念。

在這篇文章裡,我給大家介紹神策分析最近在底層架構上一些比較大的設計改進,同時也會分享我們在這些架構設計中關於"變"與"不變"的思考。

從 SQL 查詢引擎到使用者行為分析引擎

我們之前在很多場合都對神策分析的底層架構做過詳細的介紹,這個架構的主要特點之一是:

神策所有的分析結果都是從明細資料實時查詢得出,而不是基於大多數分析系統所使用的預計算技術,之所以這麼設計,因為我們希望系統資料分析能力的上限在於資料本身。

換句話說,我們期望只要是從已經採集的資料裡可以分析得到的結論,神策都希望可以幫助我們的客戶很容易的實現。

從結果看來,這種架構設計的好處是非常顯著的:

它大大簡化了整個系統的資料流,我們不需要為不同的分析模型來維護複雜的聚合表,並在資料回溯的時候保持這些資料之間的一致性(大多數類似的資料系統裡要麼拋棄資料回溯的能力,要麼放棄資料一致性)。

受益於這種架構,我們在很短的時間內推出眾多靈活的分析模型,並且這些分析模型之間可以通過分群等方式來進行自由的組合查詢。

同時,配合我們開發的查詢快取機制,這套架構也可以在報表等相對固定的資料分析需求上得到比較好的使用體驗。

當然,這種設計的另外一個結果是,神策分析很明確地拋棄了對高 QPS 查詢需求的直接支援(例如不應該嘗試在商品詳情頁裡直接從神策分析獲取這個商品本週的銷量)。

不過,整體上我們認為,犧牲一個非必要的特性來換取數倍的分析靈活性以及一個簡單可維護的架構,是一個非常划算的選擇。 

在這套架構裡,Impala 作為我們使用的資料查詢引擎,可以說是一個非常核心的模組。

在最初的設計選型上我們選擇 Impala,一方面是因為 Impala 已經是一個相對比較成熟的 MPP 架構的查詢引擎,而且對 SQL 有著比較良好的支援。另外一方面則是因為我們的研發團隊在 Impala 的使用和二次開發上有著比較多的經驗。

其中,是否支援 SQL 是一個很重要的選型依據。雖然 SQL 是一種有著幾十年歷史、至今也沒有太多變化的古老工具,但是到目前為止它依然是對錶格資料進行操作的最佳選擇,在易用性和靈活性之間做到了比較好的平衡。

更重要的是,我們當初經過簡單的調研發現,只使用 SQL 就可以很好的實現一個使用者行為分析系統的大部分需求,除此之外,還可以通過 UDF/UDAF/UDAnF 等增加擴充套件能力,則幾乎可以滿足所有常見需求。

事實上,在神策分析比較早期的版本里,所有的分析模型都是用標準 SQL 直接實現的。

隨著我們產品功能的增加,我們為了滿足越來越複雜的分析模型和更高的效能指標,也對 Impala 做了很多改造。

不過,在這個過程中,SQL 自身的描述能力和 Impala 執行架構的侷限性也逐漸暴露出來,例如我們很難像 Spark 的 DAG 模型一樣來靈活的控制 SQL 的查詢計劃,導致一些複雜查詢的效能不佳,以及在一些組合分析的場景下沒有辦法很容易的複用查詢的中間結果。

因此,我們開始基於 Impala 構建一個全新的查詢引擎。通過對已有的各種分析模型計算過程的理解,我們發現它們幾乎都可以被抽象為如下的計算過程:

▹篩選出特定時間範圍內的特定 Event 資料,如果查詢還涉及到 User/Item,那麼還需要再次進行 Join 操作,最終得到: List<Event>

▹對 List<Event> 按照 Event 中的使用者 ID 進行 Shuffle,並按時間排序,最終得到每個使用者 ID 的有序 Event 序列:(User Id, List<Event>)

▹對(User Id, List<Event>) 中的每個 UserId 的 List<Event> 應用具體的分析模型規則,例如漏斗、留存等,得出每個使用者 ID 的中間計算結果,如下:(User Id, IntermediateResult)

▹對 (User Id, IntermediateResult) 進行最後一次聚合,得到最終的結果

不難看出,上述計算過程中最核心的難點在於如何快速的得到 (User Id, List<Event>) ,這中間可能涉及重排序和大資料量的 Shuffle 等操作。對於需要 Join User/Item 表的查詢,Join 本身的效能也可能會成為瓶頸。

我們基於 Impala 原有的執行框架,在底層儲存和查詢邏輯上做了一系列的優化,最終實現的分析引擎相比於原有的方式在複雜查詢的執行效能上有 10x 的提升,同時由於開發方式的簡化,也直接加速了我們對各種複雜分析模型的迭代速度。

在後續的文章中,我們會詳細介紹這個面向使用者行為分析的查詢引擎的具體優化細節。

擴充套件 Event-User 模型

模型擴充套件:從 Event-User 到 Event-Item-User 

在神策分析最初的設計階段,我們就確定了以 Event-User 為核心的邏輯資料模型,可以說,Event-User 模型是整個神策分析架構的基礎。

3 年以來,神策分析在數百家不同行業的客戶的實踐結果也充分證明了這個模型的適應能力。

所有的資料模型本質上都是對現實世界的抽象,而在抽象之後必然會損失一些對現實世界的還原能力。

所以 Event-User 模型雖然在電商、金融、線上教育、網際網路娛樂、企業服務等不同的行業上都發揮了很好的價值,但是隨著客戶需求的不斷深入,尤其是在和具體行業業務的深入融合中,我們也逐漸發現了這個模型的一些缺點。

例如在 Event-User 模型中,出於效能和可解釋性等各方面的考慮,Event 是被設計為不可變的。從邏輯上看似乎沒有問題,因為 Event 代表的是歷史上已經發生過的事件,一般來說不應該需要進行更新。

但是,在實際的應用過程中,並不一定是這麼理想的狀態。

例如,在很多客戶進行埋點採集的過程中,他們會發現某些 Event 在最初的階段並不能很容易的採集到完整的資料。

比如一個電商客戶,在客戶端 App 裡採集"商品加入購物車"事件時,只能採集到商品的 ID、名稱等基本資訊,而對於後續分析需要的更多維度,例如商品的分類、促銷的活動資訊等等,則不一定能很容易的採集到(通常這些資訊都是客戶端在業務中沒有使用到的,如果想要採集,則需要對服務端 API、客戶端內部的資訊傳遞都做比較大的修改)。

又或者是等到真正需要分析的時候,才發現當初的採集是不完備的,這個時候想再把歷史資料補上就是一件非常困難的事情。

還有另外一種比較常見的場景。某個線上教育的 App 中會有很多和課程相關的事件,例如對課程的瀏覽、購買、學習等,而關於課程的一些基本資訊中會有許多是不斷變化的,如課程的分類、定價等等。

在 Event 裡記錄的,應當是 Event 發生的時刻這個課程的狀態,例如一個購買課程的事件,我們可以記錄下來當時課程的分類、價格屬性,作為 Event 的一部分。而課程的分類、定價後續可能會隨著業務的需要隨時調整,如果業務方希望按照最新的(或者某個特定階段的)課程分類或者定價來分析使用者的歷史行為,則是一個難以完成的需求。

從技術上來看,解決上述問題的方案並不複雜。很多熟悉資料倉庫的朋友可能會發現,這些其實是在傳統資料倉庫裡比較典型的維度表的問題,可以使用經典的雪花模型或者星型模型來輕鬆解決。

但是,我們並不希望引入這麼複雜的模型,畢竟神策分析的設計目標並不是一個通用的資料倉庫。雖然靈活性是神策分析最核心的設計目標之一,但也是建立在"使用者行為分析"這個目標的基礎之上的。

我們期望的一個理想方式是:對資料模型增加一點有限的複雜性,但是可以給整個系統帶來十倍甚至百倍的靈活性提升。 

為了滿足上述需求,我們在新版的神策分析中對 Event-User 模型進行了擴充套件,引入了 Item 的概念。這裡的所謂 Item,在嚴格意義上是指一個和使用者行為相關聯的實體,可能是一個商品、一個視訊劇集、一部小說等等。

如果不嚴格約束的話,理論上它也可以儲存其它任意的擴充套件維度資訊。

在具體的技術實現上,我們允許客戶定義多個不同的 Item 實體,例如電商有商品、配送點等不同的實體。

在使用前,客戶要定義這些實體,並且把這些實體的資料通 SDK 傳送到神策分析系統中,自動建立起一個或者多個 Item 表。然後,出於不同效能要求和業務需求的考慮,對於 Item 表的使用我們提供了不同的兩種方式。

第一種方式,客戶在進行 Event 埋點時,可以選擇要進行關聯匯入的 Item 資訊。

例如有一個"商品加入購物車"的事件,這個事件裡只採集了"商品 ID",但是同時因為我們事先已經定義好了"商品 Item",那麼通過"商品 ID"則直接可以先把 Event 和"商品 Item"進行關聯,再把"商品 Item"的某一些屬性作為 Event 的一部分進行直接匯入。

使用這種方式,可以在最大程度滿足業務分析的情況下簡化客戶端對資料採集的工作,同時在查詢效能方面也不會有任何下降。

第二種方式,更類似於傳統資料倉庫的維度表。

我們在埋點時不做任何變動,而是在需要進行查詢的時候,把 Item 表加入進來。

這種方式會有更好的靈活性,因為可以在 Event 發生之後對資料進行擴充套件,也可以支援隨時使用最新的 Item 資料進行分析,但是另外一方面,這麼做並不能很好的保留事件發生當時的某些狀態,而且由於需要在查詢的時候進行實時的資料 Join,也不可避免的會降低查詢效能。

在把 Event-User 模型擴充套件為 Event-Item-User 模型之後,神策分析對複雜業務場景有了更好的支援,無論是在埋點工作的簡化還是在分析能力的提升上都有非常直接的幫助。

後續我們也將繼續在簡化 Item 資料的接入和使用上做出更多的改進。 

使用者分群的進化

從 2 年前的神策分析 1.4 版本開始,我們引入了使用者分群功能。從架構層面,我們主要做了兩件事情:一是把分群的概念引入了我們的資料模型中,二是提供靈活、便利的定義分群規則的方式。 

對於第一點,我們把分群看作是使用者屬性的一部分,只不過這個屬性是根據使用者已有的行為特徵計算出來的,是一個衍生屬性。所以在資料模型上,分群其實是對 Event-User 模型中 User 部分的一個擴充套件。

當然,在物理儲存上,由於分群具有頻繁更新、整體刪除等特點,因此並不會直接和原有的使用者屬性資訊儲存在一起,而是採用獨立儲存的方式。

對於第二點,一方面,我們提供了一套描述規則,允許客戶直接從 UI 上定義比較複雜的分群:在某段時間做過某個 Event 幾次,或者完成了某個連續的 Event 序列等。

更重要的是,我們把所有已有分析模型的使用者列表功能都看作為是分群規則定義的一部分,這種方式使得客戶可以很容易的把各個分析模型的結果進行組合,產生 1+1>2 的效果。

整體上來看,神策分析 1.4 在引入分群的概念之後,架構上幾乎沒有做任何大的改動,就可以讓所有的分群和普通的使用者屬性一樣在任何的分析模型裡直接使用。

這個也是完全得益於前文提到的實時分析架構,以及具有良好擴充套件能力的 Event-User 模型。

隨著客戶對神策分析的使用場景越來越複雜,我們的客戶對分群功能也提出了更多的需求

一個比較顯著的問題是:現在的神策的每個分群只能儲存一個最新的結果,而不能檢視歷史的狀態。

比如在一個電商產品裡,我們可以很容易的建立一個"日購買金額>=300"的使用者分群,但是這個分群每天都會自動重新整理,並且會丟掉前一天的狀態。

如果我們想分析這個使用者分群在時間軸上的變化趨勢,或者考慮一個更復雜的場景,想分析"日購買金額>=300"的這個使用者群體在當天購買的商品品類的分佈情況,用現在的分群功能都是沒辦法直接實現的。

為了實現上述功能,我們在即將釋出的 1.13 版本也對使用者分群功能做了一次大的改進。

首先在資料模型上,我們擴充套件了分群的模型定義,加入了時間維度。即每個分群不只是代表這個分群的群體在某一時刻的狀態,而是可以儲存每天、每週等不同時間點下的狀態。

其次,我們也進一步增強了分群的描述能力,除了增強了在 UI 上進行定義的功能之外,還允許使用者直接上傳分群好的結果(例如某個線下活動的使用者列表),或者是從一個 SQL 結果匯出成一個分群,避免讓分群的能力受限於已有的規則定義。

另外,在分群的計算執行層面,我們也不再使用獨立的 MapReduce 程式來進行,而是複用了上面提到的基於 Impala 的使用者行為分析引擎。

因為分群的過程,其實也是一個很典型的使用者行為分析的計算邏輯,這樣就很自然的把整個神策系統內對於使用者行為的分析都統一到了一個計算模組上來完成。

  更精確的使用者標識體系

如何準確地標識使用者一直是使用者行為資料系統中的一大難題。在過去的 3 年裡,我們在客戶端 SDK、服務端架構、資料接入的解決方案支援上做了持續的優化,解決了很多普遍的問題。

傳統的網站或者 App 分析工具,通常以 Cookie 或裝置號作為使用者(其實是裝置)的標識,同時這些分析工具大部分也並不支援跨端的分析,所以關於使用者標識導致的各類問題並不突出。

但是在今天的使用者行為分析場景中,準確的跨端標識使用者變成了一個非常迫切的需求。尤其是在微信生態的情況下,一個自然人使用者在 App、小程式、H5、公眾號之間反覆跳轉,完成一系列行為是非常常見的場景,如果不能做到準確的標識使用者,很多資料分析的需求將會無法準確完成。 

在神策分析 1.13 版本之前,為了解決跨端標識使用者的問題,我們提供了有限度的多裝置使用者關聯體系。

這裡的“有限”主要體現在一個註冊使用者在未登入狀態下只能跟一個裝置進行繫結。很顯然,在很多場景下這種關聯並不能很好的滿足需求。

最典型的場景是,如果一個老使用者更換了新的裝置,那麼他在這個新裝置上未登入狀態下的操作將會被識別為一個全新的使用者,從而對某些分析結果的準確性產生影響。 

因此,我們在最近的 1.13 版本提供了一個註冊使用者跟任意多個裝置進行關聯的機制。在這個新的機制下,一個註冊使用者可以使用多個裝置進行登入,並且他在這些裝置上註冊/登入前後的行為都會被準確的識別到同一個使用者身上,從而能在神策分析裡更準確的還原一個使用者的行為序列。

當然,這個在新的關聯機制也並不是提供無限的靈活性。考慮這樣一個場景:一個裝置先後被多個註冊使用者登入使用,那這個裝置上產生的匿名行為(即非登入狀態下產生的行為)只會被關聯到第一個在這個裝置上登入的註冊使用者。

雖然在技術上我們也可以很容易的實現使用者和裝置之間的多重繫結,但是考慮到實際的應用場景並不常見,而且提供這種機制之後一定會給客戶帶來的更多理解上的複雜性,我們還是決定把新的關聯機制限定在一個註冊使用者多個裝置的場景下。 

全新的使用者標識體系雖然可以更準確地標識使用者,但是同時也會引入一個新的問題:允許一個註冊使用者和多個裝置進行關聯,會導致歷史資料的分析結果是不斷變化的。我們可以看一個具體的例子,假設一個使用者 X 進行了一系列操作:

▹ 7 月 1 日之前在裝置 A 上註冊、登入並使用 App

▹ 7 月 2 日開始在裝置 B 上使用 App

▹ 7 月 5 日在裝置 B 上使用之前的帳號進行登入,並繼續使用

我們可以看到,在 7 月 5 日之前,神策分析並不知道使用裝置 B 跟裝置 A 背後都是使用者 X 在操作,也就是說在這之前計算使用者數都會是 2,同時在計算留存、漏斗等資料時也都會當作兩個不同的使用者。

而一旦到 7 月 5 日使用者 X 登入了,神策分析可以知道之前的行為其實都是同一個人 X 產生的,那麼這個時候再看 7 月 5 日之前的使用者數也會變成了 1。

這種資料的變化在某些場景下可能會變得更加難以理解,我們假設一個比較極端的情況,如果上面的使用者 X 是在一年之後才在裝置 B 上進行登入,那麼這一年內裝置 B 所產生的行為是否都應該視作使用者 X 產生的?現實情況下可能是,也可能不是,只憑借這些資訊很難做出準確的判斷。

本質上,新的使用者標識體系是實現了對歷史資料的修正,同時由於神策分析又是一個完全基於明細資料進行實時查詢的分析系統,因此資料分析的結果跟著發生變化也是很自然的事情。

正如我們在上文的 Event-User 模型擴充套件中提到的,雖然 Event 代表的是已經發生的事件,但是依然會有一些資訊在 Event 發生的當時是無法得到的。

比如在上面的例子中,7 月 2 日當天我們並不知道在裝置 B 上使用的也是使用者 X,只能在 3 天之後再對這個資料進行修正。我們在一定程度上破壞了 Event 的不變性,但是也帶來了更高的資料準確性。

不過,除了技術上的難點,歷史資料的變化還會給資料的可解釋性造成比較大的影響:很多人都會對昨天甚至更早的資料報表會發生變化產生困惑。

因此,如何在提高資料準確性的同時降低客戶對資料的理解難度,會是我們後面的重點方向。

更多幹貨和案例,可以關注“神策資料”和“使用者行為洞察研究院”公眾號瞭解~