1. 程式人生 > 其它 >從0到1:構建強大且易用的規則引擎

從0到1:構建強大且易用的規則引擎

引言

2016年07月恰逢美團點評的業務進入“下半場”,需要我們在各個環節優化體驗、提升效率、降低成本。技術團隊需要怎麼做來適應這個變化?這個問題直接影響著之後的工作思路。

美團外賣的CRM業務步入成熟期,規則類需求幾乎撐起了這個業務所有需求的半邊天。一方面規則唯一不變的是“多變”,另一方面開發團隊對“規則開發”的感受是乏味、疲憊和缺乏技術含量。如何解決規則開發的效率問題,最大化解放開發團隊成為目前的一個KPI。

規則引擎作為常見的維護策略規則的框架很快進入我的思路。它能將業務決策邏輯從系統邏輯中抽離出來,使兩種邏輯可以獨立於彼此而變化,這樣可以明顯降低兩種邏輯的維護成本。

分析規則引擎如何設計正是本文的主題,過程中也簡單介紹了實現方案。

案例

首先回顧幾個美團點評的業務場景。通過這些場景大家能更好地理解什麼是規則,規則的邊界是什麼。在每個場景後面都介紹了業務系統現在使用的解決方案以及主要的優缺點。

門店資訊校驗

場景

美團點評合併前的美團平臺事業部中,門店資訊入口作為門店資訊的第一道關卡,有一個很重要的職責,就是質量控制,其中第一步就是針對一些欄位的校驗規則。

下面從流程的角度看下門店資訊入口業務裡校驗門店資訊的規則模型(已簡化),如下圖。

規則主體包括3部分:

  • 分支條件。分支內邏輯條件為“==”和“<”。
  • 簡單計算規則。如:字串長度。
  • 業務定製計算規則。如:逆地址解析、經緯度反算等。

方案——硬編碼

由於歷史原因,門店資訊校驗採用了硬編碼的方式,虛擬碼如下:

if (StringUtil.isBlank(fieldA)
    || StringUtil.isBlank(fieldB)
    || StringUtil.isBlank(fieldC)
    || StringUtil.isBlank(fieldD)) {
    return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店引數缺少必填項");
}
if (fieldA.length() < 10) {
    return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店名稱長度不能少於10個字元");
}
if (!isConsistent(fieldB, fieldC, fieldD)) {
    return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店xxx地址、行政區和經緯度不一致");
}

優點

  • 當規則較少、變動不頻繁時,開發效率最高。
  • 穩定性較佳:語法級別錯誤不會出現,由編譯系統保證。

缺點

  • 規則迭代成本高:對規則的少量改動就需要走全流程(開發、測試、部署)。
  • 當存量規則較多時,可維護性差。
  • 規則開發和維護門檻高:規則對業務分析人員不可見。業務分析人員有規則變更需求後無法自助完成開發,需要由開發人員介入開發。

門店稽核流程

場景

流程控制中心(負責在執行時根據輸入引數選擇不同的流程節點從而構建一個流程例項)會根據輸入門店資訊中的渠道來源和品牌等特徵確定本次稽核(不)走哪些節點,其中選擇策略的模型如下圖。

規則主體是分支條件:

  • 分支條件主體是“==”,參與計算的引數是固定值和使用者輸入實體的屬性(比如:渠道來源和品牌型別)。

方案——開源Drools從入門到放棄

經過一系列調研團隊選擇基於開源規則引擎Drools來配置流程中稽核節點的選擇策略。使用Drools後的規則配置流程如下圖。

上圖中DSL即是規則主體,規則內容如下:

rule "1.1"
    when
        poi : POI( source == 1 && brandType == 1 )
    then
            System.out.println( "1.1 matched" );
            poi.setPassedNodes(1);

end

rule "1.2"
    when
        poi : POI( source == 1 && brandType == 2 )
    then
            System.out.println( "1.2 matched" );

end

rule "2.1"
    when
        poi : POI( source == 2 && brandType == 1 )
    then
            System.out.println( "2.1 matched" );
            poi.setPassedNodes(2);

end

rule "2.2"
    when
        poi : POI( source == 2 && brandType == 2 )
    then
            System.out.println( "2.2 matched" );
            poi.setPassedNodes(3);

end

在實踐中,我們發現Drools方案有以下幾個優缺點:

優點

  • 策略規則和執行邏輯解耦方便維護。

缺點

  • 業務分析師無法獨立完成規則配置:由於規則主體DSL是程式語言(支援Java, Groovy, Python),因此仍然需要開發工程師維護。
  • 規則規模變大以後也會變得不好維護,相對硬編碼的優勢便不復存在。
  • 規則的語法僅適合扁平的規則,對於巢狀條件語義(then裡巢狀when...then子句)的規則只能將條件進行笛卡爾積組合以後進行配置,不利於維護。

