日處理數據量超10億:友信金服基於Flink構建實時用戶畫像系統的實踐

引言

當今生活節奏日益加快,企業面對不斷增加的海量信息,其信息篩選和處理效率低下的困擾與日俱增。由於用戶營銷不夠細化,企業App中許多不合時宜或不合偏好的消息推送很大程度上影響了用戶體驗,甚至引發了用戶流失。在此背景下,友信金服公司推行全域的數據體系戰略,通過打通和整合集團各個業務線數據,利用大數據、人工智能等技術構建統一的數據資產,如ID-Mapping、用戶標籤等。友信金服用戶畫像項目正是以此爲背景成立,旨在實現“數據驅動業務與運營”的集團戰略。目前該系統支持日處理數據量超10億,接入上百種合規數據源。

一、技術選型

傳統基於Hadoop生態的離線數據存儲計算方案已在業界大規模應用,但受制於離線計算的高時延性,越來越多的數據應用場景已從離線轉爲實時。這裏引用一張表格對目前主流的實時計算框架做個對比。

Apache Storm的容錯機制需要對每條數據進行應答(ACK),因此其吞吐量備受影響,在數據大吞吐量的場景下會有問題,因此不適用此項目的需求。

Apache Spark總體生態更爲完善,且在機器學習的集成和應用性暫時領先,但Spark底層還是採用微批(Micro Batching)處理的形式。

Apache Flink在流式計算上有明顯優勢:首先其流式計算屬於真正意義上的單條處理,即每一條數據都會觸發計算。在這一點上明顯與Spark的微批流式處理方式不同。其次,Flink的容錯機制較爲輕量,對吞吐量影響較小,使得Flink可以達到很高的吞吐量。最後Flink還擁有易用性高,部署簡單等優勢。相比之下我們最終決定採用基於Flink的架構方案。

二、用戶畫像業務架構

用戶畫像系統目前爲集團線上業務提供用戶實時標籤數據服務。爲此我們的服務需要打通多種數據源,對海量的數字信息進行實時不間斷的數據清洗、聚類、分析,從而將它們抽象成標籤,並最終爲應用方提供高質量的標籤服務。在此背景下,我們設計用戶畫像系統的整體架構如下圖所示:

整體架構分爲五層:

  1. 接入層:接入原始數據並對其進行處理,如Kafka、Hive、文件等。
  2. 計算層:選用Flink作爲實時計算框架,對實時數據進行清洗,關聯等操作。
  3. 存儲層:對清洗完成的數據進行數據存儲,我們對此進行了實時用戶畫像的模型分層與構建,將不同應用場景的數據分別存儲在如Phoenix,HBase,HDFS,Kafka等。
  4. 服務層:對外提供統一的數據查詢服務,支持從底層明細數據到聚合層數據的多維計算服務。
  5. 應用層:以統一查詢服務對各個業務線數據場景進行支撐。目前業務主要包含用戶興趣分、用戶質量分、用戶的事實信息等數據。

三、用戶畫像數據處理流程

在整體架構設計方案設計完成之後,我們針對數據也設計了詳盡的處理方案。在數據處理階段,鑑於Kafka高吞吐量、高穩定性的特點,我們的用戶畫像系統統一採用Kafka作爲分佈式發佈訂閱消息系統。數據清洗階段利用Flink來實現用戶唯一性識別、行爲數據的清洗等,去除冗餘數據。這一過程支持交互計算和多種複雜算法,並支持數據實時/離線計算。目前我們數據處理流程迭代了兩版,具體方案如下:

1.0版數據處理流程

數據接入、計算、存儲三層處理流程

整體數據來源包含兩種:

  1. 歷史數據:從外部數據源接入的海量歷史業務數據。接入後經過ETL處理,進入用戶畫像底層數據表。
  2. 實時數據:從外部數據源接入的實時業務數據,如用戶行爲埋點數據,風控數據等。

