Envoy源碼分析之Dispatcher

2020-12-06 阿里云云棲號

Dispatcher

在Envoy的代碼中Dispatcher是隨處可見的,可以說在Envoy中有著舉足輕重的地位,一個Dispatcher就是一個EventLoop,其承擔了任務隊列、網絡事件處理、定時器、信號處理等核心功能。

在Envoy threading model這篇文章所提到的EventLoop(Each worker thread runs a 「non-blocking」 event loop)指的就是這個Dispatcher對象。這個部分的代碼相對較獨立,和其他模塊耦合也比較少,但重要性卻不言而喻。下面是與Dispatcher相關的類圖,在接下來會對其中的關鍵概念進行介紹。

Dispatcher 和 Libevent

Dispatcher本質上就是一個EventLoop,Envoy並沒有重新實現,而是復用了Libevent中的event_base,在Libevent的基礎上進行了二次封裝並抽象出一些事件類,比如FileEvent、SignalEvent、Timer等。Libevent是一個C庫,而Envoy是C++,為了避免手動管理這些C結構的內存,Envoy通過繼承unique_ptr的方式重新封裝了這些libevent暴露出來的C結構。

通過CSmartPtr就可以將Libevent中的一些C數據結構的內存通過RAII機制自動管理起來,使用方式如下:

在Libevent中無論是定時器到期、收到信號、還是文件可讀寫等都是事件,統一使用event類型來表示,Envoy中則將event作為ImplBase的成員,然後讓所有的事件類型的對象都繼承ImplBase,從而實現了事件的抽象。

SignalEvent

SignalEvent的實現很簡單,通過evsignal_assign來初始化事件,然後通過evsignal_add添加事件使事件成為未決狀態(關於Libevent事件狀態見附錄)。

Timer

Timer事件暴露了兩個接口一個用於關閉Timer,另外一個則用於啟動Timer,需要傳遞一個時間來設置Timer的到期時間間隔。

創建Timer的時候會通過evtimer_assgin對event進行初始化,這個時候事件還處於未決狀態而不會觸發,需要通過event_add添加到Dispatcher中才能被觸發。

disableTimer被調用時其內部會調用event_del來刪除事件,使事件成為非未決狀態,enableTimer被調用時則間接調用event_add使事件成為未決狀態,這樣一旦超時時間到了就會觸發超時事件。

上面的代碼在計算timer時間timeval的時候實現的並不優雅,應該避免使用像1000000這樣的不具備可讀性的數字常量,社區中有人建議可以改成如下的形式。

FileEvent

socket套接字相關的事件被封裝為FileEvent,其上暴露了二個接口:activate用於主動觸發事件,典型的使用場景比如: 喚醒EventLoop、Write Buffer有數據,可以主動觸發下可寫事件(Envoy中的典型使用場景)等;setEnabled用於設置事件類型,將事件添加到EventLoop中使其成為未決狀態。

任務隊列

Dispatcher的內部有一個任務隊列,也會創建一個線程專們處理任務隊列中的任務。通過Dispatcher

的post方法可以將任務投遞到任務隊列中,交給Dispatcher內的線程去處理。

post方法將傳遞進來的callback所代表的任務,添加到post_callbacks_所代表的類型為vector的成員表變量中。如果post_callbacks_為空的話,說明背後的處理線程是處於非活動狀態,這時通過post_timer_設置一個超時時間時間為0的方式來喚醒它。post_timer_在構造的時候就已經設置好對應的callback為runPostCallbacks,對應代碼如下:

runPostCallbacks是一個while循環,每次都從post_callbacks_中取出一個callback所代表的任務去運行,直到post_callbacks_為空。每次運行runPostCallbacks都會確保所有的任務都執行完。顯然,在runPostCallbacks被線程執行的期間如果post進來了新的任務,那麼新任務直接追加到post_callbacks_尾部即可,而無需做喚醒線程這一動作。

DeferredDeletable

最後講一下Dispatcher中比較難理解也很重要的DeferredDeletable,它是一個空接口,所有要進行延遲析構的對象都要繼承自這個空接口。在Envoy的代碼中像下面這樣繼承自DeferredDeletable的類隨處可見。

