神策分析的技術選型與架構實現
很多朋友很想知道神策分析(Sensors Analytics)是如何實現在每天十億級別數據的情況下可以做到秒級導入和秒級查詢,是如何做到不需要預先指定指標和維度就能實現多維查詢的。今天正好在這篇文章里面,和大家交流一下我們的技術選型與具體的架構實現,希望能夠對大家有所啟發。
當然,脫離客戶需求談產品設計,不太現實;而脫離產品設計,純粹談技術選型與架構實現,也不現實。因此,我們首先會跟大家探討一下神策分析從產品角度,是如何從客戶需求抽象產品設計的,而產品設計,又是如何確定我們的技術選型。然后,我們則會從產品的整體架構出發,逐步介紹每一個模塊和子系統的具體實現。
1. 客戶需求決定產品設計,產品設計決定技術選型
1.1 私有化部署
在決定做神策分析這個產品的最開始,我們就準備滿足這樣一類客戶的需求,即對數據的安全與隱私有顧慮,或者希望能夠積累自己的用戶行為數據資產,并且完成數據的深度應用與二次開發。
因此,這就決定我們的產品需要是一個可以私有化部署的產品,可以部署在客戶的內網中,這基本也構成是我們產品的核心設計理念。
而正因為需要私有化部署,我們在設計上,也必須考慮到因為這一點而帶來的一系列在運維、審計方面的技術調整;同時,為了方便客戶能夠基于我們的系統二次開發,充分發揮數據的價值,這就要求我們在技術選型上,盡可能選擇熱門的開源技術,必須保留最細粒度原始數據,同時,最好在數據處理從采集、傳輸到存儲、查詢的各個環節都對外提供普適易用的接口,降低客戶的開發代價。
1.2 用戶行為分析
數據分析是一個很大的領域,而我們最核心地是滿足客戶對于用戶行為分析這一個特定領域的需求。幫助他們回答這樣一些問題:用戶使用產品的活躍情況和頻次分布;核心流程轉化情況;分析上周流失用戶的行為特征等。同時,我們期望神策分析可以適應不同的行業的不同產品,而這些不同產品肯定又會有不同的技術架構。
針對這種需求,我們在技術上做了兩個關鍵性的決策:首先,我們對分析模型進行抽象,期望用少數幾個分析模型能夠滿足大部分需求,而剩余未滿足的需求則通過自定義查詢來實現,這樣的好處,就是可以集中精力來優化查詢速度;其次,為了減少 ETL 的代價,我們盡量簡化數據模型,從而能夠減少 ETL 的環節,保證神策分析產品在不同行業的適用性。
1.3 全端數據采集
隨著互聯網的發展越來越深入,一個用戶在同一個產品上的行為,已經需要從多個不同的來源來進行采集了,這些來源包括 iOS、安卓、Web、H5、微信、業務數據庫、第三方配送服務、客服系統等。不僅僅是需要采集到,還需要能夠將同一個用戶在不同來源的數據進行打通。
針對這種需求,神策分析決定提供全端的數據采集方案,需要包括主流的客戶端平臺和主流語言的 SDK,Restful 風格的數據導入 API,全埋點與可視化埋點等埋點輔助手段。同時,為了方便埋點的迭代與管理,還需要在這方面下大力氣。同時,為了解決跨屏貫通一個用戶的問題,還需要提供 ID-Mapping 方面的解決方案。
1.4 靈活的多維分析
相比較傳統的網頁或者 App 統計工具,如百度統計、Google Analytics 等,我們想解決的是用戶更加靈活更加深入的分析需求。例如,分析不同地域、不同品類的銷量對比;分析比較不同客服的服務質量;對比不同年齡段用戶的轉化情況;分渠道分析轉化效果;察看使用不同功能后對留存的影響等。
針對這一需求,神策分析需要提供全功能的多維分析能力,能夠滿足如下技術指標:維度、指標不需要預定義;漏斗、留存、回訪分析都可以任意下鉆。而因為這些需求,所以現在很火的開源多維分析系統,如 Apache Kylin、Druid 等都無法滿足我們的需要。同時,這也決定了我們不能夠存儲聚合數據,而是應該存儲最細粒度的明細數據。當然,這一決策也是受到私有化部署提供數據進行二次開發的影響。
1.5 秒級導入和秒級查詢
在數據分析的某些應用場景下,對于數據導入后多長時間能夠查到,其實是有一定需求的。例如,分析彩票投注截止前的最后30分鐘投注情況,或者廣告投放后立刻察看效果以便調整策略等。為了滿足這部分需求,更好地讓數據為決策服務,神策分析需要讓數據能夠做到秒級導入,即一條數據接收后,在秒級就能被查到。因此,我們應該有實時數據導入流,并且盡可能地減少 ETL 的復雜程度,從而降低數據導入的開銷。
我們一直推崇數據驅動,推崇數據的需求方可以自助式地滿足數據分析需求。因此,這就需要我們的產品能夠滿足秒級查詢。這也就意味著我們在查詢引擎的選擇方面,就不能夠選擇 Hive,而應該考慮類似于 Impala、Spark SQL 等 MPP 架構的方案。
1.6 目前為止的技術決策
現在,我們回顧一下,由于之前我們列出的這一系列客戶需求,我們基本確定了如下的一些技術決策:
- 具有私有化部署能力,需要解決運維問題;
- 技術選型以開源方案為主,便于復用和客戶二次開發;
- 數據模型盡量簡潔,減少 ETL 代價;
- 每天十億級別數據量下,秒級導入,秒級查詢;
- 需要存儲明細數據,采用 ROLAP 而不是 MOLAP 的方案。
那么,下面,就讓我們進一步來看看,在這些技術決策之下,神策分析具體是怎么實現的。
2. 技術實現
2.1 整體架構
通常情況下,一個常見的數據平臺,整個數據處理的過程,可以分為如下 5 步:
神策分析設計上也基本遵循這一原則。下圖是神策分析的架構圖:
我們后面依次介紹各個子系統的具體技術實現。
2.2 數據采集子系統
神策分析主要支持采集三類數據,分別是前端操作、后端日志和業務數據。
2.2.1 采集前端操作
其中前端操作,主要是用戶在客戶端,如 iOS App、安卓 App 和網頁上的一些操作,目前主流有三種采集方案,分別是全埋點、可視化埋點和代碼埋點。
其中,全埋點是指默認地不加區分地采集所有能夠采集的控件操作和交互,然后再在后端篩選出需要的數據;可視化埋點則是通過可視化交互的方式,提前選擇要采集哪些控件的操作,然后再在操作發生時向后端發送數據;代碼埋點則是最傳統和廣泛使用的一種技術采集手段,是以代碼調用 SDK 接口的方式,來發送要采集的用戶操作。
三種采集方式,在使用上,全埋點最簡單,只需要嵌入 SDK 即可,但是會帶來額外的數據開銷;可視化埋點不會有額外的數據開銷,嵌入 SDK 后需要用可視化的方式進行控件的點選。由于國內 App 開發普遍存在不遵循開發規范的情況,導致上述兩種采集方案都存在一定的兼容性問題。代碼埋點則是每一個點都需要一行或者更多的代碼,更新迭代時使用較為復雜。
但是反過來,代碼埋點卻是采集能力最強的一種采集手段。我們以用戶在京東的提交訂單頁面,來分別描述三者在采集能力上的差異:
在這個界面上,使用三種采集方案的采集能力大概如下面所描述的那樣:
- 使用全埋點,可以知道某時某刻某人點擊了某個按鈕;
- 使用可視化埋點,可以跟進一步知道某時某刻某人提交了一個訂單;
- 使用代碼埋點,則可以更進一步地獲取訂單金額、商品名稱、用戶級別等自定義屬性。
為了方便客戶的使用,神策分析對于采集前端操作提供的方案則是:
- 以代碼埋點為主,提供了 iOS、安卓和 JS 三種 SDK,特別是針對混合開發的 App,專門提供了原生 App 和 H5 之前交換數據,打通用戶 ID 的接口;
- 以可視化埋點為輔,并且努力解決國內 App 的兼容性問題;
- 對于一些例如頁面瀏覽、App 啟動、App 進入后臺等默認事件,則提供全埋點的方案,可以嵌入 SDK 之后直接采集;
- 建議客戶根據不同的使用場景,選擇最合適的前端埋點方案。
同時,為了最大限度保證嵌入了我們 SDK 的客戶 App 的用戶體驗,我們專門針對 SDK 的數據發送策略做了很多優化,包括:
- 數據首先緩存在本地,達到一定規?;蜷g隔時才發送;
- 僅在 3G/4G/Wi-Fi 時發送數據,發送時會對數據進行打包、壓縮;
- 在產生首條數據、進入后臺、退出程序時都會嘗試發送,以盡可能兼容那些安裝了 App 簡單嘗試后就快速刪除的用戶;
- 提供了強制發送接口,也讓客戶自己可以控制發送策略。
2.2.2 采集后端日志
后端日志主要指產品的服務端模塊在運行時打印出的日志。相比采集前端操作,采集后端日志會有如下一系列的優勢:
- 傳輸時效性:如前面描述的那樣,為了保證用戶體驗,前端采集數據是不能實時向后端發送的,所以會帶來傳輸時效性的問題,而采集后端日志就不存在這個問題;
- 數據可靠性:前端采集需要通過公網進行數據傳輸,肯定會存在數據可靠性的問題,采集后端日志配合私有部署,則可以做到純內網傳輸數據,這一問題大大緩解;
能夠獲取的信息豐富程度:有很多信息,例如商品庫存、商品成本、用戶風險級別、用戶潛在價值等,在前端都采集不到的,只能在后端采集。
正因為這一點,我們建議一個行為在前端和后端都可以采集時,優先在后端進行采集,并且為此提供了一系列的后端語言 SDK、日志采集工具和數據批量導入工具等。
2.2.3 采集業務數據
業務數據主要指一些供銷存數據庫數據、從第三方系統拿到的訂單配送數據和客戶數據等,針對這一類數據,我們提供了相應的數據導入工具,以及 RESTful 風格的導入 API,用于完成數據的導入。
2.2.4 ID-Mapping
對于前面三類不同的數據源,我們期望能夠打通同一個用戶在這三類數據源中的行為,并且為此提供了如下的技術手段:
- 不同端可以自定義唯一的用戶 ID,如設備 ID、Cookie ID、注冊 ID等,客戶可以自己定義自己選擇;當然,神策分析系統內部會有唯一的 user_id;
- 提供一次性的 track_signup 接口,將兩個 ID 貫通起來,例如,可以將一個用戶在瀏覽器上的 Cookie ID 與他在產品里面的注冊 ID 貫通起來,然后這個用戶以注冊 ID 在手機 App 上登錄時,我們依然能知道是同一個用戶;
- 我們目前采用的方案不需要回溯數據,但是應用是有限制的,即只能支持一對一的 ID-Mapping,這也是一個典型的功能與性能的折衷。
2.3 數據接入子系統
不管是采用哪種采集方式,數據都是通過 HTTP API 發送給系統的。
而在數據接入子系統部分,我們采用了 Nginx 來接收通過 API 發送的數據,并且將之寫到日志文件上。使用 Nginx 主要是考慮到它的高可靠性與高可擴展性,并且在我們的應用場景下, Nginx 單機可以輕松地做到每秒接收 1 萬條請求,考慮到一條請求通常都不止一條用戶行為,可以認為很輕松就能做到數萬 TPS。
對于 Nginx 打印到文件的日志,會由我們自己開發的 Extractor 模塊來實時讀取和處理 Nginx 日志,并將處理結果發布到 Kafka 中。在這個過程中,Extractor 模塊還會進行數據格式的校驗,屬性類型的識別與相關元數據的操作。與 ID-Mapping 的處理也是在這個階段完成的。一些字段的解析和擴展工作,如基于 IP 判斷國家、省份、城市,基于 UserAgent 判斷瀏覽器、操作系統等,也是在這個階段完成的。前面提到了,我們不需要用戶預先指定指標和維度,基本實現了 schema-free,就是在 Extractor 處理階段,對這些列進行校驗,并且完成相關的元數據操作。
Kafka 是一個廣泛使用的高可用的分布式消息隊列,作為數據接入與數據處理兩個流程之間的緩沖,同時也作為近期數據的一個備份。另外,這個階段也對外提供訪問 API,客戶可以直接從 Kafka 中將數據引走,進入自己的實時計算流。
2.4 數據模型
在介紹數據導入模塊與數據存儲模塊之前,我們需要先討論一下神策分析的數據模型設計。
前面已經提到,神策分析主要解決的是用戶行為分析這么一個特定領域的數據分析需求,并且期望盡量簡化數據模型以降低 ETL 代價。最終,我們選擇了業內非常流行的 Event + User 模型,可以覆蓋客戶的絕大部分分析需求,并且對于采集到的數據不需要有太多的 ETL 工作。
Event 主要是描述用戶做了什么事情。每一條 Event 數據對應用戶的一次事件,由 用戶 ID、Event 名稱、自定義屬性三部分組成。Event 名稱主要是對 Event 的一個分類,例如“PageView”、“Search”、“PayOrder”等等。我們在客戶端會默認采集設備 ID 或者 Cookie ID 作為用戶 ID,客戶也可以自己設置一個合理的用戶 ID。
而我們最多可以支持一萬個自定義屬性,并且開發者并不需要事先告之系統,系統會自動從接收的數據中解析發現新的字段并且進行相應的處理。Event 數據以追加為主,不可修改,這也符合事件這一概念的實際物理意義。不過,為了方便后續系統的運維以及客戶的使用,我們特別為 Event 數據提供了有限的數據刪除能力,這一點會在后續存儲部分更詳盡地描述。
User 則主要描述用戶是個什么樣的人。它用 用戶 ID 與自定義屬性兩部分組成。自定義屬性是年齡、所在地、Tag等。在神策分析中,它的來源主要有三類,一類是使用者自己采集并且通過接口告之系統的,例如用戶的注冊信息;一類是基于使用者的第一方數據挖掘得到的用戶畫像數據;還有一類則是通過第三方供應商得到的用戶在第三方行為所體現出來的屬性和特質。在神策分析中,User 數據,是每一行對應一個用戶,并且可以任意修改的。
2.5 數據導入與存儲
正如前面所說,由于神策分析自身的產品需求,如導入時效性、不需要預先指定指標和維度等,目前比較熱的開源 OLAP 系統如 Apache Kylin 和 Druid,我們并不能拿來使用。而最后,我們選擇的是存儲最細粒度數據,在每次查詢時都從最細粒度數據開始使用 Impala 進行聚合和計算,而為了實現秒級查詢,我們在存儲部分做了很多優化,盡可能減少需要掃描的數據量以便加快數據的查詢速度。
具體來說,雖然存儲都是構建在 HDFS 之上,但是為了滿足秒級導入和秒級查詢,我們將存儲分為 WOS(Write Optimized Store)和 ROS(Read Optimized Store)兩部分,分別為寫入和讀取進行優化,并且 WOS 中的數據會逐漸轉入 ROS 之中。
對于 ROS,我們選擇了 Parquet,這樣一個面向分析型業務的列式存儲格式。并且,根據我們面臨的業務的具體查詢需求,對數據的分區方式做了很細致的優化。首先,我們是按照事件發生的日期和事件的名稱,對數據做 Partition;同一個 Partition 內,會有多個文件,文件大小盡量保持在 512 MB 左右;每個文件內部先按照 userid 的 hash 有序,再按照 userid 有序,最后則按照事件發生的時間有序;會有一個單獨的索引文件記錄每個 user_id 對應數據的文件 offset。
另外,與大多數列式存儲一樣,Parquet 對于不同類型的列,本身有著很好的壓縮性能,也能大大減少掃描數據時的磁盤吞吐。簡單來說,利用列式存儲,只掃描必要的列,利用我們自己的數據分區方案,則在這個基礎之上進一步只掃描需要的行,兩者一起作用,共同減少需要掃描的數據量。
雖然 Parquet 是一個查詢性能很好的列式存儲,但是它是不能實時追加的,因此在 WOS 部分,我們選擇了 Kudu。在向 Kudu 中寫入數據時,我們選擇了類似于 0/1 切換的方案,即同一時間只寫入一張表,當這張表的寫入達到閾值時,就寫入新表中,而老表則會開始轉為 Parquet 格式。
由于這樣一個轉換過程,不可避免地會帶來 Parquet 的碎文件問題,因此也需要專門解決。
下圖比較詳細地展示了這樣一個轉化的過程:
在這個導入過程中,有如下幾個關鍵的工作模塊:
- KafkaConsumer:一個常駐內存的 MapReduce 程序 (只有 Mapper),負責實時從 Kafka 中訂閱數據,并且寫入到 Kudu 中。
- KuduToParquet:一個不定時啟動的 MapReduce 程序,在 Kudu 單個表寫入達到閾值并且不再被寫入時,將它轉成多個 Parquet 文件,并且移動到對應的 Partition 中。
- LoaderDemon:一個后臺調度程序,完成一些元數據操作;
- Merger:一個定時的 MapReduce 任務,定期合并 Parquet 中每個 Partition 內的碎文件。
2.6 數據查詢子系統
在數據查詢部分,我們是通過 WebServer 這個模塊,接收客戶通過我們的 UI 界面或者通過 API 發起的查詢請求,WebServer 并不做額外的處理,而是將這些查詢請求直接轉發給 QueryEngine 模塊。QueryEngine 模塊則是將查詢請求翻譯成 SQL,并在 Impala 中發起查詢。Impala 會訪問 Kudu 與 Parquet 數據共同構成的 View,完成對應的聚合與聚散。
特別提一下,在查詢引擎部分,我們選擇 Impala 而不是 Spark SQL,并不是出于性能或者穩定性的考慮,僅僅是因為我們團隊之前有比較多的基于 Impala 的工作經驗。
為了保證秒級的查詢性能,我們除了在存儲部分做文章以外,還在查詢部分做了很多的優化,這些優化包括:
- 界面上提供的查詢模型有限,但是能滿足客戶絕大部分需求,因此專門針對這些查詢模型做有針對性的優化;
- 使用 UDF/UDAF/UDAnF 等聚合函數替代 Join,提高查詢效率,特別一提的是,由于 Impala 不支持 UDAnF,這里我們修改了 Impala 的源代碼;
- 比較精細的緩存,只對有變化的數據才進行重查;
- 提供按用戶抽樣的功能,客戶可以通過抽樣數據快速嘗試不同的猜想,并最終在全量數據上獲取準確結果。
2.7 元數據與監控
我們主要使用 MySQL、ZooKeeper 來存儲元數據,主要包括類似于 Schema、維度字典、數據概覽、漏斗、分群、預測的配置、任務調度、權限等信息。我們還用到了 Redis 來存儲查詢緩存信息。
我們有 Monitor 這樣一個常駐內存的模塊,對系統的各個部分進行語義監控,并且進行異常狀態的修復。當然,作為一個商業系統,我們對于 license 的處理也是放在這個模塊之中的。
同時,為了減少運維的代價,對一些不常用的功能,雖然不提供界面但是也工具化了,包括數據清理工具、版本升級工具、性能分析工具、多項目管理工具等。
2.8 用戶分群與用戶行為預測
用戶分群與用戶行為預測,我們單獨放在最后面進行探討。
神策分析對用戶分群的定義,是根據用戶以往的行為,給用戶打標簽。例如,找到那些“上個月有購買行為的用戶”,“最近三個月有登錄的用戶”等。它是用戶畫像的一部分,但肯定不是用戶畫像的全部。
而與之相對的,用戶行為預測則是根據用戶過往的行為,預測將來做某個行為的概率。
這一部分的具體實現,我們會在其它的文章中做相應的介紹。