網易考拉規則引擎平臺架構設計與實踐

網易雲社群發表於2018-10-29

此文已由作者肖凡授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。

背景

考拉安全部技術這塊目前主要負責兩塊業務:一個是內審,主要是通過敏感日誌管理平臺蒐集考拉所有後臺系統的操作日誌,資料匯入到es後,結合storm進行實時計算,主要有行為查詢、資料監控、事件追溯、風險大盤等功能;一個是業務風控,主要是下單、支付、優惠券、紅包、簽到等行為的風險控制,對抗的風險行為包括黃牛刷單、惡意佔用庫存、機器領券、擼羊毛等。這兩塊業務其實有一個共通點,就是有大量需要進行規則決策的場景,比如內審中需要進行實時監控,當同一個人在一天時間內的匯出操作超過多少次後進行告警,當登入時不是常用地登入並且裝置指紋不是該賬號使用過的裝置指紋時告警。而在業務風控中需要使用到規則決策的場景更多,由於涉及規則的保密性,這裡就不展開了。總之,基於這個出發點,安全部決定開發出一個通用的規則引擎平臺,來滿足以上場景。


寫在前面

在給出整體架構前,想跟大家聊聊關於架構的一些想法。目前架構上的分層設計思想已經深入人心,大家都知道要分成controller,server,dao等,是因為我們剛接觸到編碼的時候,mvc的模型已經大行其道,早期的jsp裡面包含大量業務程式碼邏輯的方式已經基本絕跡。但是這並不是一種物件導向的思考方式,而往往我們是以一種程式導向的思維去程式設計。舉個簡單例子,我們要實現一個網銀賬戶之間轉賬的需求,往往會是下面這種實現方式:


  1. 設計一個賬戶交易服務介面AccountingService,設計一個服務方法transfer(),並提供一個具體實現類AccountingServiceImpl,所有賬戶交易業務的業務邏輯都置於該服務類中。

  2. 提供一個AccountInfo和一個Account,前者是一個用於與展示層交換賬戶資料的賬戶資料傳輸物件,後者是一個賬戶實體(相當於一個EntityBean),這兩個物件都是普通的JavaBean,具有相關屬性和簡單的get/set方法。

  3. 然後在transfer方法中,首先獲取A賬戶的餘額,判斷是否大於轉賬的金額,如果大於則扣減A賬戶的餘額,並增加對應的金額到B賬戶。


這種設計在需求簡單的情況下看上去沒啥問題,但是當需求變得複雜後,會導致程式碼變得越來越難以維護,整個架構也會變的腐爛。比如現在需要增加賬戶的信用等級,不同等級的賬戶每筆轉賬的最大金額不同,那麼我們就需要在service裡面加上這個邏輯。後來又需要記錄轉賬明細,我們又需要在service裡面增加相應的程式碼邏輯。最後service程式碼會由於需求的不斷變化變得越來越長,最終變成別人眼中的“祖傳程式碼”。導致這個問題的根源,我認為就是我們使用的是一種程式導向的程式設計思想。那麼如何去解決這種問題呢?主要還是思維方式上需要改變,我們需要一種真正的物件導向的思維方式。比如一個“人”,除了有id、姓名、性別這些屬性外,還應該有“走路”、“吃飯”等這些行為,這些行為是天然屬於“人”這個實體的,而我們定義的bean都是一種“失血模型”,只有get/set等簡單方法,所有的行為邏輯全部上升到了service層,這就導致了service層過於臃腫,並且很難複用已有的邏輯,最後形成了各個service之間錯綜複雜的關聯關係,在做服務拆分的時候,很難劃清業務邊界,導致服務化程式陷入泥潭。


對應上面的問題,我們可以在Account這個實體中加入本應該就屬於這個實體的行為,比如借記、貸記、轉賬等。每一筆轉賬都對應著一筆交易明細,我們根據交易明細可以計算出賬戶的餘額,這個是一個潛在的業務規則,這種業務規則都需要交由實體本身來維護。另外新增賬戶信用實體,提供賬戶單筆轉賬的最大金額計算邏輯。這樣我們就把原本全部在service裡面的邏輯劃入到不同的負責相關職責的“領域物件”當中了,service的邏輯變得非常清楚明瞭,想實現A給B轉賬,直接獲取A實體,然後呼叫A實體中的轉賬方法即可。service將不再關注轉賬的細節,只負責將相關的實體組織起來,完成複雜的業務邏輯處理。


