貼吧低程式碼高效能規則引擎設計

語言: CN / TW / HK

作者 | 貼吧UEG技術組

導讀

本文首先介紹了規則引擎的使用場景,引出貼吧規則引擎。從元件、變數、規則、處置四個模組介紹了規則引擎的組成部分,同時對最終規則檔案的編譯過程做了詳細介紹。為了做到低程式碼,在規則配置上做到平臺化,非研發同學即可完成。增加新的變數也只需要在變數平臺進行簡單操作,無需額外的程式碼提交。另外框架層面支援並行和非同步的封裝,在服務呼叫上也儘量做到減少程式碼,提高研發同學的效率。最後文章對貼吧規則引擎做了總結,也提供了一些常見的使用場景和思路。

全文6951字,預計閱讀時間18分鐘。

01 背景

百度貼吧是一個擁有10多年曆史的UGC產品,在業務迭代中難免會有很多業務邏輯的程式碼,其中一部分業務邏輯用if-else等硬編碼的形式開發,一部分引入了配置檔案,通過配置檔案的規則去執行不同的業務邏輯。在某些運營活動或權益規則中,需要頻繁增加或者更改一些規則,這部分規則經常變動的部分就需要規則引擎來統一管理。

規則引擎是一種專注於業務規則的服務,它可以將業務規則從程式碼中剝離出來,使用預先定義好的語義規範來實現這些剝離出來的業務規則。規則引擎通過接受輸入的資料,進行業務規則的評估,並做出業務決策。

因為規則引擎將複雜的業務邏輯從業務程式碼中剝離出來,所以可以顯著降低業務邏輯實現難度;同時,剝離的業務規則使用規則引擎實現,這樣可以使多變的業務規則變的可維護,配合規則引擎提供的良好的業務規則設計器,不用編碼就可以快速實現複雜的業務規則,同樣,即使是完全不懂程式設計的運營或者產品人員,也可以使用圖形化的介面來自定義規則,實現程式碼一樣的效果。

下面對一些需要使用規則引擎的場景進行舉例:

1、單規則迭代

使用者標籤->包含A關鍵詞->權益A

使用者標籤->包含A關鍵詞->權益A

->包含B關鍵詞->權益B

使用者標籤->身份豁免策略/機器賬號->包含A關鍵詞->權益A

->包含B關鍵詞->權益B

使用者標籤->A模型結果大於1 ->豁C類使用者->包含A關鍵詞->權益C

可見隨著業務的發展,需要不斷的調整權益規則,這部分如果硬編碼寫死在程式碼中,需要頻繁上線,增加了工作量,並且隨著業務邏輯的增多,後期維護成本增高。

2、持續接入新的能力

除了目前的字串比較能力,一般的規則引擎還會接入各種各樣的模型能力,一般通過RPC的形式請求不同的服務,隨著接入的服務越來越多,可以組合的規則也是成倍的增長;

比如新接入圖片模型識別後,所有圖片識別的結果會過其他相關的模型,相關的模型呼叫邏輯就增加了一倍;

又如接入了某些模型,要根據模型的分數做相應的處理調整,需要頻繁的改動分值對應的處置手段,同時為了應對突發的場景,也需要頻繁的更改規則。

這些操作如果沒有一個自動化的規則引擎,就需要把大量的規則邏輯寫在程式碼裡,經過長時間的迭代,規則變得非常臃腫,無論對後續的開發還是定位問題的效率都會帶來問題。

02 貼吧規則引擎組成部分

貼吧規則引擎要做到規則靈活可配,無需研發介入,就需要儘可能的把包含判斷邏輯的部分全部下放到平臺,通過平臺的勾選對規則進行實現。

圖片

上圖為規則引擎整體的模組劃分,主要分為四部分:

  1. 元件服務:元件服務是對第三方服務的封裝,比如呼叫圖片模型服務、呼叫帖子屬性等內容服務,一般是RPC呼叫;元件需要RD開發程式碼,但是貼吧規則引擎的元件呼叫不摻雜業 務邏輯,僅僅是定義一個函式function,通過函式的入參呼叫第三方服務返回結果;

  2. 變數平臺:變數又稱運算元,是配置規則的引數;變數分為業務呼叫時傳的入參、使用元件返回的結果等。貼吧規則引擎通過專用平臺管理變數,RD和PM均可以在平臺上配置變數;

  3. 規則引擎:規則引擎平臺涉及到了具體每一條規則,通過圖形化的介面生成規則,該平臺不需要RD介入,通過平臺化的操作生成具體的規則。

  4. 處置方法:該處置為RD定製化開發,針對帖子、使用者或其他場景的召回處置處理。一般定義一個rpc請求回撥相關業務,處置方法因為是場景定製化的,所以這部分需要研發介入開發,但是處置方法更新的頻率非常低,一般都是複用已有的能力。

