多執行緒、事件驅動與推薦引擎框架選型

鐵芒箕發表於2019-05-27

  事件驅動程式設計是一種程式設計正規化,這裡程式的執行流由外部事件來決定。它的特點是包含一個事件迴圈,當外部事件發生時使用回撥機制來觸發相應的處理。多執行緒是另一種常用程式設計正規化,並且更容易理解。

  高效能通用型C++網路框架 Nebula 是基於事件驅動的多程式網路框架(適用於即時通訊、資料採集、實時計算、訊息推送等應用場景),已有即時通訊、埋點資料採集及實時分析的生產應用案例。經常有人問Nebula的每個程式裡是單執行緒還是多執行緒的?又問為什麼不用多執行緒?不用多執行緒又怎麼處理併發問題?

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

  為什麼是事件驅動而不是多執行緒?事件驅動無須多執行緒。我們先來回顧一下伺服器程式設計正規化。

1. 伺服器程式設計正規化

  《UNIX網路程式設計》卷一里介紹了9種伺服器設計正規化:

伺服器設計正規化圖

  九種伺服器設計正規化並不是全都有實用價值,在《UNIX網路程式設計》卷一最後一節裡給出了幾種TCP伺服器設計正規化程式碼示例:

  • TCP併發伺服器程式,每個客戶一個子程式
  • TCP預先派生子程式伺服器程式
  • TCP預先派生子程式伺服器程式,傳遞描述符
  • TCP併發伺服器程式,每個客戶一個執行緒
  • TCP預先建立執行緒伺服器程式,每個執行緒各自accept
  • TCP預先建立執行緒伺服器程式,主執行緒統一accept

  Nginx採用的是九種伺服器設計正規化裡的第5種“預先派生子程式,使用互斥鎖上鎖方式保護accept”,Nebula採用的是九種伺服器設計正規化裡的第6種“預先派生子程式,由父程式向子程式傳遞套接字檔案描述符”。

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,謝謝。

參考資料:

相關文章