那何為延遲析構呢?用在哪個場景呢?延遲析構指的是將析構的動作交由Dispatcher來完成,所以DeferredDeletable和Dispatcher密切相關。Dispatcher對象有一個vector保存了所有要延遲析構的對象。

to_delete_1_和to_delete_2_就是用來存放所有的要延遲析構的對象,這裡使用兩個vector存放,為什麼要這樣做呢?。current_to_delete_始終指向當前正要析構的對象列表,每次執行完析構後就交替指向另外一個對象列表,來回交替。

上面的代碼在執行對象析構的時候先使用to_delete來指向當前正要析構的對象列表,然後將current_to_delete_指向另外一個列表,這樣在添加延遲刪除的對象時,就可以做到安全的把對象添加到列表中了。因為deferredDelete和clearDeferredDeleteList都是在同一個線程中運行,所以current_to_delete_是一個普通的指針,可以安全的更改指針指向另外一個,而不用擔心有線程安全問題。

當有要進行延遲析構的對象時,調用deferredDelete即可,這個函數內部會通過current_to_delete_把對象放到要延遲析構的列表中,最後判斷下當前要延遲析構的列表大小是否是1,如果是1表明這是第一次添加延遲析構的對象,那麼就需要通過deferred_delete_timer_把背後的線程喚醒執行clearDeferredDeleteList函數。這樣做的原因是避免多次喚醒,因為有一種情況是線程已經喚醒了正在執行clearDeferredDeleteList,在這個過程中又有其他的對象需要析構而加入到vector中。

到此為止deferredDelete的實現原理就基本分析完了,可以看出它的實現和任務隊列的實現很類似,只不過一個是循環執行callback所代表的任務,另一個是對對象進行析構。最後我們來看一下deferredDelete的應用場景,卻「為何要進行延遲析構?」在Envoy的原始碼中經常會看到像下面這樣的代碼片段。

傳遞給Dispatcher的callback都是通過裸指針的方式進行回調,如果進行回調的時候對象已經析構了,就會出現野指針的問題,我相信C++水平還可以的同學都會看出這個問題,除非能在邏輯上保證Dispatcher的生命周期比所有對象都短,這樣就能保證在回調的時候對象肯定不會析構,但是這不可能成立的。

因為Dispatcher是EventLoop的核心。一個線程運行一個EventLoop直到線程結束,Dispatcher對象才會析構,這意味著Dispatcher對象的生命周期是最長的。所以從邏輯上沒辦法保證進行回調的時候對象沒有析構。可能有人會有疑問,對象在析構的時候把註冊的事件取消不就可以避免野指針的問題嗎?

那如果事件已經觸發了,callback正在等待運行呢? 又或者callback運行了一半呢?前者libevent是可以保證的,在調用event_del的時候可以把處於等待運行的事件取消掉,但是後者就無能為力了,這個時候如果對象析構了,那行為就是未定義了。沿著這個思路想一想,是不是只要保證對象析構的時候沒有callback正在運行就可以解決問題了呢?是的,只要保證所有在執行中的callback執行完了,再做對象析構就可以了。

可以利用Dispatcher是順序執行所有callback的特點,向Dispatcher中插入一個任務就是用來對象析構的,那麼當這個任務執行的時候是可以保證沒有其他任何callback在運行。通過這個方法就完美解決了這裡遇到的野指針問題了。

或許有人又會想,這裡是不是可以用shared_ptr和hared_from_this來解這個呢? 是的,這是解決多線程環境下對象析構的秘密武器,通過延長對象的生命周期,把對象的生命周期延長到和callback一樣,等callback執行完再進行析構,同樣可以達到效果,但是這帶來了兩個問題,第一就是對象生命周期被無限拉長,雖然延遲析構也拉長了生命周期,但是時間是可預期的,一旦EventLoop執行了clearDeferredDeleteList任務就會立刻被回收,而通過shared_ptr的方式其生命周期取決於callback何時運行,而callback何時運行這個是沒辦法保證的。

比如一個等待socket的可讀事件進行回調,如果對端一直不發送數據,那麼callback就一直不會被運行,對象就一直無法被析構,長時間累積會導致內存使用率上漲。第二就是在使用方式上侵入性較強,需要強制使用shared_ptr的方式創建對象。

總結