根據不同業務的指標需求我們直接從集團數據倉庫抽取數據並落入Kafka,或者直接從業務端以CDC(Capture Data Change)的方式寫入Kafka。在計算層,數據被導入到Flink中,通過DataStream生成ID-Mapping、用戶標籤碎片等數據,然後將生成數據存入JanusGraph(JanusGraph是以HBase作爲後端存儲的圖數據庫介質)與Kafka,並由Flink消費落入Kafka的用戶標籤碎片數據,進行聚合生成最新的用戶標籤碎片(用戶標籤碎片是由用戶畫像系統獲取來自多種渠道的碎片化數據塊處理後生成的)。

數據服務層處理流程

服務層將存儲層存儲的用戶標籤碎片數據,通過JanusGraph Spark On Yarn模式,執行TinkerPop OLAP計算生成全量用戶Yids列表文件。Yid是用戶畫像系統中定義的集團級用戶ID標識。結合Yids列表文件,在Flink中批量讀取HBase聚合成完整用戶畫像數據,生成HDFS文件,再通過Flink批量操作新生成的數據生成用戶評分預測標籤,將用戶評分預測標籤落入Phoenix,之後數據便可通過統一數據服務接口進行獲取。下圖完整地展示了這一流程。

ID-Mapping數據結構

爲了實現用戶標籤的整合,用戶ID之間的強打通,我們將用戶ID標識看成圖的頂點、ID pair關係看作圖的邊,比如已經識別瀏覽器Cookie的用戶使用手機號登陸了公司網站就形成了<cookie,mobile>對應關係。這樣所有用戶ID標識就構成了一張大圖,其中每個小的連通子圖/連通分支就是一個用戶的全部標識ID信息。

ID-Mapping數據由圖結構模型構建,圖節點包含UserKey、Device、IdCard、Phone等類型,分別表示用戶的業務ID、設備ID、身份證以及電話等信息。節點之間邊的生成規則是通過解析數據流中包含的節點信息,以一定的優先級順序進行節點之間的連接,從而生成節點之間的邊。比如,識別了用戶手機系統的Android_ID,之後用戶使用郵箱登陸了公司App,在系統中找到了業務線UID就形成了<Android_ID,mail>和<mail,UID>關係的ID pair,然後系統根據節點類型進行優先級排序,生成Android_ID、mail、UID的關係圖。數據圖結構模型如下圖所示:

Gephi

1.0版本數據處理流程性能瓶頸

1.0版本數據處理流程在系統初期較好地滿足了我們的日常需求,但隨着數據量的增長,該方案遇到了一些性能瓶頸:

  1. 首先,這版的數據處理使用了自研的Java程序來實現。隨着數據量上漲,自研JAVA程序由於數據量暴增導致JVM內存大小不可控,同時它的維護成本很高,因此我們決定在新版本中將處理邏輯全部遷移至Flink中。
  2. 其次,在生成用戶標籤過程中,ID-Mapping出現很多大的連通子圖(如下圖所示)。這通常是因爲用戶的行爲數據比較隨機離散,導致部分節點間連接混亂。這不僅增加了數據的維護難度,也導致部分數據被“污染”。另外這類異常大的子圖會嚴重降低JanusGraph與HBase的查詢性能。

Gephi

  1. 最後,該版方案中數據經Protocol Buffer(PB)序列化之後存入HBase,這會導致合併/更新用戶畫像標籤碎片的次數過多,使得一個標籤需要讀取多次JanusGraph與HBase,這無疑會加重HBase讀取壓力。此外,由於數據經過了PB序列化,使得其原始存儲格式不可讀,增加了排查問題的難度。

鑑於這些問題,我們提出了2.0版本的解決方案。在2.0版本中,我們通過利用HBase列式存儲、修改圖數據結構等優化方案嘗試解決以上三個問題。

2.0版數據處理流程

版本流程優化點

如下圖所示,2.0版本數據處理流程大部分承襲了1.0版本。新版本數據處理流程在以下幾個方面做了優化:

