構(gòu)建強大且易用的規(guī)則引擎
作者 | 張寧
2016年07月恰逢美團點評的業(yè)務(wù)進入“下半場”,需要我們在各個環(huán)節(jié)優(yōu)化體驗、提升效率、降低成本。技術(shù)團隊需要怎么做來適應(yīng)這個變化?這個問題直接影響著之后的工作思路。
美團外賣的CRM業(yè)務(wù)步入成熟期,規(guī)則類需求幾乎撐起了這個業(yè)務(wù)所有需求的半邊天。一方面規(guī)則唯一不變的是“多變”,另一方面開發(fā)團隊對“規(guī)則開發(fā)”的感受是乏味、疲憊和缺乏技術(shù)含量。如何解決規(guī)則開發(fā)的效率問題,最大化解放開發(fā)團隊成為目前的一個KPI。
規(guī)則引擎作為常見的維護策略規(guī)則的框架很快進入我的思路。它能將業(yè)務(wù)決策邏輯從系統(tǒng)邏輯中抽離出來,使兩種邏輯可以獨立于彼此而變化,這樣可以明顯降低兩種邏輯的維護成本。
分析規(guī)則引擎如何設(shè)計正是本文的主題,過程中也簡單介紹了實現(xiàn)方案。
案例
首先回顧幾個美團點評的業(yè)務(wù)場景。通過這些場景大家能更好地理解什么是規(guī)則,規(guī)則的邊界是什么。在每個場景后面都介紹了業(yè)務(wù)系統(tǒng)現(xiàn)在使用的解決方案以及主要的優(yōu)缺點。
門店信息校驗
場景
美團點評合并前的美團平臺事業(yè)部中,門店信息入口作為門店信息的第一道關(guān)卡,有一個很重要的職責(zé),就是質(zhì)量控制,其中第一步就是針對一些字段的校驗規(guī)則。
下面從流程的角度看下門店信息入口業(yè)務(wù)里校驗門店信息的規(guī)則模型(已簡化),如下圖。

規(guī)則主體包括3部分:
分支條件。分支內(nèi)邏輯條件為“==”和“<”。
簡單計算規(guī)則。如:字符串長度。
業(yè)務(wù)定制計算規(guī)則。如:逆地址解析、經(jīng)緯度反算等。
方案——硬編碼
由于歷史原因,門店信息校驗采用了硬編碼的方式,偽代碼如下:
if (StringUtil.isBlank(fieldA)
? ?|| StringUtil.isBlank(fieldB)
? ?|| StringUtil.isBlank(fieldC)
? ?|| StringUtil.isBlank(fieldD)) {
? ?return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店參數(shù)缺少必填項");
}
if (fieldA.length() < 10) {
? ?return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店名稱長度不能少于10個字符");
}
if (!isConsistent(fieldB, fieldC, fieldD)) {
? ?return ResultDOFactory.createResultDO(Code.PARAM_ERROR, "門店xxx地址、行政區(qū)和經(jīng)緯度不一致");
}優(yōu)點
當(dāng)規(guī)則較少、變動不頻繁時,開發(fā)效率最高。
穩(wěn)定性較佳:語法級別錯誤不會出現(xiàn),由編譯系統(tǒng)保證。
缺點
規(guī)則迭代成本高:對規(guī)則的少量改動就需要走全流程(開發(fā)、測試、部署)。
當(dāng)存量規(guī)則較多時,可維護性差。
規(guī)則開發(fā)和維護門檻高:規(guī)則對業(yè)務(wù)分析人員不可見。業(yè)務(wù)分析人員有規(guī)則變更需求后無法自助完成開發(fā),需要由開發(fā)人員介入開發(fā)。
門店審核流程
場景
流程控制中心(負(fù)責(zé)在運行時根據(jù)輸入?yún)?shù)選擇不同的流程節(jié)點從而構(gòu)建一個流程實例)會根據(jù)輸入門店信息中的渠道來源和品牌等特征確定本次審核(不)走哪些節(jié)點,其中選擇策略的模型如下圖。

