微博推薦計算層解決方案:lab_common_so框架

jopen 8年前發布 | 36K 次閱讀 數據庫 推薦引擎

1 背景

在微博推薦的體系架構中,計算層是一個中間層,主要承擔排序的工作,是整體架構的重要的組成部分。計算層的核心是lab_common_so框架,它是一個C/C++語言編寫的高效計算服務。(注:關于微博推薦整體的體系架構描述,可參見“ 微博推薦引擎的體系架構 ”及“微博推薦架構的演進”)

微博推薦在早期的發展過程中,承接了大量的業務,在這段時期內,根據需求不斷總結和歸納,誕生了基于apache + mod_python的common_recom_framework(以下簡稱CRF),該框架具有如下特性:

  • 透明化的數據獲取:使用統一的函數獲取數據,而不用關心數據的存儲格式;
  • 統一的接口形態:所有業務的對外接口,形式上保持一致;
  • 規范化的業務流程:業務只需要按照固定的模板編寫即可,開發極為迅速;
  • 插件式的業務開發:業務的增加,修改都是熱插拔式,互不影響。

CRF的誕生,使得開發人員,不必為多個業務維護多套代碼,降低了維護成本。同時,開發人員只需要在框架之上做一些二次開發工作,極大地提高了開發效率。

隨著微博推薦業務的高速發展,業務的訪問量越來越大,業務邏輯變得越復雜,比如CTR預估模型的引入。我們發現CRF已經有點力不從心了,主要體現在以下三個方面:

  1. CTR預估模型會耗費較大的計算量,是CPU密集型的,而python在這方面的處理效率不那么讓人滿意,耗時很難降低;
  2. 預估模型整體的邏輯較為復雜,使得代碼比較臃腫,業務迭代效率大大降低;
  3. apache是以阻塞式的多進程的方式來工作的,當套接字的I/O阻塞時,進程會掛起,一旦連接數增加,apache不得不生成更多的進程來響應請求。而更多的進程,意味著進程間切換的開銷增加,也就是說,當業務訪問量較大時,apache處理不過來。

為解決上述問題,我們決定把CPU密集型的復雜計算抽取出來,使用高效的C++服務來完成,這樣就減輕了CRF的計算壓力。同時,這種做法也可以讓CRF更加專注于策略,代碼看起來更加清爽,迭代速度會更快,能夠解決問題(1)和問題(2)。于是,lab_common_so框架應運而生。

對于問題(3),經過一段時間的探索,我們演化出了recom_unite_front(以下簡稱RUF)框架。這是一個基于openresty開發的二次框架,它比CRF更為輕量,使用了異步非阻塞的事件模型,很好的解決了問題(3)。

下圖 1展示了CRF框架的發展。可以看到,CRF向上輕量化為RUF,具有更快的迭代速度,更強的并行處理的能力。CRF向下穩定化為lab_common_so,具有更高的計算效率,用于處理復雜的業務邏輯。

圖1 CRF框架的演進

注:關于RUF,之后會有相應的文章介紹,這里按下不表。

lab_common_so是CRF的發展和延續,因此它從一開始,就天然的具有CRF的幾個特性,并在其基礎之上,新增了如下特性:

  • 數據本地化:一些小規模的數據,可以加載到共享內存中,減少訪問網絡數據的開銷;
  • 非中斷式的資源更新:共享內存中的數據,可以實時更新,而不影響業務;
  • 可配置的算法支持:算法的調用,可通過配置文件來指定,避免修改代碼;

基于以上特性,lab_common_so的可以用于但不限于如下場景:

  • 高效的實時在線計算服務
  • 并發式的離線計算服務
  • 數據存儲代理算法模型訓練

接下來的章節,將詳細剖析lab_common_so的框架。

2 框架整體設計

本節介紹lab_common_so的整體設計以及框架處理流程。

2.1 框架整體構成

lab_common_so框架整體可以使用圖 2來描述。

圖2 lab_common_so框架整體構成

從上圖可以看到, lab_common_so框架由四部分組成,分別是業務部分,算法部分,全局數據部分,和遠程數據部分,每一部分都由一個配置文件指明該部分需要用到的屬性。對于整個服務,則有一個配置文件說明服務整體的基本參數。

關于各部分的設計細節,由后續章節“3 框架類設計”敘述。

2.2 服務啟動