上面的這種架構設計方式,其實就是一種典型的“領域驅動設計(DDD)”思想,在這裡就不展開說明了(主要是自己理解的還不夠深入,怕誤導大家了)。DDD也是目前非常熱門的一種架構設計思想了,它不能減少你的程式碼量,但是能使你的程式碼具有很高的內聚性,當你的專案變得越來越複雜時,能保持架構的穩定而不至於過快的腐爛掉,不瞭解的同學可以檢視相關資料。要說明的是,沒有一種架構設計是萬能的、是能解決所有問題的,我們需要做的是吸收好的架構設計思維方式,真正架構落地時還是需要根據實際情況選擇合適的架構。


整體架構設計

上面說了些架構設計方面的想法,現在我們回到規則引擎平臺本身。我們抽象出了四個分層,從上到下分別為:服務層、引擎層、計算層和儲存層,整個邏輯層架構見下圖:


Alt pic


  • 服務層:服務層主要是對外提供服務的入口層,提供的服務包括資料分析、風險檢測、業務決策等,所有的服務全部都是通過資料接入模組接入資料,具體後面講

  • 引擎層:引擎層是整個平臺的核心,主要包括了執行規則的規則引擎、還原事件現場和聚合查詢分析的查詢引擎以及模型預測的模型引擎

  • 計算層:計算層主要包括了指標計算模組和模型訓練模組。指標會在規則引擎中配置規則時使用到,而模型訓練則是為模型預測做準備

  • 儲存層:儲存層包括了指標計算結果的儲存、事件資訊詳情的儲存以及模型樣本、模型檔案的儲存


在各個分層的邏輯架構劃定後,我們就可以開始分析整個平臺的業務功能模組。主要包括了事件接入模組、指標計算模組、規則引擎模組、運營中心模組,整個業務架構如下圖:


Alt pic


1.事件接入中心


事件接入中心主要包括事件接入模組和資料管理模組。資料接入模組是整個規則引擎的資料流入口,所有的業務方都是通過這個模組接入到平臺中來。提供了實時(dubbo)、準實時(kafka)和離線(hive)三種資料接入方式。資料管理模組主要是進行事件的後設資料管理、標準化接入資料、補全必要的欄位,如下圖: Alt pic


2.指標計算模組


指標計算模組主要是進行指標計算。一個指標由主維度、從維度、時間視窗等組成,其中主維度至少有一個,從維度最多有一個。如下圖: Alt pic