規(guī)則主體是分支條件:
分支條件主體是“==”,參與計算的參數(shù)是固定值和用戶輸入實體的屬性(比如:渠道來源和品牌類型)。
方案——開源Drools從入門到放棄
經(jīng)過一系列調(diào)研團隊選擇基于開源規(guī)則引擎Drools來配置流程中審核節(jié)點的選擇策略。使用Drools后的規(guī)則配置流程如下圖。

上圖中DSL即是規(guī)則主體,規(guī)則內(nèi)容如下:
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在實踐中,我們發(fā)現(xiàn)Drools方案有以下幾個優(yōu)缺點:
優(yōu)點
策略規(guī)則和執(zhí)行邏輯解耦方便維護。
缺點
業(yè)務(wù)分析師無法獨立完成規(guī)則配置:由于規(guī)則主體DSL是編程語言(支持Java, Groovy, Python),因此仍然需要開發(fā)工程師維護。
規(guī)則規(guī)模變大以后也會變得不好維護,相對硬編碼的優(yōu)勢便不復(fù)存在。
規(guī)則的語法僅適合扁平的規(guī)則,對于嵌套條件語義(then里嵌套when...then子句)的規(guī)則只能將條件進行笛卡爾積組合以后進行配置,不利于維護。
由于Drools的問題較多,最后這個方案還是放棄了。
績效指標(biāo)計算
場景
美團外賣業(yè)務(wù)發(fā)展非常迅速,績效指標(biāo)規(guī)則需要快速迭代才能緊跟業(yè)務(wù)發(fā)展步伐。績效考核頻率是一個月一次,因此績效規(guī)則的迭代頻率也是每月一次。因為績效規(guī)則系統(tǒng)是硬編碼實現(xiàn),因此開發(fā)團隊需要投入大量的人力滿足規(guī)則更新需求。
2016年10月底我受績效團隊委托成立一個項目組,開發(fā)部署了一套績效指標(biāo)配置系統(tǒng),系統(tǒng)上線直接減少了產(chǎn)品經(jīng)理和技術(shù)團隊70%的工作量。
下面我們首先分析下績效指標(biāo)計算的規(guī)則模型,如下圖。

規(guī)則主體是結(jié)構(gòu)化數(shù)據(jù)處理邏輯:
規(guī)則邏輯是從若干數(shù)據(jù)源獲取數(shù)據(jù),然后進行一系列聚合處理(可以采用結(jié)構(gòu)化查詢SQL語句+少量代碼實現(xiàn)),最后輸出到目標(biāo)數(shù)據(jù)源。
方案——業(yè)務(wù)定制規(guī)則引擎
績效規(guī)則主體是數(shù)據(jù)處理,但我們認(rèn)為數(shù)據(jù)處理同樣屬于規(guī)則的范疇,因此我們將其放在本文進行分析。
下圖是績效指標(biāo)配置系統(tǒng)。觸發(fā)器負(fù)責(zé)定時驅(qū)動引擎進行計算;視圖負(fù)責(zé)給商業(yè)分析師提供規(guī)則配置界面,規(guī)則表達能力取決于視圖;引擎負(fù)責(zé)將配置的規(guī)則解析成Spark原語進行計算。