由於Drools的問題較多,最後這個方案還是放棄了。

績效指標計算

場景

美團外賣業務發展非常迅速,績效指標規則需要快速迭代才能緊跟業務發展步伐。績效考核頻率是一個月一次,因此績效規則的迭代頻率也是每月一次。因為績效規則系統是硬編碼實現,因此開發團隊需要投入大量的人力滿足規則更新需求。

2016年10月底我受績效團隊委託成立一個專案組,開發部署了一套績效指標配置系統,系統上線直接減少了產品經理和技術團隊70%的工作量。

下面我們首先分析下績效指標計算的規則模型,如下圖。

規則主體是結構化資料處理邏輯:

  • 規則邏輯是從若干資料來源獲取資料,然後進行一系列聚合處理(可以採用結構化查詢SQL語句+少量程式碼實現),最後輸出到目標資料來源。

方案——業務定製規則引擎

績效規則主體是資料處理,但我們認為資料處理同樣屬於規則的範疇,因此我們將其放在本文進行分析。

下圖是績效指標配置系統。觸發器負責定時驅動引擎進行計算;檢視負責給商業分析師提供規則配置介面,規則表達能力取決於檢視;引擎負責將配置的規則解析成Spark原語進行計算。

優點

  • 規則配置門檻低:檢視和引擎內部資料模型完全貼合績效業務模型,因此業務分析師很容易上手。
  • 系統支援規則熱部署。

缺點

  • 適用範圍有限:因為檢視和引擎的設計完全基於績效業務模型,因此很難低成本修改後推廣到別的業務。

探索全新設計

“案例”一節中三種落地方案的問題總結如下:

  • 硬編碼迭代成本高。
  • Drools維護門檻高。檢視對非技術人員不友好,即使對於技術人員來說維護成本也不比硬編碼低。
  • 績效定製引擎表達能力有限且擴充套件性差,無法推廣到別的業務。

由於“高效配置規則”是業務里長期存在的剛需,且行業內又缺乏符合需求的解決方案,2017年02月我在團隊內部設立了一個虛擬小組專門負責規則引擎的設計研發。引擎設計指標是要覆蓋工作中基礎的規則迭代需求(包括但不限於“案例”一節中的多個場景),同時針對“案例”一節中已有解決方案揚長避短。下面分3節來重現這個專案的設計過程。首先“需求模型”一節會基於“案例”一節的場景嘗試抽象出規則模型,同時提煉出系統設計大綱。然後“Maze框架”一節會基於需求模型設計一個規則引擎。最後“Maze框架能力模型”一節會介紹Maze框架的特點。

需求模型

對規則引擎來說,世界皆規則。通過“案例”一節的分析,我們對規則以及規則引擎該如何構建的思路正逐漸變得清晰,下面兩節分別定義規則資料模型和規則引擎的系統模型,目標是對“Maze框架”一節中的規則引擎產品進行框架性指導。

規則資料模型

規則本質是一個函式,由n個輸入、1個輸出和函式計算邏輯3部分組成。

y = f(x1, x2, …, xn)

具體結合“案例”一節中的場景我們梳理出的規則模型如下圖所示。

主要由三部分構成:

  • FACT物件:使用者輸入的事實物件,作為決策因子使用。
  • 規則:LHS(Left Hand Side)部分即條件分支邏輯。RHS(Right Hand Side)部分即執行邏輯。LHS和RHS部分是由一個或多個模式構成的。模式是規則內最小單位。模式的輸入引數可以是另一個模式或FACT物件(比如邏輯與運算[引數1] && [引數2]中引數1可以是另一個表示式)。模式需要支援以下3種類別:
    • 客戶定義方法:FACT物件的例項方法、靜態方法。
    • 常規表示式:邏輯運算、算數運算、關係運算、物件屬性處理等。
    • 結構化查詢。
  • 結果物件:規則處理完畢後的結果。需要支援自定義型別或者簡單型別(Integer、Long、Float、Double、Short、String、Boolean等)。

系統模型

我們需要設計一個系統能配置、載入、解釋執行上節中的資料模型,另外設計時還需要規避“案例”一節3個方案的缺點。最終我們定義瞭如下圖所示的系統模型。

