多線程、事件驅動與推薦引擎框架選型

  事件驅動編程是一種編程範式,這裏程序的執行流由外部事件來決定。它的特色是包含一個事件循環,當外部事件發生時使用回調機制來觸發相應的處理。多線程是另外一種經常使用編程範式,而且更容易理解。html

  高性能通用型C++網絡框架 Nebula 是基於事件驅動的多進程網絡框架(適用於即時通信、數據採集、實時計算、消息推送等應用場景),已有即時通信、埋點數據採集及實時分析的生產應用案例。常常有人問Nebula的每一個進程裏是單線程仍是多線程的?又問爲何不用多線程?不用多線程又怎麼處理併發問題?nginx

  最近 Nebula 將會用於一個新的生產項目——推薦引擎,在此以前團隊已有使用某知名度較高的RPC框架多線程版推薦引擎(業界許多推薦引擎都用了目前比較知名的開源RPC框架來開發)。本文不作Nebula與各知名RPC框架的比較,也無心說明哪一個框架更適合作推薦引擎,只說明Nebula能夠用於推薦引擎,且有信心效果會很好。最終結果如何,等推薦引擎研發出來,拭目以待。git

  爲何是事件驅動而不是多線程?事件驅動無須多線程。咱們先來回顧一下服務器編程範式。程序員

1. 服務器程序設計範式

  《UNIX網絡編程》卷一里介紹了9種服務器設計範式:github

服務器設計範式圖

  九種服務器設計範式並非全都有實用價值,在《UNIX網絡編程》卷一最後一節裏給出了幾種TCP服務器設計範式代碼示例:web

  • TCP併發服務器程序,每一個客戶一個子進程
  • TCP預先派生子進程服務器程序
  • TCP預先派生子進程服務器程序,傳遞描述符
  • TCP併發服務器程序,每一個客戶一個線程
  • TCP預先建立線程服務器程序,每一個線程各自accept
  • TCP預先建立線程服務器程序,主線程統一accept

  Nginx採用的是九種服務器設計範式裏的第5種「預先派生子進程,使用互斥鎖上鎖方式保護accept」,Nebula採用的是九種服務器設計範式裏的第6種「預先派生子進程,由父進程向子進程傳遞套接字文件描述符」。redis

2. 單線程、多線程以及事件驅動編程模型比較

  一個典型的事件驅動的程序,就是一個死循環,並以一個線程的形式存在,這個死循環包括兩個部分,第一個部分是按照必定的條件接收並選擇一個要處理的事件,第二個部分就是事件的處理過程。程序的執行過程就是選擇事件和處理事件,而當沒有任何事件觸發時,程序會因查詢事件隊列失敗而進入睡眠狀態,從而釋放cpu。算法

  某種意義上說,服務端程序大可能是事件驅動的,或者說是IO請求事件驅動的。這裏比較的編程模型裏的事件驅動是指事件處理部分是異步的,即不只IO請求事件驅動,還有IO響應事件驅動,它的特色是當外部IO響應事件發生時使用回調機制來觸發相應的處理。編程