優(yōu)點
規(guī)則配置門檻低:視圖和引擎內(nèi)部數(shù)據(jù)模型完全貼合績效業(yè)務(wù)模型,因此業(yè)務(wù)分析師很容易上手。
系統(tǒng)支持規(guī)則熱部署。
缺點
適用范圍有限:因為視圖和引擎的設(shè)計完全基于績效業(yè)務(wù)模型,因此很難低成本修改后推廣到別的業(yè)務(wù)。
探索全新設(shè)計
“案例”一節(jié)中三種落地方案的問題總結(jié)如下:
硬編碼迭代成本高。
Drools維護門檻高。視圖對非技術(shù)人員不友好,即使對于技術(shù)人員來說維護成本也不比硬編碼低。
績效定制引擎表達能力有限且擴展性差,無法推廣到別的業(yè)務(wù)。
由于“高效配置規(guī)則”是業(yè)務(wù)里長期存在的剛需,且行業(yè)內(nèi)又缺乏符合需求的解決方案,2017年02月我在團隊內(nèi)部設(shè)立了一個虛擬小組專門負(fù)責(zé)規(guī)則引擎的設(shè)計研發(fā)。引擎設(shè)計指標(biāo)是要覆蓋工作中基礎(chǔ)的規(guī)則迭代需求(包括但不限于“案例”一節(jié)中的多個場景),同時針對“案例”一節(jié)中已有解決方案揚長避短。下面分3節(jié)來重現(xiàn)這個項目的設(shè)計過程。首先“需求模型”一節(jié)會基于“案例”一節(jié)的場景嘗試抽象出規(guī)則模型,同時提煉出系統(tǒng)設(shè)計大綱。然后“Maze框架”一節(jié)會基于需求模型設(shè)計一個規(guī)則引擎。最后“Maze框架能力模型”一節(jié)會介紹Maze框架的特點。
需求模型
對規(guī)則引擎來說,世界皆規(guī)則。通過“案例”一節(jié)的分析,我們對規(guī)則以及規(guī)則引擎該如何構(gòu)建的思路正逐漸變得清晰,下面兩節(jié)分別定義規(guī)則數(shù)據(jù)模型和規(guī)則引擎的系統(tǒng)模型,目標(biāo)是對“Maze框架”一節(jié)中的規(guī)則引擎產(chǎn)品進行框架性指導(dǎo)。
規(guī)則數(shù)據(jù)模型
規(guī)則本質(zhì)是一個函數(shù),由n個輸入、1個輸出和函數(shù)計算邏輯3部分組成。
y = f(x1, x2, …, xn)
具體結(jié)合“案例”一節(jié)中的場景我們梳理出的規(guī)則模型如下圖所示。

主要由三部分構(gòu)成:
FACT對象:用戶輸入的事實對象,作為決策因子使用。
規(guī)則:LHS(Left Hand Side)部分即條件分支邏輯。RHS(Right Hand Side)部分即執(zhí)行邏輯。LHS和RHS部分是由一個或多個模式構(gòu)成的。模式是規(guī)則內(nèi)最小單位。模式的輸入?yún)?shù)可以是另一個模式或FACT對象(比如邏輯與運算[參數(shù)1] && [參數(shù)2]中參數(shù)1可以是另一個表達式)。模式需要支持以下3種類別:
客戶定義方法:FACT對象的實例方法、靜態(tài)方法。
常規(guī)表達式:邏輯運算、算數(shù)運算、關(guān)系運算、對象屬性處理等。
結(jié)構(gòu)化查詢。
結(jié)果對象:規(guī)則處理完畢后的結(jié)果。需要支持自定義類型或者簡單類型(Integer、Long、Float、Double、Short、String、Boolean等)。
系統(tǒng)模型
我們需要設(shè)計一個系統(tǒng)能配置、加載、解釋執(zhí)行上節(jié)中的數(shù)據(jù)模型,另外設(shè)計時還需要規(guī)避“案例”一節(jié)3個方案的缺點。最終我們定義了如下圖所示的系統(tǒng)模型。

