Envoy的線程模型[翻譯]

Envoy threading Model

關於envoy 代碼的底層文檔至關稀少。爲了解決這個問題我計劃編寫一系列文檔來描述各個子系統的工做。因爲是第一篇, 請讓我知道你但願其餘主題覆蓋哪些內容。html

一個我所瞭解到最共同的技術問題是關於envoy使用的線程模型的底層描述,這篇文章將會描述envoy如何使用線程來處理鏈接的(how envoy maps connections to threads), 同事也會描述Thread Local Storage(TLS)系統是如何使內部代碼平行且高效的。linux

Threading overview

img

Figure 1: Threading overviewgit

如同圖一所示,envoy使用了三中不一樣類型的線程。github

  • Main: 這個線程管理了服務器自己的啓動和終止, 全部xDS API的處理(包括DNS, 健康檢查(health checking) 和一般的集羣管理), running, 監控刷新(stat flushing), 管理界面 還有 通用的進程管理(signals, hot restart。在這個線程中發生的全部事情都是異步和非阻塞的(non blocking)。一般,主線程用來協調全部不須要大量cpu完成的關鍵功能。這容許大部分代碼編寫的如同單線程同樣。編程

  • Worker 在envoy系統中,默認爲每一個硬件(every harware)生成一個worker線程(能夠經過--concurrency參數控制),每一個工做線程運行一個無阻塞的事件循環(event loop),for listening on every listener (there is currently no listener sharding), accept 新的鏈接,爲每一個鏈接實例化過濾棧,以及處理這個鏈接生命週期的全部io。同時,也容許全部鏈接代碼編寫的如同單線程同樣。api

  • File flusher Envoy 寫入的每一個文件如今都有一個單獨的block的刷新線程。這是由於使用O_NONBLOCK在寫入文件系統有時也會阻塞。當Worker線程須要寫入文件時,這個數據實際上被移動至in-memory buffer中,最終會被File Flusher線程刷新至文件中。envoy的代碼會在一個worker試圖寫入memory buffer時鎖住全部worker。還有一些其餘的會在下面討論。緩存

鏈接處理

如上所述, 全部worker線程都會監聽端口而沒有任何分片,所以,內核智能的分配accept的套接字給worker進程。現代內核通常都很擅長這一點,在開始使用其餘監聽同一套接字的其餘線程以前,內核使用io 優先級提高等特性來分配線程的工做,一樣的還有不適用spin-lock來如理全部請求。服務器

一旦woker accept了一個鏈接,那麼這個鏈接永遠不會離開這個worker(一個鏈接只由同一worker進行處理), 全部關於鏈接的處理都將會在worker線程內進一步處理,包括任何轉發行爲。這有一些重要的含義:網絡

  • Envoy鏈接池中都是worker線程,所以經過http/2 鏈接池每次只與上游主機創建一個鏈接,若是有4個worker,那在穩定狀態將只有四個http/2鏈接與上游主機進行鏈接。數據結構

  • Envoy以這種方式工做的緣由在於這樣幾乎全部的代碼均可以如同單線程同樣以沒有鎖的方式進行編寫。這個設計使得大部分代碼易於編寫和擴展。

  • 從內存和鏈接池效率的角度來看,調整 - concurrency選項實際上很是重要。擁有比所須要的worker更多的worker將會浪費大量的內存,形成大量空置的鏈接,並致使較低的鏈接池命中率。在lyft,envoy以較低的concurerency運行,性能大體與他們旁邊的服務相匹配。

non-blocking意味着什麼

到目前爲止,在討論主線程和worker線程時咱們已經屢次使用術語'non-blocking'.幾乎全部代碼都是假設沒有阻塞的狀況下進行編寫。可是,這並不是徹底正確(哪裏有徹底正確的東西),Envoy確實使用了一些process wide locks.

  • 正如上述,若是在寫入訪問日誌, 全部worker都會得到一樣的鎖在將訪問日誌填充至內存緩存1前。鎖應當保持較短的時間,可是這個鎖有可能在高併發和高吞吐量時進行競爭。
  • Envoy使用了一個很是複雜的系統來處理線程本地的統計數據,這將是另外一個帖子的主題,我簡要的介紹一下,做爲線程本地統計處理的一部分,它有時會得到一個對於中央統計商店2的鎖, 這個鎖不該當被常常競爭。
  • 主線程按期須要協調全部worker線程。這是經過主線程發不到工做線程來完成的。發佈過程須要lock來鎖定消息隊列。這個鎖不該當被高度爭用,但從技術上能夠進行阻止。
  • 當envoy記錄日誌至stderr, 它將會得到process wide lock。 一般來講, envoy 本地日誌被認爲對於性能來講是糟糕的,因此沒有改善該過程的想法。
  • 還有一切其餘隨機鎖。但他們都不在影響性能的過程當中,也永遠不當被爭用。

線程局部變量3

因爲envoy將主線程的職責和worker線程的職責徹底分開,須要在主線程完成複雜的處理同時使每一個worker線程高度可用。本節將介紹Envoy的高級線程本地存儲(TLS)系統。在下一節中,我將描述如何使用它來處理集羣管理。

如已經描述的那樣,主線程基本上處理Envoy過程當中的全部管理/控制平面功能。(控制平面在這裏有點過載,但在特使程序自己考慮並與工人作的轉發進行比較時,彷佛是合適的)。主線程進程執行某些操做是一種常見模式,而後須要使用該工做的結果更新每一個工做線程,而且工做線程不須要在每次訪問時獲取鎖定

img

Envoy的TLS系統工做以下:

  • 運行在主線程的代碼分配了一個線程範圍的TLS插槽4,雖然是abstracted的,但實際上時容許o(1)訪問的索引
  • 主線程能夠設置任意數據在slot中,當它完成時,這個數據將會發送到全部worker做爲一個正常的事件循環
  • 工做線程能夠從TLS插槽中讀取獲取它所能獲取的局部數據。

雖然很是簡單,但它很是強大與只讀副本更新鎖相似.(實質上,worker線程在工做時從不會看到插槽的數據發生任何改變, 變化只發生在event切換的時候5), Envoy用兩種不一樣的方式來使用它:

  • 存儲不一樣的數據在每一個worker上,獲取時不須要任何鎖
  • 經過將指向只讀全局數據的共享指針存儲到每一個worker上,從而,每一個worker具備該數據的引用計數,該計數在工做時不會遞減。僅當全部worker查詢和讀取新的共享數據時會將原數據摧毀,這與RCU相同。

集羣更新線程

在本節中,我將描述TLS如何用於集羣管理。羣集管理包括xDS API處理和DNS以及運行情況檢查。

img

圖3顯示了涉及如下組件和步驟的整體流程:

  1. 集羣管理器(Cluster Manager)在envoy中管理已知上游的集羣,包括CDS API, SDS/EDS API DNS 和活躍的健康檢查。它負責建立每一個上游主機最終一致的視圖,上游主機包括了發現的主機和健康統計。
  2. 健康檢查器(Health Checker)進行活躍健康檢查,並將健康檢查的統計結果返回給集羣管理器(Cluster Manager).
  3. CDS/SDS/EDS/DNS 用來決定集羣的成員,一旦狀態改變會返回給集羣管理器(Cluster Manager).
  4. 每一個Worker 線程包含一個 Event Loop。
  5. 當集羣管理器要改變集羣的狀態時,它將建立一個集羣狀態只讀的快照(Snapshot), 並將它發送給每個worker 線程。
  6. 在下一個靜止期中,worker線程將更新存在TLS中的快照
  7. 在IO事件須要肯定主機如何負載均衡時,負載均衡器(Load Banlancer)將會查詢TLS中存有的集羣狀態信息,這個過程當中是無鎖的。(TLS也能夠觸發事件使得負載均衡器和其餘組件從新計算高速緩存和數據結構。但這超出了本文的範圍)

經過這些過程,Envoy可以處理每一個請求而不使用任何鎖。除了TLS代碼自己的複雜度以外,大部分代碼無需理解線程是如何具體工做的,使用單線程的方式便可。這使得大部分代碼易於修改且性能較好。

其餘使用TLS的子系統

TLS和RCU(Read-Copy Update6)在Envoy內普遍使用。
其餘一些例子包括:

  • Runtime (feature flag) overide lookup 運行時覆蓋查找:如今的feature flag overide map是在主線程進行計算的。爲每一個線程提供只讀的快照用來實現RCU。
  • Route table swapping 路由表交換:對於RDS提供的路由表,路由表在主線程上實例化。
    而後使用RCU語義爲每一個工做程序提供只讀快照.這使得路由表在原子級別上進行交換。
  • HTTP data header caching http數據頭緩存: 事實證實,在每一個請求上計算HTTP日期標題很是昂貴。Envoy大約每半秒計算一第二天期標題,並經過TLS和RCU將其提供給每一個worker線程。

還有其餘狀況,但前面的例子應該已經足夠闡釋TLS在envoy中的做用。

已知的性能陷阱

雖然Envoy總體表現至關不錯,可是當它以很是高的併發和高吞吐使用時,有一些已知的點須要注意:

  • 正如前文描述的那樣,如今當寫入日誌的內存緩衝區時,全部worker線程都會得到鎖,在高併發和吞吐使用時,若要寫入最終文件,就會被要求處理全部worker線程的訪問日誌,這將致使日誌的無序。
  • 儘管統計信息已經通過了不少的優化,在很是高的併發性和吞吐量下,個別統計數據仍然可能存在原子爭用。解決方案時每一個工人進行統計,並按期刷新到中央計數器。
  • 若是Envoy部署在須要大量資源來處理的少許鏈接的場景下,現有的體系結構將沒法正常工做。這是由於沒法保證鏈接在worker 線程之間均勻分佈。這能夠經過實現再平衡來解決,其中worker 線程可以將鏈接轉發給另外一個worker 線程進行處理。

總結

Envoy的線程模型旨在簡化編程和提升性能,但若是設置不當,可能會浪費內存和鏈接使用。Envoy的線程模型容許它在很是高的worker數量和高吞吐量下表現良好。

正如我在Twitter上簡略提到的那樣,該設計也適合在DPDK之類的完整用戶模式網絡堆棧上運行,這使得商用服務器在進行完整的L7處理時每秒處理數百萬個請求。觀察envoy再接下來幾年如何進展是一件很是有趣的事情

最後一個評論:我屢次被問到爲何咱們爲Envoy選擇C ++?
緣由是它仍然是惟一普遍部署的生產語言,在該語言中能夠構建本文所述的體系結構。C ++固然不適合全部項目,甚至許多項目,但對於某些用例,它仍然是完成工做的惟一工具。


  1. 原文 in-memory buffer

  2. central "stat store"

  3. Thread Local Storage(TLS)

  4. 原文 slot

  5. 原文 Change only happens during the quiescent period between work events

  6. 能夠查看IBM對RCU的介紹

相關文章
相關標籤/搜索