圖 3展示了lab_common_so的啟動流程。

程序首先讀入一個配置文件,確定服務運行所需要的基本參數,包括日志級別,線程數目,套接字的相關屬性等。

然后程序根據讀入的基本參數,開始執行初始化。這個過程主要是將一些本地數據,加載到內存中。

第三步,創建指定數目的線程,并對每個線程進行初始化。線程的初始化工作包括db_company和work_company。

最后,綁定指定的服務端口,開始監聽。服務的端口有兩個:一個是查詢端口,服務通過監聽這個端口,來響應各業務請求。一個是控制端口,服務通過監聽這個端口,來響應命令類的請求,比如更新共享數據,更新業務等。

圖3 lab_common_so框架啟動流程

2.3 查詢服務處理

當服務端接收到查詢請求之后,開始處理。圖 4展示了查詢服務的處理流程。

第一步:服務端接受到請求。請求使用的是woo協議,這是我們內部研發的一個協議,見“3.1 woo協議”。

第二步:服務的某一個空閑線程,對接受到的請求進行處理。線程使用work_company對象的work_core函數,解析出請求中的參數。

第三步:根據上個步驟提取的參數,使用work_interface_factory對象,獲取業務處理類,然后調用業務處理類的work_core函數進行處理。

第四步:在業務處理類中,通過db_interface獲取遠程數據,通過global_db_interface獲取本地數據,通過algorithm_core函數執行算法。

第五步:將執行完的結果,拼裝成接口指定格式(一般使用json)返回。

圖片4.png

圖 4 查詢服務處理流程

2.4 控制服務處理

類似地,當服務端接收到控制請求后,開始處理。

圖 5以更新全局數據為例,展示了控制服務的處理流程。

第一步:服務端接受woo協議的請求。

第二步:服務端解析請求,以確定該如何響應。這里響應的是更新全局數據的命令。

第三步:服務端會新建一個global_db_company,并將新的數據加載入內存。

第四步:對于每個線程,使用新的global_db_company去初始化。

第五步:當所有線程都更新完畢后,交換新舊指針。

第六步,睡眠一段時間,以確保舊的資源無人使用。然后釋放舊指針指向的內存。

圖 5 控制服務處理流程

3 框架類設計

這一節將詳細講述lab_common_so框架的類設計。框架的UML類圖,如圖 6所示。

對于每個線程,包含一個WorkCompany類對象的指針和一個DbCompany類對象的指針,前者用于管理業務資源,后者用于管理數據資源。

因此,我們將可以簡單的將所有的類分為兩種類別:一種是業務類,包括“2.1 框架整體構成”中的業務部分和算法部分;一種是數據類,包括“2.1 框架整體構成”中的全局數據部分和遠程數據部分。

接下來,將對業務和算法這兩個類別,分述各類的設計。

圖片6.png

圖6 lab_common_so的UML類圖

3.1 業務類設計

業務類主要包含四個類,分別是WorkCompany,WorkInterfaceFactory,WorkInterface和AlgorithmInterface。分述如下:

3.1.1 WorkCompany

WorkCompany類中包含了WorkIntefaceFactory類對象的指針和work_core函數。這個類是業務類的最高層。

當服務的某個線程接受到請求以后,首先調用WorkCompany類的work_core函數,對傳入的請求參數進行解析,然后調用WorkCompany類的WorkIntefaceFactory類對象的指針,獲取指定的WorkInteface類對象進行業務處理。

3.1.2 WorkInterfaceFactory

顧名思義,這是一個工廠類,用于對多個WorkInterface進行管理。

該類包含的map,記錄了業務名稱與業務處理類的對應關系,這樣,我們就可以通過傳入參數中的指定字段,調用get_interface函數,獲取對應的業務處理類了。

業務名稱與業務處理類的對應關系,是通過配置文件work_config.ini指定的。當需要新增業務時,我們無需更新框架代碼,而是更新配置文件的即可。

3.1.3 WorkInterface

這是每個業務處理類的抽象基類。所有的業務處理類,都需要繼承該類,并實現該類的work_core函數。work_core函數是所有業務處理類的入口函數。

此外,在WorkInterface中,還維護了一個VEC_PAIR_MAP_ALG結構,記錄了每個算法處理類與算法名稱的對應關系,這一對應關系,也是通過配置文件指定的。在業務處理類中,可以根據這一對應關系,調用指定的算法處理函數。

