離線計算平臺系列之一

regrecall發表於2016-08-03

離線計算平臺簡介

在螞蟻金服風控體系裡面,有一個重要的環節就是離線模擬,在規則,模型上線之前,在離線的環境裡面進行模擬驗證,來對規則和模型進行效能的評估,避免人為因素造成不準確性從而造成的資損。起初為了達到這個目的,離線計算平臺就這樣孕育而生了,慢慢地整個離線平臺覆蓋了更多風控的業務,也慢慢變成目前Odps-Spark最大的使用者,擁有的叢集數目也是最大的。離線計算平臺主要以Spark為基礎,在其上建立起來的一套平臺. 後面我們團隊會給大家帶來一系列,關於離線平臺的架構以及我們做過相應業務以及經驗,希望和大家一起來探討。
下面由我來給大家分享下,我們整個團隊建立起離線計算平臺裡面的SparkContext管理以及幾個Spark優化手段。

SparkContext 管理

在我們的離線業務場景裡面,我們需要持續地接受使用者提交實驗任務進行分析以及運算,所以對於Spark的要求第一點就是需要Long-Live的SparkContext, 可以持續接受任務提交,而不是類似d2那樣提交一次性的任務。所以我們以目前Odps-Spark Client模式為基礎,建立了一套自己的SparkContext 管理系統,提供了動態新增,刪除SparkContext功能,可以持續地提交任務,排程相應任務的功能。

Spark1

由於SparkContext需要一個獨立的JVM程式服務,我們目前是利用RMI來完成啟動SparkContext的工作。 在應用伺服器上面,當它被通知自己需要啟動SparkContext的時候,就會在進行相應的Spark Jar準備,然後進行打包,並且啟動相應的RMI程式來當做SparkContext. 在任務執行的流程中,前端系統會向離線系統提交任務,離線系統就有一套自己的任務排程系統,根據目前管理的SparkContext裡面選擇一個相對比較空閒的,進行任務提交,否則就進行等待。當任務提交到RMI Server之後,就會對任務進行相應的組裝以及轉化成Spark可以執行的任務,進行執行並且同Spark Cluster進行互動。最終Spark任務結果會同步地返回給RMI Server, 然後再通過RMI Server非同步的通知到應用伺服器,並且進行任務的進一步處理。

經驗分享:控制好每個SparkContext的併發度,充分利用SparkContext的能力(目前我們一個SparkContext上面併發3個任務,後續會引入動態任務大小的評估,從而實現真正的動態任務提交以及資源分配),統一管理每個SparkContext下面的併發執行緒,而且不是每個任務都去建立執行緒池,特別注意從RMI Server返回給離線系統資料,不能帶有Scala相關的東西,離線系統無法解析。優雅地停掉SparkContext, 防止資源的洩露。


Spark程式調優手段