2.0版本數據處理流程

  1. 歷史數據的離線補錄方式由JAVA服務變更爲使用Flink實現。
  2. 優化用戶畫像圖數據結構模型,主要是對邊的連接方式進行了修改。之前我們會判斷節點的類型並根據預設的優先級順序將多個節點進行連接,新方案則採用以UserKey爲中心的連接方式。做此修改後,之前的大的連通子圖(圖6)優化爲下面的小的連通子圖(圖8),同時解決了數據污染問題,保證了數據準確性。另外,1.0版本中一條數據需要平均讀取十多次HBase的情況也得到極大緩解。採用新方案之後平均一條數據只需讀取三次HBase,從而降低HBase六七倍的讀取壓力(此處優化是數據計算層優化)。

Gephi

  1. 舊版本是用Protocol Buffer作爲用戶畫像數據的存儲對象,生成用戶畫像數據後作爲一個列整體存入HBase。新版本使用Map存儲用戶畫像標籤數據,Map的每對KV都是單獨的標籤,KV在存入HBase後也是單獨的列。新版本存儲模式利用HBase做列的擴展與合併,直接生成完整用戶畫像數據,去掉Flink合併/更新用戶畫像標籤過程,優化數據加工流程。使用此方案後,存入HBase的標籤數據具備了即席查詢功能。數據具備即席查詢是指在HBase中可用特定條件直接查看指定標籤數據詳情的功能,它是數據治理可以實現校驗數據質量、數據生命週期、數據安全等功能的基礎條件。
  2. 在數據服務層,我們利用Flink批量讀取HBase的Hive外部表生成用戶質量分等數據,之後將其存入Phoenix。相比於舊方案中Spark全量讀HBase導致其讀壓力過大,從而會出現集羣節點宕機的問題,新方案能夠有效地降低HBase的讀取壓力。經過我們線上驗證,新方案對HBase的讀負載下降了數十倍(此處優化與2優化不同,屬於服務層優化)。

四、問題

目前,線上部署的用戶畫像系統中的數據絕大部分是來自於Kafka的實時數據。隨着數據量越來越多,系統的壓力也越來越大,以至於出現了Flink背壓與Checkpoint超時等問題,導致Flink提交Kafka位移失敗,從而影響了數據一致性。這些線上出現的問題讓我們開始關注Flink的可靠性、穩定性以及性能。針對這些問題,我們進行了詳細的分析並結合自身的業務特點,探索並實踐出了一些相應的解決方案。

CheckPointing 流程分析與性能優化方案

CheckPointing流程分析:

下圖展示了Flink中checkpointing執行流程圖:

Flink中checkpointing執行流程

  1. Coordinator向所有Source節點發出Barrier。
  2. Task從輸入中收到所有Barrier後,將自己的狀態寫入持久化存儲中,並向自己的下游繼續傳遞Barrier。
  3. 當Task完成狀態持久化之後將存儲後的狀態地址通知到Coordinator。
  4. 當Coordinator彙總所有Task的狀態,並將這些數據的存放路徑寫入持久化存儲中,完成CheckPointing。

性能優化方案:

通過以上流程分析,我們通過三種方式來提高Checkpointing性能。這些方案分別是:

  1. 選擇合適的Checkpoint存儲方式
  2. 合理增加算子(Task)並行度
  3. 縮短算子鏈(Operator Chains)長度

選擇合適的Checkpoint存儲方式

CheckPoint存儲方式有MemoryStateBackend、FsStateBackend和RocksDBStateBackend。由官方文檔可知,不同StateBackend之間的性能以及安全性是有很大差異的。通常情況下,MemoryStateBackend適合應用於測試環境,線上環境則最好選擇RocksDBStateBackend。

這有兩個原因:首先,RocksDBStateBackend是外部存儲,其他兩種Checkpoint存儲方式都是JVM堆存儲。受限於JVM堆內存的大小,Checkpoint狀態大小以及安全性可能會受到一定的制約;其次,RocksDBStateBackend支持增量檢查點。增量檢查點機制(Incremental Checkpoints)僅僅記錄對先前完成的檢查點的更改,而不是生成完整的狀態。與完整檢查點相比,增量檢查點可以顯著縮短checkpointing時間,但代價是需要更長的恢復時間。

合理增加算子(Task)並行度