主要由3個模塊構(gòu)成。
知識庫:負(fù)責(zé)提供配置視圖和模式因子。知識庫之所以叫“知識”庫一個很重要的特征是知識庫可以低成本擴展知識。知識擴展包括視圖和模式的添加,視圖和模式有一對一映射關(guān)系,比如我們在界面上展示一個如:一樣的視圖,則一定有一個模式$參數(shù)1 > $參數(shù)2與之對應(yīng)。
一方面降低操作門檻。
一方面約束用戶輸入,保證輸入合法性。
視圖:用于業(yè)務(wù)分析師等非技術(shù)背景的人員配置規(guī)則。作用兩方面:
模式:構(gòu)成規(guī)則的最小單位,不可拆分,可以直接被規(guī)則引擎執(zhí)行。
資源管理器:負(fù)責(zé)管理規(guī)則。
版本管理:支持規(guī)則迭代更新、回滾和灰度等功能。
依賴管理:負(fù)責(zé)將規(guī)則解析為模式樹。為了最大限度地增強規(guī)則的表達能力,每一個模式設(shè)計都很“原子”,這樣如果想配置一個完整語義的規(guī)則,則必須由多個子規(guī)則共同構(gòu)成,因此規(guī)則之間會有樹形依賴關(guān)系。如$參數(shù)1 + $參數(shù)2 > $參數(shù)3這樣的規(guī)則便是由多個模式“復(fù)合”而成,則他的依賴關(guān)系如下所示。
? ? ? ? ? ? 最終結(jié)果 ? ? ? ? ? /** 變量模式 */
? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ?|
? ? ? ? ? ? ?中間結(jié)果 > $參數(shù)3 ?/** 關(guān)系運算模式 */
? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ?|
? ? ? ? $參數(shù)1 + $參數(shù)2 ? ? ? ?/** 算數(shù)運算模式 */規(guī)則引擎:負(fù)責(zé)執(zhí)行規(guī)則。
調(diào)度器:根據(jù)規(guī)則的依賴關(guān)系以及硬件資源驅(qū)動模式執(zhí)行器執(zhí)行模式,目標(biāo)是達到最大吞吐或最低延遲。
模式執(zhí)行器:負(fù)責(zé)直接執(zhí)行模式。執(zhí)行器可以根據(jù)業(yè)務(wù)的表達能力需求選擇基于Drools、Aviator等第三方引擎,甚至可以基于ANTLR定制。
Maze框架
基于"需求模型"一節(jié)的定義,我們開發(fā)了Maze框架(Maze是迷宮的意思,寓意:迷宮一樣復(fù)雜的規(guī)則)。
Maze框架分兩個引擎:MazeGO(策略引擎)和MazeQL(結(jié)構(gòu)化數(shù)據(jù)處理引擎)。其中MazeGO內(nèi)解析到結(jié)構(gòu)化數(shù)據(jù)處理模式會調(diào)用SQLC驅(qū)動MazeQL完成計算(比如:從數(shù)據(jù)庫里查詢某個BD的月交易額,如果交易額超過30萬則執(zhí)行A邏輯否則執(zhí)行B邏輯,這個語義的規(guī)則即需要執(zhí)行結(jié)構(gòu)化查詢),MazeQL內(nèi)解析到策略計算模式會調(diào)用VectorC驅(qū)動MazeGO進行計算(比如:有一張訂單表,其中第一列是商品ID,第二列是商品購買數(shù)量,第三列是此商品的單價,我們需要計算每類商品的總價則需要對結(jié)構(gòu)化查詢到的結(jié)果的每一行執(zhí)行第二列?*?第三列這樣的策略模式計算)。

名詞解釋:
VectorC指向量計算,針對矩陣的行列進行計算。有三種計算方式:
針對一行的多列進行策略計算。
針對一列進行計算。
針對分組聚合(GroupBy)后的每一組內(nèi)的列進行運算。
SQLC指結(jié)構(gòu)化查詢。擁有執(zhí)行SQL的能力。
MazeGO
MazeGO核心主要由3部分構(gòu)成:資源管理器、知識庫和MazeGO引擎。另外兩個輔助模塊是流量控制器和規(guī)則效果分析模塊。基本構(gòu)成如下圖。

