【翻譯】Envoy線程模型

文章翻譯: Envoy 線程模型 - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

我發現這篇文章非常有趣,並且由於 Envoy 最常用作“istio”的一部分或簡單地用作 kubernetes 的“入口控制器”,因此大多數人與它的直接交互方式與典型的交互方式不同。Nginx或Haproxy 安裝。 然而,如果有東西壞了,最好從內部了解它是如何運作的。 我嘗試將盡可能多的文本翻譯成俄語,包括特殊單字;對於那些覺得看這些很痛苦的人,我將原文留在了括號中。 歡迎來到貓。

Envoy 程式碼庫的低階技術文件目前相當稀疏。 為了解決這個問題,我計劃撰寫一系列有關 Envoy 各個子系統的部落格文章。 由於這是第一篇文章,請讓我知道您的想法以及您可能對以後的文章感興趣的內容。

我收到的有關 Envoy 的最常見技術問題之一是要求對其使用的線程模型進行低階描述。 在這篇文章中,我將描述 Envoy 如何將連接映射到線程,以及它內部使用的線程本地儲存系統,以使程式碼更加並行和高效能。

線程概述

【翻譯】Envoy線程模型

Envoy 使用三種不同類型的串流:

  • 主要的: 此執行緒控制進程的啟動和終止、XDS(xDiscovery Service)API 的所有處理,包括 DNS、健康檢查、常規叢集和運行時管理、統計重置、管理和常規進程管理 - Linux 訊號、熱重啟等。該線程中發生的事情是異步且“非阻塞”的。 一般來說,主執行緒協調所有不需要大量 CPU 運行的關鍵功能進程。 這使得大多數控製程式碼都可以像單線程一樣編寫。
  • 工人: 預設情況下,Envoy 會為系統中的每個硬體線程建立一個工作線程,這可以使用選項進行控制 --concurrency。 每個工作執行緒執行一個「非阻塞」事件循環,負責監聽每個監聽器;在撰寫本文時(29 年 2017 月 XNUMX 日),監聽器沒有分片,接受新連接,實例化一個過濾器堆疊連接,並在連接的生命週期內處理所有輸入/輸出(IO) 操作。 同樣,這允許大多數連接處理程式碼像單線程一樣編寫。
  • 文件刷新器: Envoy 寫入的每個檔案(主要是存取日誌)目前都有一個獨立的阻塞執行緒。 這是因為即使在使用時寫入文件系統緩存的文件 O_NONBLOCK 有時會被阻塞(嘆氣)。 當工作執行緒需要寫入檔案時,資料實際上會移動到記憶體中的緩衝區,最終透過執行緒刷新 文件重新整理。 從技術上講,這是一個程式碼區域,所有工作執行緒在嘗試填充記憶體緩衝區時都可以阻塞相同鎖。

連接處理

如同上面簡要討論的,所有工作執行緒都會偵聽所有偵聽器,而無需任何分片。 因此,核心用於優雅地將接受的套接字發送到工作線程。 現代核心通常非常擅長這一點,它們使用輸入/輸出(IO)優先權提升等功能來嘗試在開始使用也在同一套接字上偵聽的其他線程之前為線程填充工作,並且也不使用循環法鎖定(Spinlock)來處理每個請求。
一旦工作線程接受連接,它就永遠不會離開該線程。 連接的所有進一步處理完全在工作線程中處理,包括任何轉發行為。

這有幾個重要的後果:

  • Envoy 中的所有連線池都指派給一個工作執行緒。 因此,雖然 HTTP/2 連接池一次只與每個上游主機建立一個連接,但如果有四個工作線程,則每個上游主機在穩定狀態下將有四個 HTTP/2 連接。
  • Envoy 以這種方式運作的原因是,透過將所有內容保留在單一工作執行緒上,幾乎所有程式碼都可以在不阻塞的情況下編寫,就像單執行緒一樣。 這種設計使得編寫大量程式碼變得容易,並且可以很好地擴展到幾乎無限數量的工作執行緒。
  • 然而,主要的收穫之一是,從記憶體池和連接效率的角度來看,配置 --concurrency。 擁有過多的工作線程會浪費記憶體、創建更多空閒連接並降低連接池的速率。 在 Lyft,我們的 Envoy Sidecar 容器以非常低的並發性運行,因此效能與它們旁邊的服務大致相符。 我們僅在最大並發時將 Envoy 作為邊緣代理程式運行。