Dispatcher總的來說其實現還是比較簡單明了的,比較容易驗證其正確性,同樣功能也相對較弱,和chromium的MessageLoop、boost的asio都是相似的用途,但是功能上差得比較多。好在這是專門給Envoy設計的,而且Envoy的場景也比較單一,不必做成那麼通用的。另外一個我覺得比較奇怪的是。

為什麼在DeferredDeletable的實現中要用to_delete_1_和to_delete_2_兩個隊列交替來存放,其實按照我的理解一個隊列即可,因為clearDeferredDeleteList和deferredDelete是保證在同一個線程中執行的,就和Dispatcher的任務隊列一樣,用一個隊列保存所有要執行的任務,循環的執行即可。但是Envoy中沒有這樣做,我理解這樣設計的原因可能是因為相比於任務隊列來說延遲析構的重要性更低一些,大量對象的析構如果保存在一個隊列中循環的進行析構勢必會影響其他關鍵任務的執行,所以這裡拆分成兩個隊列,多個任務交替的執行,就好比把一個大任務拆分成了好幾個小任務順序來執行。

附錄

Libevent狀態轉換圖

相關焦點

  • Envoy - Envoy: 外交使節,特使-英語點津
    外電報導如下:Gunmen seized Egypt's top envoy to Iraq, officials and witnesses said Sunday, in an apparent bid to discourage the country's Arab neighbors from bolstering ties to the embattled U.S.
  • java任務調度之Timer定時器(案例和源碼分析)
    定時器相信大家都不陌生,平時使用定時器就像使用鬧鐘一樣,我們可以在固定的時間做某件事,也可以在固定的時間段重複做某件事,今天就來分析一下java中自帶的定時任務器Timer。對此就有必要深入其源碼看看了。二、Timer源碼分析對於一個類的源碼分析,我一貫的思路就是先從參數開始,然後構造方法,最後就是常用方法。下面我們就按照這個思路開始今天的源碼分析,在這裡基於jdk1.8。
  • Spring Boot自動裝配原理源碼分析
    011.環境準備使用IDEA Spring Initializr快速創建一個Spring Boot項目添加一個Controller類主配置類如下022.註解分析我們點開@SpringBootApplication註解慢慢分析(下面代碼中省略元註解)...
  • React源碼之組件的實現與首次渲染
    源碼中的兩種數據結構 貫穿源碼,常見的兩種數據結構,有助於快速閱讀源碼。 ReactElement ] }, }; } } ReactDOM.render( { type: App, props: {}, }, document.getElementById("root") ); ReactDOM.render 先來看下 ReactDOM.render 源碼的執行過程
  • 俯瞰Dubbo全局,閱讀源碼前必須掌握這些!!
    但是,為了更好的理解Dubbo,我將本文重點分成三個部分:Dubbo中的核心角色、搭建Dubbo源碼環境、Dubbo核心模塊說明、運行Dubbo的示例程序 四個部分。說幹就幹,上重點。註:本系列專題,我是基於Dubbo 2.7.8版本進行源碼分析的。
  • 源碼資本張宏江博士連續三年居全球頂級計算機科學家榜單中國大陸...
    源碼資本投資合伙人張宏江博士居中國大陸科學家之首,華人排名第5,全球排名第36。源碼資本投資合伙人 張宏江博士張宏江博士2011年底加入金山,任集團執行董事及執行長(CEO),兼任金山雲的執行長以及獵豹移動、迅雷及世紀互聯的董事,並於2016年12月從金山退休,之後加盟源碼資本任投資合伙人。
  • 超聲波測距原理(帶原理圖及源碼)
    STC11:STC單片機,處理邏輯06 源碼關鍵點分析源碼配套硬體:源碼流程圖:距離換算公式
  • 仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]
    ,效果可媲美微信 [附件下載]》《高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]》《Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]》《高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]》《分享java AMR音頻文件合併源碼,全網最全》《Android版高仿微信聊天界面源碼
  • 直播+電商碰撞起火花,電商直播系統源碼開發新模式
    一、電商直播系統源碼三種模式目前直播與電商結合的大趨勢,正在向三種模式發展。一是電商平臺增加直播功能,二是直播平臺通過商品連結倒流至第三方電商平臺,三是新型「直播+電商」模式平臺的出現。二、電商直播系統源碼開發的商業價值  
  • 雲原生時代的流量入口:Envoy Gateway
    Envoy 核心能力介紹Envoy 是一個為雲原生應用設計的開源邊緣與服務代理(ENVOY IS AN OPEN SOURCE EDGE AND SERVICE PROXY, DESIGNED FOR CLOUD-NATIVE APPLICATIONS,@envoyproxy.io),是雲原生計算基金會(CNCF)第三個畢業的項目,GitHub
  • 源碼資本王星石:消費升級大潮下,如何挖掘獨角獸? | 獵雲網
    源碼資本王星石認為,需求分化、此消彼長是消費升級的主要行為模式,「此消」、「彼長」兩個方向都有機會出現獨角獸。獨角獸來自三個象限,即大眾升級需求、大眾降級需求、小眾升級需求。未來大眾升級需求裡面的偉大公司,很可能隱藏在小眾升級需求市場裡面。文章轉自:源碼資本(ID:sourcecodecapital),作者:王星石。王星石:投資部副總裁,2014年加入源碼資本。
  • B 站後臺源碼疑似被上傳至 GitHub
    IT之家4月22日消息 今日有網友發現,B站(嗶哩嗶哩)的後臺源碼疑似被上傳至GitHub。目前B站方面尚未對此次事件進行回應,其真實性如何,是否影響到平臺用戶等問題尚未有答案。儘管官方尚未發聲,此庫在遭到曝光之後,已經受到了大量網友的關注。
  • ——源碼太陽能路燈
    以上就是關於高效太陽能板的特點是什麼的講解,源碼太陽能路燈採用高效單晶A級組件,轉化率大於19%,通過了權威檢測認證機構的測試與認證:德國TUV 和中 國質量認證中心CQC,歐盟CE,ROHS等認證。
  • 源碼張宏江博士連續三年居全球頂級計算機科學家榜單中國大陸第一
    源碼資本投資合伙人張宏江博士居中國大陸科學家之首,華人排名第5,全球排名第36。(CEO),兼任金山雲的執行長以及獵豹移動、迅雷及世紀互聯的董事,並於2016年12月從金山退休,之後加盟源碼資本任投資合伙人。
  • 源碼張宏江博士連續三年居全球頂級計算機科學家榜單中國大陸第一
    源碼資本投資合伙人張宏江博士居中國大陸科學家之首,華人排名第5,全球排名第36。源碼資本投資合伙人 張宏江博士張宏江博士2011年底加入金山,任集團執行董事及執行長(CEO),兼任金山雲的執行長以及獵豹移動、迅雷及世紀互聯的董事,並於2016年12月從金山退休,之後加盟源碼資本任投資合伙人。
  • 直播系統源碼開發:關於安卓開發工具和obs直播推流
    隨著移動網際網路技術的不算發展,直播系統源碼不再局限於娛樂直播的範疇尤其對於今年來說,購物直播行業的迅速發展,對直播系統源碼開發的需求進一步擴大,同時對直播源碼開發技術也有了新的要求。
  • 直播帶貨app源碼用Java語言來開發有哪些好處?
    而直播帶貨APP源碼的開發十分的重要,且在目前來看,最常用的還是Java語言,那麼相比較於其他語言開發,Java語言的直播帶貨系統有什麼優勢呢?下面就由小編為大家介紹吧。 一、源碼獨立性 Java開發直播帶貨APP源碼可以給企業自主搭建的權利,無需通過第三方平臺交易,不再依賴第三方平臺的流量。
  • 源碼雲:個人沒有編程經驗,如何快速製作小程序?望相互轉告
    所謂「只要思想不滑坡,辦法總比問題多」,今天源碼雲作者就和大家來分享一下如何快速製作小程序。首先,在做小程序之前,需要做好以下準備工作:首先。域名和伺服器準備。域名,最好是用自己的身份信息已經備案過的,域名備案在之前的已發布文章中有詳細地教程,可以參考著,也可以購買別人已經備案過的域名。
  • SpringSecurity 默認表單登錄頁展示流程源碼
    :f520875f-ea2b-4b5d-9b0c-f30c0c17b90b登錄成功並且瀏覽器又會重定向到剛剛訪問的接口2.springSecurityFilterchain 過濾器鏈如果你看過我另一篇關於SpringSecurity初始化源碼的博客