單線程、多線程、事件驅動比較圖

  在單線程同步模型中,任務按照順序執行。若是某個任務由於I/O而阻塞,其餘全部的任務都必須等待,直到它完成以後它們才能依次執行。這種明確的執行順序和串行化處理的行爲是很容易推斷得出的。若是任務之間並無互相依賴的關係,但仍然須要互相等待的話這就使得程序沒必要要的下降了運行速度。後端

  在多線程模型,每一個任務分別在獨立的線程中執行。這些線程由操做系統來管理,在多處理器系統上能夠並行處理,或者在單處理器系統上交錯執行。這使得當某個線程阻塞在某個資源的同時其餘線程得以繼續執行。與完成相似功能的同步程序相比,這種方式更有效率,但程序員必須寫代碼來保護共享資源,防止其被多個線程同時訪問。多線程程序更加難以推斷,由於這類程序不得不經過線程同步機制如鎖、可重入函數、線程局部存儲或者其餘機制來處理線程安全問題,若是實現不當就會致使出現微妙且使人痛不欲生的bug。另外一個問題,操做系統內核在切換線程的同時也要切換線程的上下文,當線程數量過多時,時間將會被耗用在上下文切換中。因此在大併發量時,多線程結構仍是沒法作到強大的伸縮性。

  在事件驅動版本的程序中,3個任務交錯執行,但仍然在一個單獨的線程控制中。當處理I/O或者其餘昂貴的操做時,註冊一個回調到事件循環中,而後當I/O操做完成時繼續執行。回調描述了該如何處理某個事件。事件循環輪詢全部的事件,當事件到來時將它們分配給等待處理事件的回調函數。這種方式讓程序儘量的得以執行而不須要用到額外的線程。當無IO操做時每一個任務佔用cpu的時間又比較少,進程就會處於空閒狀態。同等併發量狀況下,事件驅動佔用的系統資源會更好,負載足夠大時,事件驅動程序能夠將cpu利用到100%。事件驅動型程序比多線程程序更容易推斷出行爲,由於程序員不須要關心線程安全問題。

3. 事件驅動 != 只有一個線程

  事件驅動的一個很是有表明性的實現Node.js和redis,都是一個單進程(單線程)的服務(redis的數據落地或主從同步線程排除,其服務就是單線程的),事件處理都經過異步回調執行。第二節中單線程、多線程、事件驅動編程模型等相似比較中看起來事件驅動是單線程的,Node.js這一典型的事件驅動服務也是單線程的,致使許多人覺得事件驅動只能是單線程的,不能充分利用多CPU多核資源。其實否則,Nginx也是一個典型的事件驅動服務,而Nginx是多進程的。從邏輯上劃分後端服務,Nginx歸爲接入通訊層(openresty這種nginx+lua實現業務邏輯的不在討論範圍),Node.js歸爲業務邏輯層。接入通訊層的特色都是IO行爲幾乎不大消耗CPU是自然適合事件驅動的,也比較容易實現,而業務邏輯層的特色決定了事件驅動方式實現很是複雜,但這並意味着業務邏輯層的多線程事件驅動難以實現。

  Nebula就是一個多進程事件驅動服務的典型。事件驅動的每個進程都足夠高效,多個進程(多線程)又充分利用多CPU多核資源。Nebula的進程模型與Nginx類似,區別在於Nginx是各worker互斥鎖上鎖accept,而Nebula是由master進程accept後將鏈接對應的文件描述符傳送給worker進程(跟Memcached類似)。Nebula是從知足即時通信應用而開發的Starship框架發展而來的,與nginx的進程(線程)模型存在類似純屬偶然。爲何Nebula選擇傳送文件描述符而不是各worker進程搶accept?跟Nebula定位有關係,Nebula不只須要作接入通訊層、數據代理層,更要作業務邏輯層,分佈式服務的各層服務均可以且應該用Nebula實現,這意味着每個worker進程接近於分佈式服務的一個節點的功能,若是是worker搶佔式accept就沒法作定向路由。爲何選擇多進程而不是多線程?先看看多進程與多線程的優缺點比較:

  多進程:

  • 編程相對容易;一般不須要考慮鎖和同步資源的問題
  • 更強的容錯性:比起多線程的一個好處是一個進程崩潰了不會影響其餘進程
  • 有內核保證的隔離:數據和錯誤隔離
  • 進程切換開銷大

  多線程:

  • 建立速度快
  • 共享數據,多線程間能夠共享同一虛擬地址空間,多進程間的數據共享就須要用到共享內存、信號量等IPC技術
  • 較輕的上下文切換開銷
  • 一旦有一個線程掛掉,整個進程均可能會掛掉
  • 須要對共享資源的訪問進行同步

  多進程的前三點都是優勢,第四點是缺點。Nebula選擇多進程就不須要考慮鎖和同步資源問題,數據和錯誤隔離,worker進程崩潰不會影響整個節點服務,會被master進程迅速拉起。第四點缺點在Nebula不須要考慮,由於Nebula事件驅動的進程之間是不須要切換的,能夠近似地認爲每一個worker進程都是一個節點,節點與節點之間只有網絡通訊,不須要共享資源更不須要作切換。