3個核心模塊(引擎、知識庫和資源管理器)的職責(zé)見“需求模型”一節(jié)中“系統(tǒng)模型”一節(jié)。下面只介紹下和“系統(tǒng)模型”不同的部分。
MazeGO引擎:
預(yù)加載規(guī)則實例。首先為了避免訪問規(guī)則時需要實時執(zhí)行遠程調(diào)用而造成較大的時延,另外規(guī)則并不是時刻發(fā)生變更沒有必要每次訪問時拉取一次最新版本,基于以上兩個原因規(guī)則管理模塊會在引擎初始化階段將有效版本的規(guī)則實例緩存在本地并且監(jiān)聽規(guī)則變更事件(監(jiān)聽可以基于ZooKeeper實現(xiàn))。
預(yù)編譯規(guī)則實例。因為規(guī)則每次編譯執(zhí)行會導(dǎo)致性能問題,因此會在引擎初始化和規(guī)則有變更這兩個時機將增量版本的規(guī)則預(yù)編譯成可執(zhí)行代碼。
規(guī)則管理模塊。職責(zé)如下:
流量控制器:負(fù)責(zé)不同版本規(guī)則的調(diào)度。方便業(yè)務(wù)方修改規(guī)則后,灰度部分流量到新規(guī)則。
規(guī)則效果分析:規(guī)則新增或修改后,業(yè)務(wù)方需要分析效果。本模塊會提供:規(guī)則內(nèi)部執(zhí)行路徑、運行時參數(shù)和結(jié)果的鏡像數(shù)據(jù),數(shù)據(jù)可以存儲在hbase上。
MazeQL
MazeQL核心主要由3部分構(gòu)成:配置中心、MazeQL引擎和平臺。

MazeQL引擎:
調(diào)度器。SQLC和VectorC類規(guī)則大多由多個規(guī)則組合而成(對于SQLC而言可以將依賴的規(guī)則簡單的理解為子查詢),因此也需要和“系統(tǒng)模型”一節(jié)一樣的調(diào)度管理,實現(xiàn)層面完全一致。
QL驅(qū)動器。驅(qū)動平臺進行規(guī)則計算。因為任務(wù)的實際執(zhí)行平臺有多種(會在下一個“平臺”部分介紹),因此QL驅(qū)動器也有多種實現(xiàn)。
預(yù)加載規(guī)則實例。首先為了避免訪問規(guī)則時需要實時執(zhí)行遠程調(diào)用而造成較大的時延,另外規(guī)則并不是時刻發(fā)生變更沒有必要每次訪問時拉取一次最新版本,基于以上兩個原因規(guī)則管理模塊會在引擎初始化階段將有效版本的規(guī)則實例緩存在本地并且監(jiān)聽規(guī)則變更事件(監(jiān)聽可以基于ZooKeeper實現(xiàn))。
預(yù)解析規(guī)則實例。因為規(guī)則每次解析執(zhí)行會導(dǎo)致性能(大對象)問題,因此會在引擎初始化階段解析為運行時可用的調(diào)度棧幀。
規(guī)則管理模塊。職責(zé)如下:
運行時模塊。分為調(diào)度器和QL驅(qū)動器。
平臺:負(fù)責(zé)實際執(zhí)行規(guī)則邏輯。分兩種運行模式:一種是以嵌入式方式運行在客戶端進程內(nèi)部,好處是實時性更好,時延更低,適合小批量數(shù)據(jù)處理;另一種是以遠程方式運行在Spark平臺,適合離線大規(guī)模數(shù)據(jù)處理。
嵌入式模式下是基于Mysql和Derby等實時性較好的數(shù)據(jù)庫實現(xiàn)的。
在Spark平臺上是基于Spark SQL實現(xiàn)的。
QL執(zhí)行器。負(fù)責(zé)執(zhí)行結(jié)構(gòu)化查詢邏輯。兩種不同的運行模式下QL執(zhí)行器在執(zhí)行SQL模式時會選擇兩種不同的QL執(zhí)行器實現(xiàn),兩種實現(xiàn)分別是:
配置中心:提供規(guī)則配置視圖。
版本管理。同“系統(tǒng)模型”一節(jié)。
數(shù)據(jù)源綁定。即是定義參與計算的SQL邏輯中使用到的數(shù)據(jù)源,便于系統(tǒng)進行管理。
結(jié)構(gòu)查詢定義。即是定義SQL規(guī)則,這是主體規(guī)則內(nèi)容。
向量計算定義。定義VectorC類計算(VectorC見“Maze框架”章節(jié)開頭的介紹)。
Maze框架能力模型
Maze框架是一個適用于非技術(shù)背景人員,支持復(fù)雜規(guī)則的配置和計算引擎。