主要由3個模組構成。

  • 知識庫:負責提供配置檢視和模式因子。知識庫之所以叫“知識”庫一個很重要的特徵是知識庫可以低成本擴充套件知識。知識擴充套件包括檢視和模式的新增,檢視和模式有一對一對映關係,比如我們在介面上展示一個如:大於小於等於一樣的檢視,則一定有一個模式$引數1 > $引數2與之對應。
    • 一方面降低操作門檻。
    • 一方面約束使用者輸入,保證輸入合法性。
    • 檢視:用於業務分析師等非技術背景的人員配置規則。作用兩方面:
    • 模式:構成規則的最小單位,不可拆分,可以直接被規則引擎執行。
  • 資源管理器:負責管理規則。
    • 版本管理:支援規則迭代更新、回滾和灰度等功能。
    • 依賴管理:負責將規則解析為模式樹。為了最大限度地增強規則的表達能力,每一個模式設計都很“原子”,這樣如果想配置一個完整語義的規則,則必須由多個子規則共同構成,因此規則之間會有樹形依賴關係。如$引數1 + $引數2 > $引數3這樣的規則便是由多個模式“複合”而成,則他的依賴關係如下所示。
             最終結果           /** 變數模式 */
                |
                |
              中間結果 > $引數3  /** 關係運算模式 */
                |
                |
         $引數1 + $引數2        /** 算數運算模式 */
  • 規則引擎:負責執行規則。
    • 排程器:根據規則的依賴關係以及硬體資源驅動模式執行器執行模式,目標是達到最大吞吐或最低延遲。
    • 模式執行器:負責直接執行模式。執行器可以根據業務的表達能力需求選擇基於Drools、Aviator等第三方引擎,甚至可以基於ANTLR定製。

Maze框架

基於"需求模型"一節的定義,我們開發了Maze框架(Maze是迷宮的意思,寓意:迷宮一樣複雜的規則)。

Maze框架分兩個引擎:MazeGO(策略引擎)和MazeQL(結構化資料處理引擎)。其中MazeGO內解析到結構化資料處理模式會呼叫SQLC驅動MazeQL完成計算(比如:從資料庫裡查詢某個BD的月交易額,如果交易額超過30萬則執行A邏輯否則執行B邏輯,這個語義的規則即需要執行結構化查詢),MazeQL內解析到策略計算模式會呼叫VectorC驅動MazeGO進行計算(比如:有一張訂單表,其中第一列是商品ID,第二列是商品購買數量,第三列是此商品的單價,我們需要計算每類商品的總價則需要對結構化查詢到的結果的每一行執行第二列 * 第三列這樣的策略模式計算)。

名詞解釋:

  • VectorC指向量計算,針對矩陣的行列進行計算。有三種計算方式:
    1. 針對一行的多列進行策略計算。
    2. 針對一列進行計算。
    3. 針對分組聚合(GroupBy)後的每一組內的列進行運算。
  • SQLC指結構化查詢。擁有執行SQL的能力。

MazeGO

MazeGO核心主要由3部分構成:資源管理器、知識庫和MazeGO引擎。另外兩個輔助模組是流量控制器和規則效果分析模組。基本構成如下圖。

3個核心模組(引擎、知識庫和資源管理器)的職責見“需求模型”一節中“系統模型”一節。下面只介紹下和“系統模型”不同的部分。

  1. MazeGO引擎:
    • 預載入規則例項。首先為了避免訪問規則時需要實時執行遠端呼叫而造成較大的時延,另外規則並不是時刻發生變更沒有必要每次訪問時拉取一次最新版本,基於以上兩個原因規則管理模組會在引擎初始化階段將有效版本的規則例項快取在本地並且監聽規則變更事件(監聽可以基於ZooKeeper實現)。
    • 預編譯規則例項。因為規則每次編譯執行會導致效能問題,因此會在引擎初始化和規則有變更這兩個時機將增量版本的規則預編譯成可執行程式碼。
    • 規則管理模組。職責如下:
  2. 流量控制器:負責不同版本規則的排程。方便業務方修改規則後,灰度部分流量到新規則。
  3. 規則效果分析:規則新增或修改後,業務方需要分析效果。本模組會提供:規則內部執行路徑、執行時引數和結果的映象資料,資料可以儲存在hbase上。

MazeQL