3.1.4 AlgorithmInterface

這是每個算法處理類的抽象基類。所有的算法處理類,都需要繼承該類,并實現該類的algorithm_core函數。其設計和WorkInterface相似。

3.2 數據類設計

在lab_common_so中,db_company管理了兩類數據分。一類是靜態全局數據,這類數據被加載在共享內存中,所有的線程共享,其相關類包括GlobalDbCompany,GlobalDbInterfaceFactory和GlobalDbInterface。另一類是遠程數據,支持多種數據庫,比如redis,memcache等,其相關類包括DbInterfaceFactory和DbInterface。

3.2.1 db_company

每個db_company的對象,包含了一個global_db_company,同時還記錄了db_interface映射關系。

其中global_db_company管理了全局數據資源,db_interface映射關系記錄了db_id以及db_name和db_interface的對應關系,在訪問數據時,根據配置文件中的DB_ID字段或者DB_NAME字段,就可以獲取對應的db_interface類對象的指針。

3.2.2 GlobalDbCompany

該類包含了一個map,記錄了全局數據庫名稱和全局數據庫的對應關系,這個對應關系,同樣是通過配置文件指定的。load_config函數的作用即是讀取配置文件,生成對應關系。給定指定的數據庫名稱,就可通過get_global_db_interface獲取全局數據庫對象。

update_global_db_interface函數則是用于響應全局數據更新命令。

3.2.3 GlobalDbInterfaceFactory

該類只包含了一個函數,get_global_db_interface函數。這個函數和GlobalDbCompany中的同名函數的不同之處在于,該函數是根據指定的數據庫類型,構建全局數據庫對象。因此,該函數僅在GlobalDbCompany中的load_config函數中被調用。

3.2.4 GlobalDbInterface

該類是每個全局數據類的抽象基類。在此基礎上,可以派生出多種數據類型。

is_exist函數用于確定指定數據是否在數據庫中,load_db_config函數用于將讀取靜態數據,載入到內存中。

3.2.5 DbInterfaceFactory

該類的設計和GlobalDbInterfaceFactory一樣。

3.2.6 DbInterface

該類是遠程數據的抽象基類。在此基礎上,可以派生出多種數據存儲格式。

很多遠程數據庫如redis、memcache等,都支持批量獲取多個key的value值,因此我們將mget函數設計成為純虛函數,必須實現。

3.3 總結

可以看到,無論是業務類還是算法類,無論是全局數據類還是遠程數據類,我們的設計思路都是一致,都采用了工廠模式。

我們通過繼承的方式提供了統一的接口,這樣在使用之時,對數據的訪問是透明化的。

我們通過工廠模式,隱藏了類的創建細節,從而使得程序具有更高的擴展性。

4 框架基礎

本節將描述在lab_common_so框架中的一些基礎的定義。

4.1 woo協議

woo是一個輕型通訊框架,其通訊協議及日志系統比較完善。

woo的服務端由C/C++語言編寫,客戶端則提供了基于C/C++,PHP以及Python等多種語言版本。

woo協議采用了基于epoll的I/O通信模型,具有較好的I/O處理能力。還提供了對多線程的支持。

由于協議結構簡單,通信輕量化,在微博推薦內部,使用的較為廣泛。

4.2 GlobalDb類型

該類型用于支持全局靜態數據的載入及獲取。數據多以文件形式掛在本地磁盤,在框架啟動時加載入內存,全局唯一,所有線程共享。

GlobalDbInterface是所有全局靜態數據讀取類的基類,提供了一個is_exist通用函數接口用于查詢指定key是否在數據庫中。該類的某些派生類還提供了get_value和mget_value等特定函數接口。

目前支持如下類型:

  • __gnu_cxx::hash_set<uint64_t>
  • __gnu_cxx::hash_map<uint64_t, uint32_t>
  • MapDb(自研)

4.3 Db類型

該類型用于支持遠程數據的獲取。

DbInterface是所有遠程數據讀取類的基類,目前派生出了四種類型:

  • redis:提供對遠程redis數據庫的訪問
  • woo:提供對基于woo協議服務的訪問
  • openAPI:提供對http服務的訪問
  • MC:提供對遠程memcache數據庫的訪問