2.1 元件服務

規則引擎所有配置的資料不可能都是上游引數傳遞,很多是通過呼叫第三方服務獲取;比如通過帖子id獲取的帖子詳情資料,通過使用者uid獲取使用者的擴充套件屬性,這裡都需要呼叫第三方服務;

元件的開發非常簡單,只需要宣告一個函式,並實現其靜態方法。為了後續的效能考慮,函式宣告時可以指定sync(序列)、async(非同步)、parallel(並行)三種執行方式,貼吧規則引擎會在排程的時候按照型別,使用更高效的方式執行對應的方法。

圖片

圖中給出了一個demo元件,可以看出元件是不關注業務的,可以自定義入參和返回值,具體呼叫函式的入口及引數也不需要額外關注,更符合lib庫或者util方法的實現方式,這種元件的好處是開發簡單,解耦業務邏輯,增加元件的複用性,同時也降低了研發同學的工作量。

另外對於mode的工作模式,分為以下三類,具體的實現都是框架實現,元件的開發方不需要關注:

  1. sync:同步呼叫,使用的時候序列執行,函式間是阻塞的;

  2. async:非同步呼叫,定義function的時候分為before、after兩組方法。Before階段為發起rpc請求,等待第三方服務回撥後執行after方法,可以應對好耗時的服務接入。

  3. parallel:並行模式,屬於同一層級的parallel 函式並行執行,類似於多執行緒或者golang的goroutine模式,目前貼吧的規則引擎採用php開發,不具備多執行緒的相關能力,所以這裡並行是在before組裝rpc引數,通過curl_multi統一發起並行請求,在after函式取到結果。

以上能力在規則引擎框架上已經封裝,元件的研發RD只需要關注PRC的實現即可,根據函式的定義框架實現並行或者非同步的呼叫。

2.2 變數平臺

變數或者運算元就是規則引擎中做規則判斷使用的引數,比如使用者名稱、帖子id、使用者等級、帖子內容、模型識別的結果等,這部分的內容越多,規則引擎可以建立規則的『素材』越多;

變數的來源分為三個部分:

**(1)平臺預定義的變數:**比如一些常量數字或者特定字串,這部分內容比較固定,變動較少。

**(2)業務入參:**業務在請求規則引擎的時候可以把儘可能多的引數變數傳遞過來,除了帖子、使用者、吧相關的資料,還可以把使用者ip、ua等各種資料一併傳遞,這些資料在變數的平臺化介面上可以做簡單的篩選或者摘取,生成新的變數。

圖片

如圖舉例:input是一個完成的業務請求的變數,取input中的title生成testTitle變數,這樣就可以單獨使用testTitle做一些規則上的判斷。

**(3)元件呼叫:**在元件的部分已經定義了具體的方法,該方法類似於lib庫或者util,具體的請求的入口在變數平臺實現,入參就是其他變數。

圖片

如圖舉例:testRPC是定義的元件,其中入參是testTitle變數,返回的大結果作為一個testRPC變數。

後面可以對testRPC變數做具體的拆分,比如testRPC中的data.score作為一個單獨的變數,用score這個變數做後面的規則定義。

整體來說,input是上游傳過來的基礎變數。對入參變數的整理和過濾可以生成額外的基礎變數;使用基礎引數(比如帖子id),通過rpc呼叫,可以生成擴充套件的結果;對結果的提取可以生成額外的第二級變數;進一步對二級變數繼續呼叫服務,可以生成更多的變數:比如通過圖片模型結果過一些文字模型,但是隨著層級的變深,整體服務呈現多級依賴關係,這也增加了整體的系統耗時。

圖片

對於入參或者RPC請求結果的處理,全部可以在變數平臺上進行操作,變數平臺定義了規則引擎可以使用的變數及具體的實現方式。因為變數平臺支援編輯程式碼,所以一般的變數都可以直接在變數平臺編輯完成,而對相對複雜的模型呼叫,則可以封裝通用的util方法,之後在變數平臺直接使用這些方法。

2.3 規則引擎平臺

元件是一個個簡單的util靜態方法,通過入參及呼叫元件生成擴充套件的變數;自此進行規則判斷的『素材』準備好了,接下來需要使用這些變數配置規則,而核心的規則引擎平臺就是使用這些變數,生成規則。