Checkpointing需要對每個Task進行數據狀態採集。單個Task狀態數據越多則Checkpointing越慢。所以我們可以通過增加Task並行度,減少單個Task狀態數據的數量來達到縮短CheckPointing時間的效果。

縮短算子鏈(Operator Chains)長度

Flink算子鏈(Operator Chains)越長,Task也會越多,相應的狀態數據也就更多,Checkpointing也會越慢。通過縮短算子鏈長度,可以減少Task數量,從而減少系統中的狀態數據總量,間接的達到優化Checkpointing的目的。下面展示了Flink算子鏈的合併規則:

  1. 上下游的並行度一致
  2. 下游節點的入度爲1
  3. 上下游節點都在同一個Slot Group中
  4. 下游節點的Chain策略爲ALWAYS
  5. 上游節點的Chain策略爲ALWAYS或HEAD
  6. 兩個節點間數據分區方式是Forward
  7. 用戶沒有禁用Chain

基於以上這些規則,我們在代碼層面上合併了相關度較大的一些Task,使得平均的操作算子鏈長度至少縮短了60%~70%。

Flink背壓產生過程分析及解決方案

背壓產生過程分析:

在Flink運行過程中,每一個操作算子都會消費一箇中間/過渡狀態的流,並對它們進行轉換,然後生產一個新的流。這種機制可以類比爲:Flink使用阻塞隊列作爲有界的緩衝區。跟Java裏阻塞隊列一樣,一旦隊列達到容量上限,處理速度較慢的消費者會阻塞生產者向隊列發送新的消息或事件。下圖展示了Flink中兩個操作算子之間的數據傳輸以及如何感知到背壓的:

首先,Source中的事件進入 Flink並被操作算子1處理且被序列化到Buffer中,然後操作算子2從這個Buffer中讀出該事件。當操作算子2處理能力不足的時候,操作算子1中的數據便無法放入Buffer,從而形成背壓。背壓出現的原因可能有以下兩點:1、下游算子處理能力不足;2、數據發生了傾斜。

背壓解決方案

實踐中我們通過以下方式解決背壓問題。首先,縮短算子鏈會合理的合併算子,節省出資源。其次縮短算子鏈也會減少Task(線程)之間的切換、消息的序列化/反序列化以及數據在緩衝區的交換次數,進而提高系統的整體吞吐量。最後,根據數據特性將不需要或者暫不需要的數據進行過濾,然後根據業務需求將數據分別處理,比如有些數據源需要實時的處理,有些數據是可以延遲的,最後通過使用keyBy關鍵字,控制Flink時間窗口大小,在上游算子處理邏輯中儘量合併更多數據來達到降低下游算子的處理壓力。

優化結果

經過以上優化,在每天億級數據量下,用戶畫像可以做到實時信息實時處理並無持續背壓,Checkpointing平均時長穩定在1秒以內。

五、未來工作的思考和展望

端到端的實時流處理

目前用戶畫像部分數據都是從Hive數據倉庫拿到的,數據倉庫本身是T+1模式,數據延時性較大,所以爲了提高數據實時性,端到端的實時流處理很有必要。

端到端是指一端採集原始數據,另一端以報表/標籤/接口的方式對這些對數進行呈現與應用,連接兩端的是中間實時流。在後續的工作中,我們計劃將現有的非實時數據源全部切換到實時數據源,統一經過Kafka和Flink處理後再導入到Phoenix/JanusGraph/HBase。強制所有數據源數據進入Kafka的一個好處在於它能夠提高整體流程的穩定性與可用性:首先Kafka作爲下游系統的緩衝,可以避免下游系統的異常影響實時流的計算,起到“削峯填谷”的作用;其次,Flink自1.4版本開始正式支持與Kafka的端到端精確一次處理語義,在一致性方面上更有保證。

作者介紹:

楊毅:友信金服計算平臺部JAVA工程師

穆超峯:友信金服計算平臺部數據開發高級工程師

賀小兵:友信金服計算平臺部數據開發工程師

胡夕:友信金服計算平臺部技術總監

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章