非阻塞是什麼意思?

到目前為止,在討論主線程和工作線程如何運作時,術語“非阻塞”已被多次使用。 所有程式碼都是在沒有任何內容被阻塞的假設下編寫的。 然而,這並不完全正確(什麼不完全正確?)。

Envoy 使用了幾個長進程鎖定:

  • 如前所述,在寫入存取日誌時,所有工作執行緒在記憶體日誌緩衝區被填滿之前都會取得相同的鎖定。 鎖的持有時間應該很低,但是在高並發、高吞吐量的情況下有可能發生鎖定競爭。
  • Envoy 使用非常複雜的系統來處理線程本地的統計資訊。 這將是另一篇文章的主題。 但是,我將簡要提及,作為本地處理線程統計資訊的一部分,有時需要獲取中央「統計資訊儲存」上的鎖。 永遠不需要這種鎖定。
  • 主執行緒需要定期與所有工作執行緒進行協調。 這是透過從主線程“發布”到工作線程,有時從工作線程“發布”回主線程來完成的。 發送需要鎖定,以便發布的訊息可以排隊等待稍後傳送。 這些鎖永遠不應該受到嚴重爭議,但從技術上講它們仍然可以被阻止。
  • 當 Envoy 將日誌寫入系統錯誤流(標準錯誤)時,它會取得整個進程的鎖定。 總的來說,從效能的角度來看,Envoy 的本地日誌記錄被認為很糟糕,因此沒有太多關注對其進行改進。
  • 還有一些其他隨機鎖,但它們都不是性能關鍵的,並且永遠不應該受到挑戰。

線程本地存儲

由於Envoy將主執行緒的職責與工作執行緒的職責分開的方式,因此要求能夠在主執行緒上完成複雜的處理,然後以高並發的方式提供給每個工作執行緒。 本節從較高層次描述了 Envoy 執行緒本地儲存 (TLS)。 在下一節中,我將描述如何使用它來管理叢集。
【翻譯】Envoy線程模型

如前所述,主執行緒實際上處理 Envoy 進程中的所有管理和控制平面功能。 控制平面在這裡有點過載,但是當您在 Envoy 進程本身中查看它並將其與工作線程所做的轉發進行比較時,它是有道理的。 一般規則是主執行緒進程執行一些工作,然後需要根據該工作的結果更新每個工作執行緒。 在這種情況下,工作線程不需要在每次訪問時獲取鎖.

Envoy 的 TLS(執行緒本地儲存)系統的工作原理如下:

  • 在主執行緒上執行的程式碼可以為整個行程分配一個 TLS 槽。 儘管這是抽象的,但實際上它是向量的索引,提供 O(1) 存取。
  • 主執行緒可以將任意資料安裝到其槽中。 完成此操作後,資料將作為正常事件循環事件發佈到每個工作執行緒。
  • 工作執行緒可以從其 TLS 插槽中讀取並檢索其中可用的任何執行緒本地資料。

雖然它是一個非常簡單且非常強大的範例,但它與 RCU(讀取-複製-更新)阻塞的概念非常相似。 本質上,工作運行時,工作執行緒永遠不會看到 TLS 槽中的任何資料變更。 變化僅發生在工作活動之間的休息期間。

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

  • 透過在每個工作執行緒上儲存不同的數據,可以無任何阻塞地存取數據。
  • 透過在每個工作執行緒上以唯讀模式維護指向全域資料的共用指標。 因此,每個工作執行緒都有一個資料引用計數,該計數在工作執行時無法遞減。 只有當所有工作人員冷靜下來並上傳新的共享資料時,舊資料才會被銷毀。 這與 RCU 相同。

叢集更新執行緒

在本節中,我將描述如何使用 TLS(線程本地儲存)來管理叢集。 叢集管理包括 xDS API 和/或 DNS 處理以及執行狀況檢查。
【翻譯】Envoy線程模型

