作者:閑魚技術-興往
背景
現在移動網絡越來越發達,移動生活越來越豐富,在使用者手機上可能同時存在數百種APP,這注定了使用者使用某一款APP的時間也将逐漸縮短。如果使用者在APP内僅浏覽了幾分鐘甚至幾十秒,那我們将很難為使用者提供更有價值的服務與資訊,大部分應用的做法是将最最熱銷的産品或最最火爆的活動放在應用的閃屏或是首焦上,對于閑魚這樣具有豐富業務生态的應用來說,顯然還不夠。
那如何在使用者駐留的短暫時間内為使用者提供更有價值的資訊呢,閑魚基于使用者的通路路徑,為使用者提供了更加豐富的優質資訊與服務。例如使用者釋出了商品,我們會推薦目前正在進行的包郵活動,使用者沒買到想要的商品,我們會及時給使用者推薦同款。
為了實作這樣的資訊提供模式,閑魚打造了一套流量調控系統。
是什麼
流量調控系統分為雲側事件處理和端側事件處理,雲側事件處理能中心化的處理所有能收集的事件,但由于處理量非常之大,在處理速度和資源消耗上有不小的壓力,而今天要介紹的,負責端側事件實時處理的架構,端側架構僅運作在單個手機APP上,能替雲側分擔資源的壓力,同時實時效率上進一步的提升。
閑魚實時流量調控系統的端側版本是一個合作項目,我們稱之為BehaviR,BehaviR架構在縱向流程上,涵蓋了使用者行為的實時處理,高品質的資料供給,強實時的資料傳輸,實時計算,決策與使用者觸達,在廣度上能支撐使用者在應用内的全場景,同時對典型場景會有定制的支援。而本篇介紹的實時處理架構即是其中的一部分,負責實時計算的閉環,讓我們看下BehaviR全貌:
為了完成實時計算的閉環,實時處理架構需要具備的能力:
在實時處理架構中,負責規則計算的部分即是端側計算引擎,同雲側計算引擎一樣,端側計算引擎的目的是也将擷取到的無終結狀态的資料進行有狀态的實時流式計算。但由于計算發生在端側,在一個輸入源相對有限、運算資源相對有限、有版本限制的運算環境下,要完成上述目的,它需要解決以下幾個問題:
- 如何在端側保證無終結狀态的資料持續供給
- 如何在端側進行有狀态的實時計算
- 如何将實時計算結果進行有效輸出
資料
在端側,資料的供給流程如圖:
資料的供給能力由BehaviR的資料子產品完成,業務方在APP内內建BehaviR,然後将頁面操作資料、網絡請求資料、存儲操作資料等有序灌給BehaviR,BehaviR會根據計算配置将待計算資料按需進行持久化和供給。
例如端上指定如下配置:
{
"actionTypeIn":[
"leave"
],
"sceneIn":[
"https://market.m.taobao.com/app/idleFish-F2e/idlefish-renting/home",
"https://market.wapa.taobao.com/app/idleFish-F2e/idlefish-renting/home"
],
"taskArray":[
{
"taskType":"py_backtrace",
"pythonName":"test_cep_rent_rule2",
"filter":[
{
"alias":"e1",
"actionType":"leave"
},
{
"alias":"e2",
"actionType":"pv"
}
]
}
]
}
我們會在"actionType"内配置觸發器,即BehaviR将會在"actionType"為"leave"的事件發生時,将擁有的資料提供給計算引擎。在計算發生前,計算所需要的資料類型是可預期的,是以BehaviR會根據"filter"内的配置,将資料過濾後傳輸給計算引擎。
在APP中運作計算任務前,每一個計算任務都将擁有一份這樣屬于自己的配置,來指定需要的計算資料類型。在BehaviR中根據端側所有計算任務的資料需求,可以擇優進行持久化來盡可能少的占用磁盤空間和記憶體空間。
計算
雲側實時計算能力已經成為了業界先驅,Flink、Siddhi、Spark等如數家珍,而端側并沒有計算架構先例,那我們在端側需要做的,就是深入了解雲側引擎,然後針對端側環境進行輕量化和定制化。結合我們已承接業務的經驗,将計算架構從功能層面劃分如下,并與雲側進行比較:
實時計算的視窗,可以對資料進行一定的預處理,例如按時間間隔每N秒處理一次,按數量每N個事件處理一次等。而在端上由于每個行為都有典型的特征,而大多數場景都對這些特征有着明确的預期,是以端上以Trigger的方式進行事件的處理,在Trigger上會描述我們需要開始計算的事件具有的屬性,當事件屬性與Trigger的屬性比對時,即開始計算。
(不确定有限)狀态機和共享緩沖區是計算處理的核心,其中共享緩沖區是對狀态計算中狀态與事件對應關系的存儲優化,我們在初版上也實作了其必要的組成部分。
在并發計劃和事件時序控制上,端上暫時通過單端事件的有序錄入,來保障絕大多數事件的時序,而并發的優化并不是初版的瓶頸所在,于是我們直接放棄了并發控制,在單線程上進行計算。
簡而言之,我們去掉了多流處理的設計,在容錯機制上弱化處理,但保留了核心的計算能力,同時将實時流處理上的概念進行融合,更加貼近我們的應用場景。關于計算部分的具體實作,後續将提供更詳細的介紹。
實作方案選型
當架構的概念與功能确定後,我們需要考慮使用什麼樣的方式去實作。于是将預備的實作方案從動态性、執行效率、開發效率等方面進行了比較:
由于在整個端側處理架構研發上,我們是首次嘗試落地,期望能快速驗證可行性,是以項目上線及運作過程會具備很高的不确定性及穩定性風險。綜上幾個因素,恰逢集團能擁有高效、穩定的Python運作時架構Walle,因而我們選擇在Python運作時架構下Walle,使用Python進行研發。
描述及編譯
在開始運算前,我們會有一套基于Python的應用程式設計接口來描述計算邏輯,當使用該接口編寫完成後,會在運作時編譯并建構計算圖執行個體,然後進行計算。
該程式設計接口的表達以“通路詳情然後離開”為例,我們可以描述為:
Pattern('e1') \
.where(KVCondition('actionType', 'pv')) \
.and(KVCondition('scene', 'item_detail')) \
.followby('e2') \
.where(KVCondition('actionType', 'leave')) \
.and(KVCondition('scene', 'item_detail'))
為了雲與端的體驗一緻性,我們也将支援從調控系統的标準DSL(該DSL已成為Blink支援的标準)到Python描述的轉換,上述代碼用DSL來描述,即:
EVENT: e1->e2
WHERE e1.extra_info.actionType = 'pv'
AND e1.extra_info.scene = 'item_detail'
AND e2.extra_info.actionType = 'leave'
AND e2.extra_info.scene = 'item_detail'
輸出
當計算完成後,我們會将計算結果進行輸出,在端側的輸出主要分為三個方面,一是業務權益觸達,二是算法模型輸入,三是另一次計算輸入。
由于在業務權益觸達形式上,不同的應用有着自己的展現形式和實作方式,是以我們僅保留公用的通訊協定來将計算結果進行格式化輸出。在閑魚端内,我們會介入協定的響應方,并接入閑魚的決策分發子產品進行統一管控。決策分發子產品将對單次政策的觸達效果進行管控,同時對同一使用者的所有政策進行調控。在閑魚應用内已經注入了豐富的觸達形式供決策子產品選擇。
端側的算法模型運作時,經常需要特定的行為作為模型運作的觸發器,那麼當計算結果作為算法模型輸入時,由于在集團内端側算法模型有統一的運作平台,因而我們需要将計算輸出與統一的模型運作架構對接,支援特定的計算結果自動喚醒對應模型的運作,然後将計算結果經過有選擇性的篩選後輸出給算法模型。
如果需要通過多個計算政策串聯計算後産出結果,那我們會提前約定好計算串聯前的資料規範,當另一次計算處理需要以目前計算結果作為輸入時,我們會在本次計算結果模拟為一次約定的特殊類型的行為資料并流入資料子產品,然後在另一次計算配置上添加該類型資料的監聽,即可順利執行。
流程回顧
當我們了解了端側複雜事件實時處理架構的各個部分後,可以用下圖再回顧下整個運作過程:
我們會在應用啟動的時候去同步本次啟動需要進行的計算任務配置,同時資料子產品會開始實時處理資料,當處理的資料與任務配置相比對時,即開始給計算架構供給資料,然後進行實時計算。計算完成後,會流轉決策分發子產品,由決策子產品決定計算結果的輸出方向,如果決策為觸達使用者,則會采用相關的觸達配置與形式進行觸達。
展望
我們目前在閑魚APP線上穩定運作了該實時處理架構的首個版本,雲側需要處理5秒左右,而現在全部流程可在毫秒級内完成,對伺服器資源零消耗。但在計算細節上,我們仍然有很多需要打磨的地方,例如端上程序退出帶來的計算終斷問題, 少量事件亂序問題,需要能承接更多複雜場景的聚合計算等。由此,我們将為了更高效的計算和更穩定的服務而努力。更加詳情的計算細節介紹,盡請期待!