目前提供了四個通用函數接口:

  • get(uint64_t n_key):根據整型key訪問數據庫
  • s_get(char* p_str_key):根據字符串key訪問數據庫
  • mget(uint64_t n_keys[]):根據多個整型key訪問數據庫
  • s_mget(char* p_str_keys[]):根據多個字符串key訪問數據庫

此外,還提供了一個get_multi_db函數,可對多種數據請求,并行訪問不同數據庫。

4.4 Work類型

該類型是業務類,提供給用戶進行二次開發。

框架對業務執行了so化,使得業務可以快速上線迭代,并能讓服務支持多個業務。

我們定義了統一的接口格式,在基類work_interface中提供了統一的接口函數work_core。

對于輸入的請求串,必須是json格式的,例如{"api":"example", "cmd":"query", "body":"welcome!"}。其中api是必需的,以此來獲取指定的業務,并將請求中的其它參數傳遞給業務。對于輸出格式,沒有進行限制。

4.5 Algorithm類型

該類型是算法類,提供給用戶進行二次開發。

框架對算法執行了so化,使得算法庫可以迅速上線,并能讓多個業務使用多個算法庫。

我們定義了統一的接口格式,在基類algorithm_interface中提供了統一的接口函數algorithm_core。

4.6 服務類型

框架可編譯成兩個可執行文件,分別為lab_common_main和lab_common_svr。

lab_common_main程序是離線處理程序,執行完請求之后會退出。

lab_common_svr程序是在線服務程序,持續監聽端口,接收請求并處理。

目前在線程序提供兩種服務,一種是查詢服務,一種是控制服務。

5 lab_common_so框架的演進

早期的計算層框架,名為lab_common,是一個具有實驗性質的通用框架,它是lab_common_so框架的雛形。我們首先使用這個框架開發了幾個業務,性能超出預期。

在lab_common框架的使用過程中,我們發現這個框架有一個很大的缺點:業務邏輯的每一次變動,都需要重新編譯代碼,生成新的服務程序。發布到線上則需要重啟服務,期間所有的業務都會暫停。也就是說,lab_common框架并沒有將業務做到熱插拔,實際還是損失了CRF的特性。盡管計算層的業務趨于穩定,但是每一次改動都會導致服務中斷,這是不可接受的。

于是,我們對框架進行了升級。我們將每一個業務代碼,都編譯成so文件,框架通過讀取配置文件,打開指定業務的so文件。當我們修改一個業務,或者新增一個業務時,只需要單獨編譯業務代碼,將生成的新的so文件,發送到線上,然后發送更新配置文件的命令,即可實現動態加載。在整個更新過程中,服務無需終止,其它業務不受影響,可以繼續對外提供服務。這樣,我們就成功的使框架具備了熱插拔的特性。因為我們將業務so化了,所以,我們將框架的名稱修改為lab_common_so。

之后,lab_common_so框架由于其易用性與穩定性,開始在團隊內部被廣泛使用。隨著越來越多的業務使用該框架開發,我們逐漸發現了一些功能及細節上的問題。對此,我們一一做了擴充支持與修復,比如支持多線程獲取數據,支持多個算法調用,支持多種格式的數據獲取等等。

如今,已有十多個使用lab_common_so框架開發的業務,在線上穩定運行,每天流量數以億計。

從最早的lab_common框架誕生,到當前lab_common_so框架在線上穩定運行,已經過去了一年半的時間。微博推薦團隊的各個成員,都參與到了這個框架的設計與演進中。因此,當前的lab_common_so框架,是大家群策群力的結果。

當然,框架本身還有不足,比如:

(1)db_interface類可以寫的更抽象一些;

(2)HTTP訪問目前僅支持GET方式;

(3)db_interface類也可以像work_interface類一樣so化

(4)多線程獲取數據有一定的局限性

這些都是以后框架演進過程中需要解決的問題。

6 總結

本篇文章簡單地介紹了lab_common_so框架,主要從框架的起源,發展,設計方面進行描述,力圖讓讀者對本框架整體有一個初步的了解。

lab_common_so框架已經開源,具體的實現細節,可以從 https://github.com/wbrecom/lab_common_so 上獲取。開源項目中也包含了一份較為詳細的框架的使用說明文檔。

更多文章,請持續關注wbrecom博客: http://www.wbrecom.com/

來自: http://www.wbrecom.com/?p=634

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!