【翻譯】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, проверку работоспособности (health checking), общее управление кластером и процессом работы сервиса (runtime), сбросом статистики, администрирование и общее управление процессами — Linux сигналы, горячий перезапуск (hot restart) и т. д. Все, что происходит в этом потоке, является асинхронным и «неблокирующим». В целом основной поток координирует все критические процессы функциональности, для выполнения которых не требуется большого количества ЦПУ. Это позволяет большую часть кода управления писать так, как если бы он был однопоточным.
  • 工人: 預設情況下,Envoy 為系統中的每個硬體線程建立一個工作線程,這可以透過選項進行控制 --concurrency。每個工作執行緒執行一個「非阻塞」事件循環,負責監聽每個監聽器(在撰寫本文時(29 年 2017 月 XNUMX 日)沒有監聽器分片)、接受新連接、為連接實例化過濾器堆疊,以及處理連接生命週期內的所有 IO。同樣,這允許大多數連接處理程式碼被編寫成好像是單線程的。
  • 文件沖洗器: Envoy 寫入的每個檔案(主要是存取日誌)目前都有一個獨立的鎖定執行緒。這是因為即使使用 O_NONBLOCK 有時它會被阻塞(嘆氣)。當工作執行緒需要寫入檔案時,資料實際上會移動到記憶體中的緩衝區,最終透過執行緒刷新。 文件重新整理。從技術上講,這是程式碼的一個區域,其中所有工作執行緒在嘗試填充記憶體緩衝區時都可以阻塞同一個鎖。

連接處理

正如上面簡要討論的那樣,所有工作線程都會監聽所有監聽器,沒有任何分段。這樣,核心就可以智慧地將接收到的套接字分派給工作執行緒。現代核心通常非常擅長這一點,使用 IO 增強等功能嘗試在偵聽同一套接字的其他線程開始使用之前用工作填充線程,而不是使用自旋鎖來處理每個請求。
一旦工作線程接受了連接,它就永遠不會離開該線程。連接的所有進一步處理都完全在工作線程中處理,包括任何轉發行為。

這有幾個重要的後果:

  • 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 和/或 DNS API 處理,以及健康檢查。
【翻譯】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 的子系統

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

使用範例:

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

還有其他情況,但前面的例子應該能讓你很好地理解 TLS 的用途。

已知的性能缺陷

雖然 Envoy 通常表現良好,但在使用非常高的並發性和吞吐量時,有幾個已知領域需要注意:

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

結論

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

程式碼連結

本文討論的介面和頭檔實現的連結:

來源: www.habr.com

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster