Pinterest是如何基於Flink做實時分析的?

在Pinterest,我們每天都要進行數千個實驗。我們主要依靠日常實驗指標來評估實驗效果。日常實驗管道運行一次可能會花費10多個小時,有時還會超時,因此想要驗證實驗設置、觸發的正確性以及預期的實驗性能時就沒那麼方便了。當代碼中存在一些錯誤時這個問題尤爲突出。有時可能要花幾天時間才能發現錯誤,這對用戶體驗和重要指標造成了更大的損害。我們在Pinterest開發了一個近實時實驗平臺,以提供更具時效性的實驗指標,從而幫助我們儘快發現這些問題。

可能出現的問題有:

  1. 實驗導致impression的統計數據顯著下降,因此需要儘快關閉實驗。
  2. 與對照組相比,實驗導致搜索的執行次數顯著增加。

圖1-帶有置信區間的實時實驗指標

上圖的面板顯示了所選事件的實驗組和對照組的流量(也就是動作數)和傾向(也就是unique user的數量)。自實驗開始以來,這些計數已經累計了3天時間。如果在3天后發生了re-ramp(分配給實驗組和對照組的用戶數量增加),則計數會歸零0並重新開始累計3天時間。

爲了確保實驗組與對照組之間的對比在統計上是有效的,我們做了一些統計檢驗。由於指標是實時交付的,因此每次按順序收到新記錄時,我們都必須進行這些檢驗。這需要與傳統的固定視野檢驗不一樣的方法,否則會帶來較高的假正率。我們考慮過幾種順序測試方法,包括賭徒破產貝葉斯A/B檢驗Alpha消耗函數方法。爲了保證數值穩定性,我們從t檢驗+ Boferroni校正(將我們的案例作爲多次檢驗進行處理)開始,併爲我們的初始實現預先確定了檢驗次數。

高階設計

圖2-實時實驗管道的高階設計

實時實驗管道包括下列主要組件:

  • 最近ramp的實驗組作業→每5分鐘將一個CSV文件發佈到一個S3位置。這個CSV是過去3天中所分配用戶有所增加的實驗組的快照。通過查詢託管實驗元數據的內部Analytics(分析)應用程序的MySQL數據庫,就能獲得這一信息。
  • 篩選事件作業→我們分析了Pinterest上的數百種用戶動作。這一作業僅保留最關key的業務事件,這些事件已插入“filtered_events”Kafka主題中。這些事件被剝離掉了不需要的字段,因此filtered_events主題相當輕巧。該作業運行在Flink processing時間內,並且通過Flink的增量檢查點,每隔5秒將其進度保存到HDFS中。
  • 過濾實驗Activation作業→每當一個用戶被觸發進入一個實驗時,都會創建一個Activation(激活)記錄。觸發規則取決於實驗邏輯,一名用戶可以被觸發進入一個實驗數百次。我們只需要最近3天啓動,或組分配增加的實驗的Activation記錄即可。

爲了過濾Activation記錄,此作業使用Flink的廣播狀態模式。每10秒檢查一次“最近ramp的實驗組”作業所發佈的CSV的更改情況,並將其發佈到一個KeyedBroadcastProcessFunction的所有分區上,該函數也消費Activation。

KeyedBroadcastProcessFunction將廣播的CSV與Activation流結合在一起,就可以過濾掉那些最近3天內未ramp-up實驗的Activation記錄。此外,“group-ramp-up-time”已添加到Activation記錄中,並插入“filtered_experiment_activations”kafka主題中。

圖3-Scala對象被插入中間層Kafka主題中

圖4-實時實驗累積作業圖

上面是實時累積(Aggregation)Flink作業的高階概覽。這裏簡單提及了一些operator,後文中還將詳細介紹另一些operator。Source operator從Kafka讀取數據,而sink使用一個REST接口寫入我們的內部Analytics Store上。

刪除重複事件→這裏用一個KeyedProcessFunction實現,由(event.user_id,event.event_type,event.timestamp)作爲key。這裏的思想是,如果來自同一用戶的相同事件類型的事件具有相同的時間戳,則它們是重複事件。第一個這樣的事件被髮送到下游,但也會緩存進狀態持續5分鐘時間。任何後續事件都將被丟棄。5分鐘後,一個計時器會啓動並清除狀態。這裏的假定是所有重複事件之間的間隔都在5分鐘之內。