MazeQL核心主要由3部分構成:配置中心、MazeQL引擎和平臺。

  1. MazeQL引擎:
    • 排程器。SQLC和VectorC類規則大多由多個規則組合而成(對於SQLC而言可以將依賴的規則簡單的理解為子查詢),因此也需要和“系統模型”一節一樣的排程管理,實現層面完全一致。
    • QL驅動器。驅動平臺進行規則計算。因為任務的實際執行平臺有多種(會在下一個“平臺”部分介紹),因此QL驅動器也有多種實現。
    • 預載入規則例項。首先為了避免訪問規則時需要實時執行遠端呼叫而造成較大的時延,另外規則並不是時刻發生變更沒有必要每次訪問時拉取一次最新版本,基於以上兩個原因規則管理模組會在引擎初始化階段將有效版本的規則例項快取在本地並且監聽規則變更事件(監聽可以基於ZooKeeper實現)。
    • 預解析規則例項。因為規則每次解析執行會導致效能(大物件)問題,因此會在引擎初始化階段解析為執行時可用的排程棧幀。
    • 規則管理模組。職責如下:
    • 執行時模組。分為排程器和QL驅動器。
  2. 平臺:負責實際執行規則邏輯。分兩種執行模式:一種是以嵌入式方式執行在客戶端程序內部,好處是實時性更好,時延更低,適合小批量資料處理;另一種是以遠端方式執行在Spark平臺,適合離線大規模資料處理。
    • 嵌入式模式下是基於Mysql和Derby等實時性較好的資料庫實現的。
    • 在Spark平臺上是基於Spark SQL實現的。
    • QL執行器。負責執行結構化查詢邏輯。兩種不同的執行模式下QL執行器在執行SQL模式時會選擇兩種不同的QL執行器實現,兩種實現分別是:
  3. 配置中心:提供規則配置檢視。
    • 版本管理。同“系統模型”一節。
    • 資料來源繫結。即是定義參與計算的SQL邏輯中使用到的資料來源,便於系統進行管理。
    • 結構查詢定義。即是定義SQL規則,這是主體規則內容。
    • 向量計算定義。定義VectorC類計算(VectorC見“Maze框架”章節開頭的介紹)。

Maze框架能力模型

Maze框架是一個適用於非技術背景人員,支援複雜規則的配置和計算引擎。

規則迭代安全性

規則支援熱部署:系統通過版本控制,可以灰度一部分流量,增加上線信心。

規則表達能力

框架的表達能力覆蓋絕大部分程式碼表達能力。下面用虛擬碼的形式展示下Maze框架的規則部分具有的能力。

// 輸入N個FACT物件
function(Fact[] facts) {   
    // 從FACT物件裡提取模式     
    String xx= facts[0].xx;  
    // 從某個資料來源獲取特徵資料,SQLC資料處理能力遠超sql語言本身能力,SQLC具有程式設計+SQL的混合能力
    List<Fact> moreFacts = connection.executeQuery("select * from xxx where xx like '%" + xx + "%');  
    // 對特徵資料和FACT物件應用使用者自定義計算模式
    UserDefinedClass userDefinedObj = userDefinedFuntion(facts, moreFacts);  
    // 使用系統內建表示式模式處理特徵                      
    int compareResult = userDefinedObj.getFieldXX().compare(XX); 
    // 宣告使用者自定義物件         
    UserDefinedResultClass userDefinedResultObj = new UserDefinedResultClass();  
    // 使用系統內建條件語句模式處理特徵                              
    if (compareResult  == 0) {     
        userDefinedResultObj.setCompareResult(Boolean.FALSE);
    } else if (compareResult > 0) {
        userDefinedResultObj.setCompareResult(Boolean.FALSE);
    } else {
        userDefinedResultObj.setCompareResult(Boolean.TRUE);
    }
    // 將結果返回給客戶
    return userDefinedResultObj;        
}

規則執行效率

執行效率分三方面:

  1. 引擎的排程模組會確保吞吐優先,並且排程併發度等系統配置可以根據資源情況調整。
  2. 引擎執行過程中沒有遠端通訊開銷。
  3. 引擎執行程式碼實現編譯或解析後執行,執行效率較高。

規則接入成本

開發人員接入

  1. 首先,開發人員在專案工程裡匯入一個MazeGO jar包。
  2. 然後,開發人員在專案工程裡需要呼叫計算規則的地方引入MazeGO client(如下程式碼片段)。 // 初始化MazeGO client,建議在本應用程式的初始化階段執行 MazeGOReactor reactor = new MazeGOReactor(); reactor.setMazeIds(Arrays.asList(<mazeId>)); reactor.init(); // 呼叫MazeGO client執行規則 reactor.go(<mazeId>, <fact>); // 銷燬MazeGO client,建議在本應用程式的銷燬階段執行 reactor.destroy();

規則配置

規則配置基本實現由業務分析師、產品經理或運營人員自助完成。

業務分析師在MazeGO上配置規則的檢視如下圖所示。

總結

本文開頭介紹了幾個工作中的規則使用場景,順帶引出了多個不同的解決方案,最後介紹了Maze框架的設計,基本上展現了我們對這個框架思考和設計的整個過程。