4. 事件驅動適用場景

  對於IO密集型的業務,事件驅動比多線程同步的併發能力要高不少,能夠說不是一個數量級的。而大部分互聯網業務都屬於IO密集型業務,所以事件驅動的適用場景很是普遍。程序中有許多高度獨立的任務,在等待事件到來時,某些任務會阻塞,單個任務須要佔用較少CPU資源。

  Nebula 適用於即時通信、數據採集、實時計算、消息推送等應用場景,也適用於web後臺服務。Nebula已有即時通信、埋點數據採集及實時分析的生產應用案例,很快將有一個面向億級用戶的推薦引擎生產應用案例。

5. 推薦引擎框架選型

  說到推薦系統,首先被想到的多是基於內容、協同過濾、基於人口統計學、基於知識、基於社區、混合推薦等推薦技術。推薦技術的實施一般基於hadoop,用hive、spark、storm、flink等來實現。這些一般被稱爲推薦的數據挖掘部分。

  推薦引擎是推薦系統核心之一,負責將數據挖掘的結果按必定排序推送給用戶,這就是推薦引擎的主要功能。

  已知業界推薦引擎有使用C++開發也有使用Java開發,C++開發佔大多數。在Bwar瞭解到的C++開發的推薦引擎中多使用rpc框架,使用thrift的4個,使用brpc的2個,使用grpc的1個,使用tars的1個。因這些開源rpc框架不是專爲推薦引擎所開發的框架,開發人員一般會在這些框架之上再架設一層框架,而後纔是業務邏輯開發。Bwar接觸的一個推薦引擎就是基於brpc再開發了本身的框架而後才作業務邏輯開發,其開發難度比較大,且不容易擴展。也許是開發人員對這些開源rpc框架理解不夠深刻,致使業務邏輯開發比較複雜,對後續需求擴展不易。

  Nebula是Bwar開發的C++網絡框架,生而爲分佈式服務,通過兩個生產環境的應用。Nebula不是rpc框架而是一個基proactor(框架層實現proactor而非操做系統支持)事件驅動(回調)的框架。並不像大多數異步事件回調框架那樣開發者須要本身註冊回調函數,Nebula同時也是個IoC框架,經過actor類的巧妙設計實現下降了異步編程的複雜度,開發者真正意義上只需聚焦業務邏輯開發。

Actor類圖

  Nebula框架提供的Cmd類很是適合推薦服務的邏輯入口,支持動態加載,隨時不停機升級推薦算法推薦模型。Step類異步獲取redis等存儲中的數據,無阻塞等待讓cpu資源只用於推薦邏輯。session類用於緩存用戶、item、模型等數據。全部的數據獲取、傳遞都可經過session智能指針十分方便而高效地獲得。

  在那些基於rpc框架的推薦引擎中,許多開發人員提到了反射功能,而且經過大量宏以很費勁很難理解的方式實現了所謂的反射功能。這些都不是IoC框架,Bwar不理解爲何須要實現反射功能,若是用Nebula來作將是很是簡單的事,Nebula是IoC框架,全部的actor實例建立都是經過反射建立的,無須開發者作業務邏輯以外的任何事情。Nebula的反射實現很優雅,若是感興趣,能夠參考這篇文章《C++反射機制:可變參數模板實現C++反射》

  開發Nebula框架目的是致力於提供一種基於C++快速構建高性能的分佈式服務。若是以爲本文對你有用,別忘了到Nebula的 Github 或 碼雲 給個star,謝謝。

參考資料:

相關文章
相關標籤/搜索