叢集流管理包括以下元件和步驟:

  1. 群集管理器是Envoy 中的一個元件,用於管理所有已知的群集上游、群集發現服務(CDS) API、秘密發現服務(SDS) 和端點發現服務(EDS) API、DNS 以及主動外部檢查、健康檢查。 它負責建立每個上游叢集的「最終一致」視圖,其中包括發現的主機以及健康狀態。
  2. 健康檢查器執行主動健康檢查並向群集管理器報告健康狀態變化。
  3. 執行 CDS(叢集發現服務)/SDS(秘密發現服務)/EDS(端點發現服務)/DNS 以確定叢集成員資格。 狀態改變被傳回給群集管理器。
  4. 每個工作執行緒連續執行一個事件循環。
  5. 當群集管理器確定叢集的狀態已變更時,它會建立叢集狀態的新唯讀快照並將其傳送到每個工作執行緒。
  6. 在下一個安靜期間,工作執行緒將更新指派的 TLS 槽中的快照。
  7. 在確定要進行負載平衡的主機的 I/O 事件期間,負載平衡器將請求 TLS(執行緒本地儲存)插槽以取得有關主機的資訊。 這不需要鎖。 另請注意,TLS 還可以觸發更新事件,以便負載平衡器和其他元件可以重新計算快取、資料結構等。 這超出了本文的範圍,但在程式碼中的多個位置使用。

使用上述過程,Envoy 可以無任何阻塞地處理每個請求(除非前面描述過)。 除了 TLS 程式碼本身的複雜性之外,大多數程式碼不需要了解多執行緒如何運作,並且可以編寫單執行緒。 除了卓越的效能之外,這使得大多數程式碼更容易編寫。

使用 TLS 的其他子系統

Envoy 中廣泛使用了 TLS(執行緒本地儲存)和 RCU(讀取複製更新)。

使用範例:

  • 在執行期間更改功能的機制: 目前啟用的功能清單是在主執行緒中計算的。 然後,每個工作執行緒都會使用 RCU 語意獲得一個唯讀快照。
  • 替換路由表:對於RDS(路由發現服務)提供的路由表,路由表是在主執行緒上建立的。 隨後將使用 RCU(讀取複製更新)語義將只讀快照提供給每個工作執行緒。 這使得更改路由表的原子效率很高。
  • HTTP 標頭快取: 事實證明,計算每個請求的 HTTP 標頭(每個核心運行約 25K+ RPS)的成本相當昂貴。 Envoy 大約每半秒集中計算一次標頭,並透過 TLS 和 RCU 將其提供給每個工作人員。

還有其他情況,但前面的範例應該可以很好地理解 TLS 的用途。

已知的性能缺陷

雖然 Envoy 整體表現相當不錯,但在極高並發性和吞吐量的情況下使用時,有一些值得注意的地方需要注意:

  • 如本文所述,目前所有工作執行緒在寫入存取日誌記憶體緩衝區時都會取得鎖定。 在高並發和高吞吐量的情況下,您需要對每個工作執行緒的存取日誌進行批次處理,但在寫入最終檔案時會出現亂序交付。 或者,您可以為每個工作線程建立單獨的訪問日誌。
  • 儘管統計資料經過高度最佳化,但在非常高的並發性和吞吐量下,單一統計資料可能會出現原子爭用。 此問題的解決方案是為每個工作執行緒設定計數器,並定期重置中央計數器。 這將在後續帖子中討論。
  • 如果 Envoy 部署在連接很少且需要大量處理資源的場景中,則目前架構將無法正常運作。 無法保證連線將在工作執行緒之間均勻分佈。 這可以透過實現工作連接平衡來解決,這將允許工作線程之間的連接交換。

結論

Envoy 的線程模型旨在提供易於編程和大規模並行性的功能,但如果配置不正確,則可能會浪費記憶體和連接。 該模型使其能夠在非常高的線程數和吞吐量下表現良好。
正如我在Twitter 上簡要提到的,該設計還可以在完整的用戶模式網路堆疊(例如DPDK(資料平面開發套件))之上運行,這可以使傳統伺服器透過完整的L7 處理每秒處理數百萬個請求。 看看未來幾年會建造什麼將會非常有趣。
最後一點簡短的評論:我多次被問到為什麼我們為 Envoy 選擇 C++。 原因仍然是,它仍然是唯一可以建造本文中描述的架構的廣泛使用的工業級語言。 C++ 絕對不適合所有甚至許多項目,但對於某些用例,它仍然是完成工作的唯一工具。

程式碼連結

具有本文討論的介面和標頭實現的文件的連結:

來源: www.habr.com

添加評論