對於風控來說,檢視使用者歷史資料來進行判斷風險的行為是一種常見的手段,所以動不動就30天的交易記錄,90的某相累計資料。。。 做離線的小夥伴已經哭暈在廁所,在做Spark離線的期間,我們也產出了自己的東西,這裡我就介紹三種比較有意義的內容,後續歡迎大家來交流。

  • 動態載入Jar
    由於安全性的問題,目前Odps-Spark並沒有實現動態載入Jar的功能,那如果真有這部分需求的時候,我們應該怎麼做呢?例如有些規則指令碼在我們實驗的時候進行了更新,離線也需要同步更新,可是這裡SparkContext已經建立出來,沒有辦法再新增Jar了。目前我們的解決方案是,在執行任務的時候,把這些Jar打好包,並且建立出一個ClassLoader來載入這些可變的Jar, 然後在實驗的時候,把Classloader 通過broadcast機制分發到各個worker上面,然後再需要利用這些Jar的閉包裡面,通過這個classloader進行載入並且執行。這裡需要注意的問題是: 多執行緒的問題, 因為broadcast value每臺worker只有一份,如果一個臺worker上面有多個cpu去訪問broadcast value的時候,如果沒有控制好多執行緒的問題,就會出現一些奇葩問題,而且這種問題不好排查,因為已經在worker上面,不好去打日誌進行驗證。所以在處理Broadcast value的時候,儘量做到執行緒安全。
  • 減少shuffle key的數目
    在Spark開發中,基本上都會用到join, cogroup等操作,這類操作就會產生shuffle操作, Spark程式的效能很大程度就是取決於shuffle的效能上面,除了調優修改shuffle的引數(spark.shuffle.memoryFraction, spark.shuffle.manager, spark.shuffle.file.buffer等), 也可以利用其它手段來完成

    1. map side join, 特別是在兩個RDD, 一個資料量小, 一個資料量大的情況,我們可以把資料量小的那個做成broadcast, 從而把這次shuffle操作轉變成一次map操作,大大地減少shuffle中的效能消耗。
    2. 利用Bloom Filter過濾掉多餘的key, 這也是我們在實踐發現的,當兩個RDD進行cogroup的時候,其實會有很多無用的key, 例如用userId進行關聯的時候,會很多無用的userId進行干擾,但是他們也參與整個shuffle的流程中,這時候我們可以把key數目相對比較少的那個RDD的key收集來(可能會有多次collect操作,因為每次collect操作有大小限制),然後把這些做成Bloom Filter去過濾另外一個RDD裡面的key, 從而達到減少shuffle中間的資料量的效果。
  • 資料傾斜的解決
    當你在logview上面發現你的2400分割槽的資料,2399都跑完,另外一個分割槽怎麼跑都跑不完,並且執行時間已經遠遠超越同伴啦。那麼恭喜你,很有可能是資料傾斜發生了。目前介紹下我們的做法

    • 首先是去採集哪些key會出現資料傾斜,這裡可以使用groupbykey,然後進行count, 如果這樣都會掛掉,那麼進行sample抽樣來解決,隨機抽樣10%的資料來進行判斷。
    • 找到這樣的key之後, 在RDD A裡面過濾掉熱點對應的key, 形成nonHot的RDD A, 然後針對有HotKey的RDD, 裡面的每一個key打上 n 以內的隨機數作為字尾。

        hasHotEvent.map(x =>   
      if (hotKeySet.value.contains(x.1)) {  
      (x.1 + "@" + Random.nextInt(hashPartitionSize), x.2)  
       else   
      x  
        
      })
    • 在RDD B裡面首先同樣先過濾掉熱點的key, 形成nonHot的RDD B, 然後針對HotKey的RDD, 裡面每一個key按順序附加 0 – n 的字尾,每條hotKey的資料就會膨脹成n條資料

    1. hotValues = res.filter(x => hotKeySet.value.contains(x.1)).flatMap(x =>
      for (i <- 0.until(hashPartitionSize)) yield {

      (x.1 + "@" + i, x.2)  

      }

      })
    • 最後notHotKey的RDD 進行join, hotkey的RDD進行join, 最後再進行union操作就可以等到最後的結果。

經驗總結:主要思路就是把少數的key,打散成n份去進行join,這樣就不會集中大量的資料在一個worker中,並且把它高掛。如果針對RDD裡面很多的key都是hot key的情況,就無需過濾hot key, 直接給一個RDD打上隨機數,另外一個RDD擴容n份進行join.


To Be Continued

還有很多細節以及經驗想跟大家分享,奈何實力有限,後續的分享會持續到來,大家會看到變態資料點優化,velocity資料結構優化,會針對新版的記憶體計算平臺進行相應的改造以及相關的技術落地。 以上都是來自 螞蟻金服-大安全-成都安全技術平臺的小夥伴們。順便打個小廣告,我們在大量招人,希望回成都的同學,以及想做離線這塊的同學(當然我們也很多實時的業務),可以聯絡@鋒揚,@雲震。


相關文章