查找首次觸發時間→這裏是一個Flink KeyedProcessFunction,由(experiment_hash,experiment_group,user_id)作爲key。這裏的假設是,爲一個用戶收到的第一個實驗Activation記錄也是具有第一個觸發時間的Activation。一個實驗ramp-up以後,收到的第一個Activation將發送至下游,並保存爲狀態並持續3天時間(我們累積了實驗組ramp-up以來爲期3天的計數)。經過3天的ramp時間後,一個計時器將清除狀態。

15分鐘的processing時間tumbling窗口→事件進入並向下遊發送結果時,Numerator Computer和Denominator computer都將累積計數。這意味着數百萬條記錄,但是我們不需要如此頻繁地將結果發送到Analytics Store上。我們可以在processing時間內運行一個持續15分鐘的Flink tumbling窗口,這樣效率更高。對於Numerator Computer來說,這個窗口由(“experiment_hash”,“experiment_group”,“event_type”,“timestamp”)作爲key。當窗口在15分鐘後觸發時,將獲取帶有max_users的記錄並將其發送到下游的Analytics Store sink。

連接事件和Activation

圖5-通過用戶ID連接Activation流與事件流

我們使用Flink的IntervalJoin operator實現流到流的連接。IntervalJoin會在接下來的3天內緩衝每位用戶的單個Activation記錄,並且所有匹配事件都將與Activation記錄中的其他實驗元數據一起發送到下游。

這種方法的侷限性:

  1. 對我們的需求而言,IntervalJoin operator有點不夠靈活,因爲它的間隔是固定的而不是動態的。比如說,用戶可以在實驗啓動2天后加入進來,但IntervalJoin還是會爲這名用戶運行3天時間,也就是說我們停止累積數據後還會運行2天時間。如果3天后組很快re-ramp,則一位用戶也可以有2個這樣的連接。這種情況會在下游處理。
  2. 事件和Activation不同步:如果Activation作業失敗並且Activation流被延遲,則可能會丟失一些數據,因爲沒有匹配Activation的事件還會繼續流動。這將導致計數不足。

我們研究了Flink的IntervalJoin源代碼。它會在“左側緩衝區”中緩衝Activation 3天時間,但事件將被立即刪除。目前似乎無法通過配置更改此行爲。我們正在研究使用Flink的協同處理函數來實現這個Activation到事件的連接,該函數是用於流到流連接的更通用的函數。我們可以將事件緩衝X分鐘,這樣即使Activation流延遲了X分鐘,管道也可以處理延遲而不會出現計數不足。這將幫助我們避免同一用戶的兩次連接,並能形成更加動態的管道,其可以立即感知到實驗組的re-ramp,並支持更多動態行爲,例如在組re-ramp時自動擴展累積的覆蓋範圍 。

Join Results Deduplicator

圖6-Join Results Deduplicator