規則引擎目前支援的運算規則:

  • 規則運算子:目前支援變數值與常量的比較,包含基本的>,<,>=,<=,==,!=多種比較方式;另外不僅可以直接使用變數,對於陣列型別的變數,還可以直接使用了變數的計數count,對於string型別,可以使用變數的長度len做引數直接進行判斷。

  • 詞表比較:判斷某個詞是否在詞表中是一個比較常用的規則;規則引擎平臺支援本地詞表與遠端詞表;遠端詞表為了解決詞表量級太大的問題。

    字元與詞表的比較包括精確匹配,包含、不包含、字首匹配及字尾匹配幾種方式,基本覆蓋了常見的使用方式。

  • 粒度控制判斷:為了判斷某個調整在一段時間內的出現次數,平臺支援配置變數的出現次數統計。

    對於某些使用頻次較少的運算規則,平臺不在功能上進行統一支援,但是可以通過修改變數來支援。比如想判斷變數A的sin值大於Z,可以在變數平臺新配置一個變數B,它的定位為sin(A),然後在規則引擎上使用B這個變數做判斷,就解決了某些特殊的計算方式。

    對於判斷邏輯,目前支援if條件判斷,switch多分支判斷,確定召回,確定豁免四種方式,基本囊括了常用的判斷邏輯。

圖片 △簡單的策略配置demo

2.4 處置方法

處置方法是針對不同的業務場景召回的個性化處置邏輯,這部分需要RD開發程式碼,做個性化的處理;比如命中召回後返回true or false或者命中的規則號或者回調特殊標記。

處置方法新增的頻率不會很高,基本固定對帖子、使用者或者各個場景有1-2個處置方法即可,後續的多個規則直接複用處置方法。

03 規則引擎實現原理

規則引擎最終生成是一個包含所有規則邏輯的程式碼塊,程式碼塊在規則引擎框架中執行;生成的程式碼塊類似研發開發的程式碼:程式碼的邏輯依舊是定義變數、使用變數做條件判斷(規則)、命中召回的處置。

1. 變數

這部分比較容易理解,就是2.2部分;將所有定義的變數取出來,當然因為變數之間是遞迴依賴的,所以當變數中需要其他變數時,會遞迴獲取內容,直到獲取常量或沒有依賴為止,最後倒序輸出為程式碼片段。

2. 規則檔案生成

每一條規則在儲存上都是一個json串,儲存形式為一個nodeTree。其中一個node節點儲存了型別:判斷節點、召回節點、豁免節點以及多組(switch)判斷節點。其中召回節點和豁免節點是程式判斷的終止位置,當執行到召回節點時會載入規則引擎對應的處置方法。判斷節點是整個規則引擎的核心,包含對應的變數與比較方式。其中比較方式有數字比較、字串比較及詞表比較。比如內容中是否包含關鍵詞“AB”,則在判斷節點上選取內容變數,比較方式為詞表包含,詞表內容為“AB”。

在規則檔案的設計上,採用nodeTree的方式,既能方便後續擴充套件node的屬性和型別,又通過父子節點樹的方式多層級的表示複雜的if、switch邏輯,層級可以無限深。

在新的規則上線時,將nodeTree檔案從資料庫中全部匯出,生成全部的規則檔案。規則檔案依賴的變數已經在變數檔案中全部定義好,剩下的工作就是將變數與規則進行組裝,生成最終的可執行程式碼。

另外對於某些特殊的需求,需要對白名單中的uid或者型別進行全部策略豁免。對於此類需求可以修改所有的規則,增加前置判斷邏輯,但是此操作需要對現有的全部規則及增量規則都修改,且在規則執行中會增加額外的判斷邏輯,增加整體規則引擎的執行耗時,所以除了普通的規則外,貼吧規則引擎增加了全域性規則區。全域性規則區相當於所有規則的前置條件,具體配置的規則為普通的node判斷節點,當全域性的所有規則判斷均為true時才會依次執行具體的普通規則,這樣對於想全域性豁免的需求,只需要簡單配置全域性規則即可,不需要修改具體的詳細規則。

3. 生成可執行規則檔案

規則引擎的前期編譯工作需要生成可以執行的程式碼,這部分就是將圖形化配置的規則與變數進行組合,優化整體的程式碼執行邏輯,生成可執行的程式碼,將檔案下發到所有的線上機器。

其中變數檔案是可以執行的php語法,規則為匯出的json檔案,需要將不同型別的檔案進行組合,這裡需要將不同檔案源轉為同一種結構化資料。

對於原本是php語法的檔案,貼吧規則引擎採用ply和yacc進行詞法和語法的解析,對php語法中array、函式、賦值、條件判斷、運算子等進行提取,轉為結構化的資料。

對於規則檔案,因為是預先定義好的json nodeTree,包含的格式是有限可列舉的,只要將每種型別與規則對映為結構化的欄位,就可以將規則檔案轉位目標結構化資料。

之後就是可執行檔案的生成過程,具體需要以下步驟:

圖片

