Envoy爲什麼能戰勝Ngnix——線程模型分析篇

導讀:隨着Service Mesh在最近一年的流行,Envoy 作爲其中很關鍵的組件,也開始被廣大技術人員熟悉。作者是Envoy的開發者之一,本文詳細說明了Envoy的線程模型,對於理解Envoy如何工作非常有幫助。內容較爲深入,建議細細品讀。

關於Envoy的基礎技術文檔目前相當少。爲了改善這一點,我正在計劃做一系列關於Envoy各個子系統的文章。 這是第一篇文章,請讓我知道你的想法以及你希望涵蓋的其他主題。最常見的問題之一是對Envoy使用的線程模型進行描述。
本文將介紹Envoy如何將連接映射到線程,以及Envoy內部使用的線程本地存儲(TLS)系統,正是因爲該系統的存在纔可以保證Envoy以高度並行的方式運行並且保證高性能。

 

線程概述

圖1:線程概述

Envoy使用三種不同類型的線程,如圖1所示。

  • Main:此線程可以啓動和關閉服務器。負責所有xDS API處理(包括DNS , 運行狀況檢查和常規集羣管理 ), 運行時 ,統計刷新,管理和一般進程管理(信號, 熱啓動等)。 在這個線程上發生的一切都是異步的和“非阻塞的”。通常,主線程負責所有不需要消耗大量CPU就可以完成的關鍵功能。 這可以保證大多數管理代碼都是以單線程運行的。

  • Worker:默認情況下,Envoy爲系統中的每個硬件線程生成一個工作線程。(可以通過--concurrency選項控制)。 每個Worker線程是一個“非阻塞”事件循環,負責監聽每個偵聽器,接受新連接,爲每個連接實例化過濾器棧,以及處理所有連接生命週期內IO事件。 這也允許大多數連接處理代碼以近似單線程運行。

  • 文件刷新器:Envoy寫入的每個文件(主要是訪問日誌)都有一個獨立的刷新線程。 這是因爲即使用O_NONBLOCK寫入文件系統有時也會阻塞。 當工作線程需要寫入文件時,數據實際上被移入內存緩衝區,最終通過文件刷新線程刷新至磁盤。 這是一個共享內存區域,理論上說所有Worker都可以在同一個鎖上阻塞,因爲他們可能會同時嘗試寫內存緩衝區。 這一部分內容將在後面進一步討論。

 

連接處理

如上所述,所有工作線程都會在沒有任何分片的情況下監聽所有偵聽器。內核將接收的socket分派給工作線程。 現代內核一般都很擅長幹這個; 內核使用諸如IO優先級提升之類的功能來嘗試提高線程處理能力而非使用其他線程處理,這些線程也在同一個套接字上偵聽,並且對每個連接來說不需要使用自旋鎖來處理。
一旦Worker接受了連接, 連接就永遠不會離開那個Worker。所有進一步的處理都在Worker線程內完成,其中包括轉發。 這就意味着:

  • Envoy中的所有連接池都和Worker線程綁定。 儘管HTTP/2連接池一次只與每個上游主機建立一個連接,但如果有四個Worker,則每個上游主機在穩定狀態下將有四個HTTP/2連接。

  • Envoy以這種方式工作的原因是將所有連接都在單個Worker線程中處理,這樣幾乎所有代碼都可以在無鎖的情況下編寫,就像它是單線程一樣。 這種設計使得大多數代碼更容易編寫,並且可以非常好地擴展到幾乎無限數量的Worker。

  • 主要的問題是,從內存和連接池效率的角度來看,調整--concurrency選項實際上非常重要。 擁有太多的Worker將浪費內存,創建更多空閒連接,並導致連接池命中率降低。 在Lyft,作爲邊車運行的Envoy併發度很低,性能大致與他們旁邊的服務相匹配。 但是我們以最大併發度運行邊緣節點Envoy。

 

非阻塞意味着什麼