Join Results Deduplicator是一個Flink KeyedProcessFunction,它由experiment_hash,experiment_group,event_type,user_id作爲key。這個operator的主要目的是在向下遊發送記錄時插入“user_first_time_seen”標誌——下游Numerator Computer使用這個標誌來計算傾向編號(# unique users),而無需使用設置的數據結構。

這個operator將狀態存儲到last-ramp-time+ 3天,之後狀態將被清除。

Numerator Computer

圖7-Numerator Computer

Numerator Computer是一個KeyedProcessFunction,由experiment_hash,experiment_group,event_type作爲key。它會在最後2小時內一直滾動15分鐘的存儲桶(bucket),每當有新記錄進入時都會更新這些桶。對於流量來說,每個動作都很重要;因此對於每個事件,動作計數都會增加。對於傾向數字(unique user)——它取決於"first_time_seen”標誌(僅在爲true時遞增)。

隨着時間的流逝,存儲桶會滾動/旋轉。每次新事件進入時,存儲桶數據都會向下遊刷新到15分鐘的tumbling窗口中。

它有一個時間爲3天的計時器(從ramp-up時間→3天),可在觸發後清除所有狀態,這樣就能在ramp-up3天后重置/清除計數,完成歸零。

垃圾消息與處理

爲了使我們的流管道具有容錯能力,Flink的增量檢查點和RocksDB狀態後端被用來保存應用程序檢查點。我們面臨的一項有趣挑戰是檢查點失敗。問題似乎在於檢查點流程需要花費很長時間,並且最終會超時。我們還注意到,在發生檢查點故障時通常也會有很高的背壓。

圖8-Flink UI中顯示的檢查點故障

在仔細檢查了檢查點故障的內部機制之後,我們發現超時是由於某些子任務未將確認發送給檢查點協調器而導致的,整個檢查點流程都卡住了,如下所示。

圖9-子任務未發送確認

然後我們針對導致失敗的根本原因應用了一些調試步驟:

  1. 檢查作業管理日誌
  2. 檢查在檢查點期間卡住的子任務的任務管理器日誌
  3. 使用Jstack詳細查看子任務

原來子任務運行很正常,只是抽不出空來處理消息。結果,這個特定的子任務具有很高的背壓,從而阻止了barrier通過。沒有barrier的收據,檢查點流程將無法進行。

在進一步檢查所有子任務的Flink指標之後,我們發現其中一個子任務產生的消息數量比其對等任務多100倍。由於消息是通過user_id在子任務之間分區的,這表明有些用戶產生的消息比其他用戶多得多,這就意味着那是垃圾消息。臨時查詢我們的spam_adjusted數據集後也確認了這一結果。

圖10-不同子任務的消息數

爲了緩解該問題,我們在“過濾器事件作業”中應用了一個上限規則:對於一個小時內的用戶,如果我們看到的消息多於X條,則僅發送前X條消息。應用上限規則後,檢查點就不再出現故障了。

數據穩健性和驗證

數據準確性對於實驗指標的計算而言更爲重要。爲了確保我們的實時實驗流程按預期運行,並始終提供準確的指標,我們啓動了一個單獨的每日工作流,其執行與流作業相同的計算,但使用的是臨時方式。如果流作業結果違反以下任一條件,則會提醒開發人員:

  • 在同一累積期間(本例中爲3天),計數不應減少
  • 如果在第一個累積期之後進行了re-ramp,則計數應從0開始再累積3天
  • 流結果與驗證流結果之間的差異不應超過某個閾值(在我們的例子中爲2%)。

通過查詢實驗元數據,我們分別在3種情況下對實驗進行了驗證:

  1. 單次ramp-up實驗
  2. 在初始累積期間內進行多次ramp-up實驗
  3. 在初始累積期後進行多次ramp-up實驗

這一流程如下所示:

圖11-驗證流程

規模

在這一部分中,我們提供了一些基本統計信息,展示實時實驗管道的規模:

  1. 輸入主題流量(一天的平均值):
Kafka主題名稱 消息數/每秒 MB/每秒
experiment_activation 2,513,006.863 1,873.295
event 127,347.091 64.704
filted_experiment_activation 876,906.711 88.237
filtered_backend_event 9,478.253 0.768
  1. 100G檢查點
  2. 200~300個實驗
  3. 8個master,50個worker,每個都是ec2 c5d.9xlarge
  4. 計算的並行度爲256

未來計劃

  1. 支持更多指標,例如PWT(pinner等待時間),這樣如果實驗導致Pinner的延遲異常增加,則可以儘快停止。
  2. 可能更新管道以使用Flink的協同處理功能代替“間隔連接”,使管道更具動態性和彈性,以應對事件流和Activation流之間的不同步問題。
  3. 分區:研究分區可以支持的分區類型,因爲分區會導致狀態增加。
  4. 通過電子郵件或Slack支持實時警報。

致謝

實時實驗分析是Pinterest在生產環境中的第一個基於Flink的應用程序。非常感謝我們的大數據平臺團隊(特別感謝Steven Bairos-Novak、Jooseong Kim和Ang Zhang)構建了Flink平臺並將其作爲服務提供出來。同時還要感謝Analytics Platform團隊(Bo Sun)出色的可視化效果,Logging Platform團隊提供實時數據提取,以及Data Science團隊(Brian Karfunkel)提供的統計諮詢!

原文鏈接

https://www.ververica.com/blog/real-time-experiment-analytics-at-pinterest-using-apache-flink

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