舉個例子,若有這樣一個指標:“最近10分鐘,同一個賬號在同一個商家的下單金額”,那麼主維度就是下單賬號+商家id,從維度就是訂單金額。可以看到,這裡的主維度相當於sql裡面的group by,從維度相當於count,數值累加相當於sum。從關於指標計算,有幾點說明下:


  1. key的構成。我們的指標儲存是用的redis,那麼這裡會涉及到一個key該如何構建的問題。我們目前的做法是:key=指標id+版本號+主維度值+時間間隔序號。

  • 指標id就是指標的唯一標示;

  • 版本號是指標物件的版本,每次更新完指標都會更新對應的版本號,這樣可以讓就的指標一次全部失效;

  • 主維度值是指當前事件物件中,主維度欄位對應的值,比如一個下單事件,主維度是使用者賬號,那麼這裡就是對應的類似XXX@163.com,如果有多個主維度則需要全部組裝上去;

  • 如果主維度的值出現中文,這樣直接拼接在key裡面會有問題,可以採用轉義或者md5的方式進行。

  • 時間間隔序號是指當前時間減去指標最後更新時間,得到的差值再除以取樣週期,得到一個序號。這麼做主要是為了實現指標的滑動視窗計算,下面會講

  • 滑動視窗計算。比如我們的指標是最近10分鐘的同一使用者的下單量,那麼我們就需要實現一種類似的滑動視窗演算法,以便任何時候都能拿到“最近10分鐘”的資料。這裡我們採用的是一種簡單的演算法:建立指標時,指定好取樣次數。比如要獲取“最近10分鐘”的資料,取樣次數設定成30次,這樣我們會把每隔20秒的資料會放入一個key裡面。每次一個下單事件過來時,計算出時間間隔序號(見第1點),然後組裝好key之後看該key是否存在,存在則進行累計,否則往redis中新增該key。

  • 如何批量獲取key。每次獲取指標值時,我們都是先計算出需要的key集合(比如我要獲取“單個賬號最近10分鐘的下單量”,我可能需要獲取30個key,因為每個key的跨度是20s),然後獲取到對應的value集合,再進行累加。而實際上我們只是需要累加後的值,這裡可以通過redis+lua指令碼進行優化,指令碼里面直接根據key集合獲取value後進行累加然後返回給客戶端,這樣就較少了每次響應的資料量。

  • 如何保證指標的計算結果不丟失?目前的指標是儲存在redis裡面的,後來會切到solo-ldb,ldb提供了持久化的儲存引擎,可以保證資料不丟失。


  • 3.規則引擎模組

    計劃開始做規則引擎時進行過調研,發現很多類似的平臺都會使用drools。而我們從一開始就放棄了drools而全部使用groovy指令碼實現,主要是有以下幾點考慮:


    • drools相對來說有點重,而且它的規則語言不管對於開發還是運營來說都有學習成本

    • drools使用起來沒有groovy指令碼靈活。groovy可以和spring完美結合,並且可以自定義各種元件實現外掛化開發。

    • 當規則集變得複雜起來時,使用drools管理起來有點力不從心。


    當然還有另外一種方式是將drools和groovy結合起來,綜合雙方的優點,也是一種不錯的選擇,大家可以嘗試一下。


    規則引擎模組是整個平臺的核心,我們將整個模組分成了以下幾個部分: Alt pic


    規則引擎在設計中也碰到了一些問題,這裡給大家分享下一些心得:


    • 使用外掛的方式載入各種元件到上下文中,極大的方便了功能開發的靈活性。

    • 使用預載入的方式載入已有的規則,並將載入後的物件快取起來,每次規則變更時重新load整條規則,極大的提升了引擎的執行效率

    • 計數器引入AtomicLongFieldUpdater工具類,來減少計數器的記憶體消耗

    • 靈活的上下文使用方式,方便定製規則執行的流程(規則執行順序、同步非同步執行、跳過某些規則、規則集短路等),靈活定義返回結果(可以返回整個上下文,可以返回每條規則的結果,也可以返回最後一條規則的結果),這些都可以通過設定上下文來實現。

    • groovy的方法查詢策略,預設是從metaClass裡面查詢,再從上下文裡找,為了提升效能,我們重寫了metaClass,修改了這個查詢邏輯:先從上下文裡找,再從metaClass裡面找。


    規則配置如下圖所示:


    Alt pic


    未來規劃

    後面規則引擎平臺主要會圍繞下面幾點來做:


    1. 指標儲存計劃從redis切換到hbase。目前的指標計算方式會導致快取key的暴漲,獲取一個指標值可能需要N個key來做累加,而換成hbase之後,一個指標就只需要一條記錄來維護,使用hbase的列族來實現滑動視窗的計算。

    2. 規則的灰度上線。當一條新規則建立後,如果不進行灰度的測試,直接上線是可能會帶來災難的。後面再規則上線流程中新增灰度上線環節,整個引擎會根據配置的灰度比例,複製一定的流量到灰度規則中,並對灰度規則的效果進行展示,達到預期效果並穩定後才能審批上線。

    3. 事件接入的自動化。dubbo這塊可以採用泛化呼叫,http介面需要統一呼叫標準,訊息需要統一格式。有了統一的標準就可以實現事件自動接入而不需要修改程式碼上線,這樣也可以保證整個引擎的穩定性。

    4. 模型生命週期管理。目前模型這塊都是通過在猛獁平臺上提交jar包的方式,離線跑一個model出來,沒有一個統一的平臺去管控整個模型的生命週期。現在杭研已經有類似的平臺了,後續需要考慮如何介入。

    5. 資料展示優化。現在整個平臺的數字化做的比較弱,沒法形成資料驅動業務。而風控的運營往往是需要大量的資料去驅動規則的優化的,比如規則閾值的除錯、規則命中率、風險大盤等都需要大量資料的支撐。


    網易雲免費體驗館,0成本體驗20+款雲產品!

    更多網易技術、產品、運營經驗分享請點選


    相關文章:
    【推薦】 【網易嚴選】iOS持續整合打包(Jenkins+fastlane+nginx)
    【推薦】 基於雲原生的秒殺系統設計思路
    【推薦】 一行程式碼搞定Dubbo介面呼叫


    相關文章