到目前爲止,在討論主線程和Woker線程如何操作時,已經多次使用術語“非阻塞”。 所有代碼都是在假設沒有任何阻塞的情況下編寫的。 然而,這並不完全正確。 Envoy確實使用了一些進程範圍的鎖:

  • 如前所述,如果正在寫入訪問日誌,則所有Worker在訪問日誌緩衝區之前都會獲取相同的鎖。雖然鎖保持時間應該非常短,但是也可能會在高併發性和高吞吐量時發生爭用。

  • Envoy採用了一個非常複雜的系統來處理線程本地的統計數據。我會有後續文章討論這個話題。 這裏會簡要提一下,作爲線程本地統計處理的一部分,有時需要獲取對“stat store”的鎖。這種鎖不應該高度爭用。

  • 主線程需要定期與所有Worker線程同步數據。 這是通過從主線程“發佈”到Worker線程(有時從Worker線程返回到主線程)來完成的。 發佈需要獲取鎖,將發佈的消息放入隊列中以便後續操作。 這些鎖永遠不應該高度爭用,但它們仍然是阻塞的。

  • 當Envoy輸出日誌到標準錯誤時,它會獲得進程範圍的鎖。 一般來說,Envoy本地日誌性能也不好,所以我們沒有特意考慮提升改善鎖性能。

  • 還有一些其他隨機鎖,但它們都不在性能關鍵路徑中,永遠不應該爭用。

 

線程本地存儲

由於Envoy將主線程職責與Worker線程職責分開,因此需要在主線程上完成複雜的處理,然後以高度併發的方式讓每個Worker線程處理。 本節將介紹Envoy線程本地存儲(TLS)系統。 在下一節中,我將描述如何使用它來處理集羣管理。

圖2:線程本地存儲(TLS)系統

如已經描述的那樣,主線程基本上處理Envoy中的所有管理/控制面功能。(控制面在主線程似乎有點多,但在考慮到Worker做的工作時,似乎也是合適的)。 主線程執行某些操作是一種常見模式,然後通過Worker線程獲取結果,並且Worker線程不需要在每次訪問時獲取鎖。

Envoy的TLS系統的工作原理如下:

  • 在主線程上運行的代碼可以分配進程範圍的TLS槽。 這是一個允許O(1)訪問的向量索引。

  • 主線程可以將任意數據置入其槽中。 完成此操作後,數據將作爲循環事件發佈到每個Worker中。

  • Worker可以從其TLS槽讀取,並將檢索那裏可用的任何線程本地數據。

雖然非常簡單,但這是一個非常強大的範例,與RCU鎖的概念非常相似。(實質上,Worker線程在工作時從不會看到TLS槽中的數據發生任何變化。變化只發生在工作事件之間的靜止期)。

Envoy以兩種不同的方式使用它:

  • 在沒有任何鎖的情況下,每個Worker存儲不同的數據

  • 將共享指針存儲到每個Worker的全局只讀數據。因此每個Worker在工作時都無法操作數據的引用計數。 只有當所有Worker都停頓並加載新的共享數據時,舊數據纔會被銷燬。 這與RCU相同。

 

羣集更新線程

在本節中,我將描述TLS如何用於集羣管理。

羣集管理包括xDS API處理和/或DNS以及運行狀況檢查。

圖3:集羣管理器線程

圖3顯示了以下組件和步驟的總體流程:

  1. 集羣管理器是Envoy的內部組件,用於管理所有已知的上游集羣,CDS API,SDS/EDS API,DNS和運行狀況檢查。 它負責創建上游集羣的最終一致視圖,其中包括已發現的主機以及運行狀況。

  2. 運行狀況檢查器執行活動運行狀況檢查,並將運行狀況更改報告給集羣管理器。

  3. 執行CDS/SDS/EDS/DNS以確定羣集成員資格。 狀態更改將報告回集羣管理器。

  4. 每個工作線程都在不斷運行事件循環。

  5. 當集羣管理器確定集羣的狀態已更改時,它會創建集羣狀態的只讀快照 ,並將其發佈到每個Worker線程。

  6. 在下一個靜止期間,工作線程將更新分配的TLS槽中的快照。

  7. 在需要確定要負載均衡主機的IO事件期間,負載均衡器將在TLS插槽中查詢主機信息。 執行此操作不需要獲取鎖。 (另請注意,TLS還可以在更新時觸發事件,以便負載均衡器和其他組件可以重新計算緩存,數據結構等。這超出了本文的範圍,但在代碼中不同位置使用)。

