歡迎光臨
每天分享高質量文章

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語意向每個工作人員提供只讀快照。

  • 路由表交換:對於RDS提供的路由表,路由表在主執行緒上實體化。然後使用RCU語意為每個工作程式提供只讀快照。 

  • HTTP日期essay-header快取:事實證明,對每個請求都計算HTTP日期essay-header(當每個核執行~25K + RPS時)開銷很大。 Envoy大約每半秒計算一次日期essay-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/


    相關閱讀:


    阿裡雲故障,僅是運維操作失誤?

    微博開源的Motan RPC最新進展:新增跨語言及服務治理支持

    Java 微服務框架新選擇:Spring 5

    從單體應用走向微服務:一次API Gateway升級的啟示

    他們將生產環境從nginx遷移到envoy,原因竟然是……


    本文作者mattklein,由方圓翻譯,轉載本文請註明出處,技術原創及架構實踐文章,歡迎通過公眾號選單「聯繫我們」進行投稿。


    高可用架構

    改變互聯網的構建方式

    長按二維碼 關註「高可用架構」公眾號

    赞(0)

    分享創造快樂