語法樹展開:通過遞迴呼叫,將函式巢狀展開。比如res = funA(funB($params))展開為 tmp1 = funB($params);res = funA(tmp1);展開後將高階函式展開成普通函式,方便後續的優化處理。

接下來就是語句優化部分:

將不同變數重名的部分自動增加_n字尾,避免變數的相互覆蓋;遍歷整體規則中使用的變數,如果存在變數從未使用過,從整體程式碼中去除;對於定義了多遍重複的函式呼叫,整體去重只保留一份;對並行或者非同步方法的函式組拆分成真正可執行的靜態方法。經過以上步驟,對將要生成的最終規則檔案進行了初步的整理及優化。

在元件服務中提到了非同步函式async;對於某些耗時非常高的模型服務,非同步函式的作用是觸發呼叫後結束,等待第三方服務回撥。

對於使用非同步函式的情況,至少拆分成兩步,第一步發起觸發,第二步收到模型回撥,取到該步驟的結果作為變數,所有依賴該變數的規則只能放到第二步執行。如果有函式依賴第二步的結果,則步驟會繼續增加,該函式取某變數的非同步結果,發起服務請求,第三步回撥收到結果;非同步函式展開的作用是將所有無依賴的非同步函式請求方法統一放在一起,並行請求,通過回撥觸發執行第二步的規則邏輯。這樣貼吧規則引擎可以很方便的接入高耗時的模型服務。

除了非同步函式,還存在一種parallel並行呼叫的方式。由於規則引擎採用php的語言選型,沒有其他語言方便的多執行緒或者協程呼叫方式,對於無依賴的函式不能支援並行呼叫,所以在規則引擎的設計上通過curl_multil並行rpc呼叫服務的形式來減少耗時。

目前比較耗時的函式一般是請求資料庫服務或者第三方服務,這裡將資料庫及第三方的呼叫全部封裝為http協議的形式,在策略檔案呼叫上通過before方法整理入參,通過類似curl_multil的方法並行呼叫服務,取到結果後執行各自函式的after方法,整理函式對應的變數,這樣就將無依賴關係的呼叫進行了並行處理,整體降低了耗時。並行函式合併就是框架層面做的自動化合並,規則引擎的研發同學只需要簡單定義before和after方法,編譯階段就會自動將所有無依賴的函式before方法執行,組裝rpc請求。如果某些函式在before階段依賴其他服務的結果,那麼這批函式將在第二次發起請求,即無任何依賴的函式先發起並行請求,依賴第一批結果的函式再發起第二次並行請求,以此類推,最大限度的使用並行呼叫的方式。

圖片

最終生成的可執行的檔案,基本的最小單元為name、func、param、cond四組欄位組成。如果cond判斷條件為真,則name通過函式和入引數執行對應方法,產出值;該值又是其他單元的條件變數或者函式入參,這樣由上到下依次執行,完成了所有規則的執行。

仍然以上述demo策略為例:

圖片

最終生成了四組單元,基本格式如下:

圖片

如圖所示,基於上述的規則,只需要四組基本單元,每一組通過函式計算結果,下一組的條件依賴結果的值,如果走到“召回”邏輯,則進行表示規則命中,返回對應的規則號及其處置方法,框架中根據處置方法執行對應的邏輯。

每一個規則都是上述基本單元組成,最終將nodeTree中的全部規則生成基本單元,檔案下發到所有執行的機器上,至此完成了規則檔案的產出與規則上線。

04 總結

貼吧規則引擎搭配圖形化的介面,非常方便非技術同學配置業務規則,將冗餘的業務邏輯全部託管在規則引擎平臺上,無需程式碼開發,即可上線或者修改規則。

另外框架層面將非同步、並行等複雜邏輯進行了封裝,研發同學呼叫新的模型只需要按照模版修改簡單的引數整理及返回資料整理,即可完成並行或者非同步的操作,減少規則引擎的執行耗時。對於變數結果的轉換,也可以通過變數管理平臺,在平臺上簡單的修改即可完成一些基本的整理邏輯,大大減少程式碼的開發量。

通過規則引擎,可以靈活配置運營活動中的抽獎規則、使用者身份權益配置、商品價格等包含複雜業務邏輯判斷的部分,將規則抽象出來,解放研發同學的人力,同時規則在平臺上可以方便查詢和定位,方便後續的維護。

——END——

推薦閱讀

淺談許可權系統在多利熊業務應用

分散式系統關鍵路徑延遲分析實踐

百度工程師教你玩轉設計模式(裝飾器模式)

百度工程師帶你體驗引擎中的nodejs

揭祕百度智慧測試在測試定位領域實踐

百度工程師帶你探祕C++記憶體管理(ptmalloc篇)