通過前面描述的過程,Envoy能夠在處理請求的時候不需要任何鎖(除了之前描述的那些)。 除了TLS代碼之外,大多數代碼都不設計線程相關操作,可以編寫爲單線程程序。 除了達到出色的性能之外,這使得大多數代碼更容易編寫。

 

其他使用TLS的子系統

TLS和RCU在Envoy內廣泛使用。 其他一些例子包括:

  • 運行時(特徵標識)覆蓋查找:當前特徵標識覆蓋映射是在主線程上計算的。然後使用RCU語義向每個worker提供只讀快照。

  • 路由表交換:對於RDS提供的路由表,路由表在主線程上實例化。然後使用RCU語義爲每個worker提供只讀快照。 

  • HTTP日期header緩存:事實證明,對每個請求都計算HTTP日期header(當每個核執行~25K + RPS時)開銷很大。 Envoy大約每半秒計算一次日期header,並通過TLS和RCU將其提供給每個Worker。

還有其他例子,但前面的例子應該已經說明了TLS在Envoy內部如何廣泛使用。

 

已知的性能陷阱

雖然Envoy整體表現相當不錯,但是當它以非常高的併發性和吞吐量使用時,還是有一些需要注意的地方:

  • 正如本文中已經描述的那樣,當前所有Worker在寫入訪問日誌的內存緩衝區時都會獲得鎖。 在高併發性和高吞吐量的情況下,當寫入最終文件時,將需要對每個Worker的訪問日誌進行批處理。 作爲優化,每個Worker線程可以有自己的訪問日誌。

  • 儘管統計信息已經優化,但在非常高的併發性和吞吐量下,個別統計信息可能存在原子爭用。 對此的解決方案是使用Worker計數器,定期同步到中央計數器。 這將在後續文章中討論。

  • 如果Envoy用在少量連接佔用大量資源的情況下,現有的體系結構將無法正常工作。這是因爲無法保證連接在Worker之間均勻分佈。 這可以通過實現Worker連接負載均衡來解決,其中Worker能夠將連接轉發給另一個Worker進行處理。

 

結論

Envoy的線程模型旨在支持簡單編程範式和大規模並行,但如果調整不當可能會浪費內存和連接。該模型允許Envoy在非常高的Worker數量和吞吐量下有良好表現。
正如我在Twitter上提到的那樣,該設計也適合在DPDK之類的用戶空間網絡堆棧上運行,這可能讓商用服務器可以達到每秒鐘幾百萬請求處理速度。 看看未來幾年能做到什麼樣也是非常有趣的。
最後一點:我多次被問到爲什麼我們爲Envoy選擇C++。 原因是它仍然是唯一廣泛部署的生產級語言,在該語言中可以構建本文所述的體系結構。 C++當然不適合所有項目,甚至許多項目,但對於某些用例,它仍然是完成工作的唯一工具。
 

代碼鏈接

本文中討論的一些接口和頭文件的鏈接:

https://github.com/lyft/envoy/blob/master/include/envoy/thread_local/thread_local.h

https://github.com/lyft/envoy/blob/master/source/common/thread_local/thread_local_impl.h

https://github.com/lyft/envoy/blob/master/include/envoy/upstream/cluster_manager.h

https://github.com/lyft/envoy/blob/master/source/common/upstream/cluster_manager_impl.h

 

英文原文:

https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

更多 Envoy 介紹:

https://www.envoyproxy.io/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章