規(guī)則迭代安全性
規(guī)則支持熱部署:系統(tǒng)通過版本控制,可以灰度一部分流量,增加上線信心。
規(guī)則表達能力
框架的表達能力覆蓋絕大部分代碼表達能力。下面用偽代碼的形式展示下Maze框架的規(guī)則部分具有的能力。
// 輸入N個FACT對象
function(Fact[] facts) { ?
? ?// 從FACT對象里提取模式 ? ?
? ?String xx= facts[0].xx; ?
? ?// 從某個數(shù)據(jù)源獲取特征數(shù)據(jù),SQLC數(shù)據(jù)處理能力遠超sql語言本身能力,SQLC具有編程+SQL的混合能力
? ?List moreFacts = connection.executeQuery("select * from xxx where xx like '%" + xx + "%'); ?
? ?// 對特征數(shù)據(jù)和FACT對象應(yīng)用用戶自定義計算模式
? ?UserDefinedClass userDefinedObj = userDefinedFuntion(facts, moreFacts); ?
? ?// 使用系統(tǒng)內(nèi)置表達式模式處理特征 ? ? ? ? ? ? ? ? ? ? ?
? ?int compareResult = userDefinedObj.getFieldXX().compare(XX);
? ?// 聲明用戶自定義對象 ? ? ? ?
? ?UserDefinedResultClass userDefinedResultObj = new UserDefinedResultClass(); ?
? ?// 使用系統(tǒng)內(nèi)置條件語句模式處理特征 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ?if (compareResult ?== 0) { ? ?
? ? ? ?userDefinedResultObj.setCompareResult(Boolean.FALSE);
? ?} else if (compareResult > 0) {
? ? ? ?userDefinedResultObj.setCompareResult(Boolean.FALSE);
? ?} else {
? ? ? ?userDefinedResultObj.setCompareResult(Boolean.TRUE);
? ?}
? ?// 將結(jié)果返回給客戶
? ?return userDefinedResultObj; ? ? ? ?
} 規(guī)則執(zhí)行效率
執(zhí)行效率分三方面:
引擎的調(diào)度模塊會確保吞吐優(yōu)先,并且調(diào)度并發(fā)度等系統(tǒng)配置可以根據(jù)資源情況調(diào)整。
引擎運行過程中沒有遠程通信開銷。
引擎執(zhí)行代碼實現(xiàn)編譯或解析后執(zhí)行,運行效率較高。
規(guī)則接入成本
開發(fā)人員接入
首先,開發(fā)人員在項目工程里導(dǎo)入一個MazeGO jar包。
然后,開發(fā)人員在項目工程里需要調(diào)用計算規(guī)則的地方引入MazeGO client(如下代碼片段)。
// 初始化MazeGO client,建議在本應(yīng)用程序的初始化階段執(zhí)行
MazeGOReactor reactor = new MazeGOReactor();
reactor.setMazeIds(Arrays.asList());
reactor.init();
// 調(diào)用MazeGO client執(zhí)行規(guī)則
reactor.go(, );
// 銷毀MazeGO client,建議在本應(yīng)用程序的銷毀階段執(zhí)行
reactor.destroy();
規(guī)則配置
規(guī)則配置基本實現(xiàn)由業(yè)務(wù)分析師、產(chǎn)品經(jīng)理或運營人員自助完成。

業(yè)務(wù)分析師在MazeGO上配置規(guī)則的視圖如下圖所示。

總結(jié)
本文開頭介紹了幾個工作中的規(guī)則使用場景,順帶引出了多個不同的解決方案,最后介紹了Maze框架的設(shè)計,基本上展現(xiàn)了我們對這個框架思考和設(shè)計的整個過程。
【墻裂推薦】
最近熱門內(nèi)容回顧? ?#技術(shù)人系列

下方二維碼關(guān)注我

互聯(lián)網(wǎng)草根,堅持分享技術(shù)、創(chuàng)業(yè)、產(chǎn)品等心得和總結(jié)~

點擊“閱讀原文”,領(lǐng)取 2020 年最新免費技術(shù)資料大全
