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

此文已由做者肖凡受權網易雲社區發佈。
html

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

背景

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


寫在前面

在給出總體架構前,想跟你們聊聊關於架構的一些想法。目前架構上的分層設計思想已經深刻人心,你們都知道要分紅controller,server,dao等,是由於咱們剛接觸到編碼的時候,mvc的模型已經大行其道,早期的jsp裏面包含大量業務代碼邏輯的方式已經基本絕跡。可是這並非一種面向對象的思考方式,而每每咱們是以一種面向過程的思惟去編程。舉個簡單例子,咱們要實現一個網銀帳戶之間轉帳的需求,每每會是下面這種實現方式:算法


  1. 設計一個帳戶交易服務接口AccountingService,設計一個服務方法transfer(),並提供一個具體實現類AccountingServiceImpl,全部帳戶交易業務的業務邏輯都置於該服務類中。spring

  2. 提供一個AccountInfo和一個Account,前者是一個用於與展現層交換帳戶數據的帳戶數據傳輸對象,後者是一個帳戶實體(至關於一個EntityBean),這兩個對象都是普通的JavaBean,具備相關屬性和簡單的get/set方法。sql

  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的方式進行。

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

  2. 滑動窗口計算。好比咱們的指標是最近10分鐘的同一用戶的下單量,那麼咱們就須要實現一種相似的滑動窗口算法,以便任什麼時候候都能拿到「最近10分鐘」的數據。這裏咱們採用的是一種簡單的算法:建立指標時,指定好採樣次數。好比要獲取「最近10分鐘」的數據,採樣次數設置成30次,這樣咱們會把每隔20秒的數據會放入一個key裏面。每次一個下單事件過來時,計算出時間間隔序號(見第1點),而後組裝好key以後看該key是否存在,存在則進行累計,不然往redis中添加該key。

  3. 如何批量獲取key。每次獲取指標值時,咱們都是先計算出須要的key集合(好比我要獲取「單個帳號最近10分鐘的下單量」,我可能須要獲取30個key,由於每一個key的跨度是20s),而後獲取到對應的value集合,再進行累加。而實際上咱們只是須要累加後的值,這裏能夠經過redis+lua腳本進行優化,腳本里面直接根據key集合獲取value後進行累加而後返回給客戶端,這樣就較少了每次響應的數據量。

  4. 如何保證指標的計算結果不丟失?目前的指標是存儲在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接口調用

相關文章
相關標籤/搜索