原文連接:https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310html
關於Envoy代碼庫的低級技術文檔目前至關稀少。 爲了糾正這個問題,我打算作一系列關於各類子系統的博客文章。 因爲這是第一篇文章,請讓我知道您的想法以及您但願瞭解的其餘主題。git
我獲得的關於Envoy的最多見技術問題之一是要求對其使用的線程模型進行低級描述。 這篇文章將介紹Envoy如何將鏈接映射到線程,以及內部使用的線程本地存儲(TLS)系統的描述,以使代碼極其平行且性能高。github
Envoy使用三種不一樣類型的線程,如圖1所示。編程
Main:此線程擁有服務器啓動和關閉,全部xDS API處理(包括DNS,運行情況檢查和常規集羣管理),運行時,統計刷新,管理和通常進程管理(信號,熱啓動等)。 在此線程上發生的全部事情都是異步的而且是「非阻塞的」。一般,主線程協調全部不須要大量CPU來完成的關鍵過程功能。 這容許將大多數管理代碼編寫爲單線程編寫。緩存
Worker:默認狀況下,Envoy爲系統中的每一個硬件線程生成一個工做線程。 (這能夠經過--concurrency 選項控制)。 每一個工做線程運行一個「非阻塞」事件循環,負責監聽每一個偵聽器(當前沒有偵聽器分片),接受新鏈接,爲鏈接實例化過濾器堆棧,以及處理全部IO的生命週期。 鏈接。 一樣,這容許將大多數鏈接處理代碼寫成好像是單線程的。服務器
文件刷新器:Envoy寫入的每一個文件(主要是訪問日誌)當前都有一個獨立的阻塞刷新線程。 這是由於即便使用O_NONBLOCK寫入文件系統緩存文件有時也會阻塞(嘆息)。 當工做線程須要寫入文件時,數據實際上被移入內存緩衝區,最終經過文件刷新線程刷新。 這是代碼的一個區域,技術上全部工做人員均可以阻止同一個鎖嘗試填充內存緩衝區。 還有一些其餘的將在下面進一步討論。網絡
鏈接處理數據結構
如上所述,全部工做線程都會在沒有任何分片的狀況下監聽全部偵聽器。 所以,內核用於智能地將接受的套接字分派給工做線程。 現代內核通常都很擅長這個; 他們使用諸如IO優先級提高之類的功能來嘗試填充線程的工做,而後開始使用同時監聽同一套接字的其餘線程,以及不使用單個自旋鎖來處理每一個接受。架構
一旦工人接受了鏈接,它就永遠不會離開那個工人。 全部進一步的鏈接處理都在工做線程內徹底處理,包括任何轉發行爲。 這有一些重要的含義:併發
Envoy中的全部鏈接池都是每一個工做線程。 所以,儘管HTTP / 2鏈接池一次只與每一個上游主機創建一個鏈接,但若是有四個工做站,則每一個上游主機在穩定狀態下將有四個HTTP / 2鏈接。
Envoy以這種方式工做的緣由是由於經過將全部代碼保存在單個工做線程中,幾乎全部代碼均可以在沒有鎖的狀況下編寫,就像它是單線程同樣。 這種設計使得大多數代碼更易於編寫,而且能夠很是好地擴展到幾乎無限數量的工做人員。
然而,一個主要的問題是,從內存和鏈接池效率的角度來看,調整 -- 併發選項實際上很是重要。 擁有比所需更多的工做人員將浪費內存,建立更多空閒鏈接,並致使更低的鏈接池命中率。 在Lyft,咱們的邊車Envoys以很是低的併發性運行,所以性能大體與他們旁邊的服務相匹配。 咱們只以最大併發性運行咱們的邊緣Envoys。
什麼是非阻塞
到目前爲止,在討論主線程和工做線程如何操做時,已經屢次使用術語「非阻塞」。 全部代碼都是在假設沒有任何阻塞的狀況下編寫的。 然而,這並不徹底正確(徹底是真的嗎?)。 特使確實採用了一些過程寬鎖:
如前所述,若是正在寫入訪問日誌,則全部工做程序在填充內存訪問日誌緩衝區以前都會獲取相同的鎖。 鎖定保持時間應該很是低,可是這種鎖能夠在高併發性和高吞吐量下競爭。
Envoy採用了一個很是複雜的系統來處理線程本地的統計數據。 這將是一個單獨的帖子的主題。 可是,我將簡要提一下,做爲線程本地統計處理的一部分,有時須要獲取對中央「stat store」的鎖定。這種鎖定不該該高度爭用。
主線程須要按期與全部工做線程協調。 這是經過從主線程「發佈」到工做線程(有時從工做線程返回到主線程)來完成的。 發佈須要鎖定,以便將發佈的消息放入隊列中以便之後發送。 這些鎖永遠不該該高度爭用,但它們仍然能夠在技術上阻止。
當Envoy將本身記錄到標準錯誤時,它會獲取進程範圍的鎖定。 通常來講,Envoy本地記錄被認爲是表現糟糕的,因此沒有多少考慮改善這一點。
還有一些其餘隨機鎖,但它們都不在性能關鍵路徑中,永遠不該該爭用。
線程本地存儲
因爲Envoy將主線程職責與工做線程職責分開,所以須要在主線程上完成複雜處理,而後以高度併發的方式使每一個工做線程可用。 本節介紹了Envoy的高級線程本地存儲(TLS)系統。 在下一節中,我將描述如何使用它來處理集羣管理。
如已經描述的那樣,主線程基本上處理Envoy過程當中的全部管理/控制平面功能。 (控制平面在這裏有點過載可是當在特使過程當中考慮並與工人作的轉發進行比較時,彷佛是合適的)。 主線程進程執行某些工做是一種常見模式,而後須要使用該工做的結果更新每一個工做線程,而且工做線程不須要在每次訪問時獲取鎖定。
Envoy的TLS系統的工做原理以下:
在主線程上運行的代碼能夠分配進程範圍的TLS槽。 雖然是抽象的,但實際上,這是一個容許O(1)訪問的向量索引。
主線程能夠將任意數據設置到其槽中。 完成此操做後,數據將做爲正常事件循環事件發佈到每一個工做程序中。
工做線程能夠從其TLS槽讀取,並將檢索那裏可用的任何線程本地數據。
雖然很是簡單,但這是一個很是強大的範例,與RCU鎖定概念很是類似。 (實質上,工做線程在工做時從不會看到TLS插槽中的數據發生任何變化。更改只發生在工做事件之間的靜止期間)。 特使以兩種不一樣的方式使用它:
經過在沒有任何鎖定的狀況下訪問每一個工做人員存儲不一樣的數據
經過將共享指針存儲到每一個worker的只讀全局數據。 所以,每一個工做者都具備對在工做時不能遞減的數據的引用計數。 只有當全部工做人員都已停頓並加載新的共享數據時,舊數據纔會被銷燬。 這與RCU相同。
集羣線程更新
在本節中,我將描述TLS如何用於集羣管理。 羣集管理包括xDS API處理和/或DNS以及運行情況檢查。
圖3顯示了涉及如下組件和步驟的整體流程:
集羣管理器是Envoy內部的組件,用於管理全部已知的上游集羣,CDS API,SDS / EDS API,DNS和活動(帶外)運行情況檢查。 它負責建立每一個上游集羣的最終一致視圖,其中包括已發現的主機以及運行情況。
運行情況檢查程序執行活動運行情況檢查,並將運行情況更改報告回集羣管理器。
執行CDS / SDS / EDS / DNS以肯定羣集成員資格。 狀態更改將報告回集羣管理器。
每一個工做線程都在不斷運行事件循環。
當集羣管理器肯定集羣的狀態已更改時,它會建立集羣狀態的新只讀快照,並將其發佈到每一個工做線程。
在下一個靜止期間,工做線程將更新分配的TLS插槽中的快照。
在須要肯定要負載均衡的主機的IO事件期間,負載均衡器將在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 ++固然不適合全部項目,甚至許多項目,但對於某些用例,它仍然是完成工做的惟一工具。
代碼連接
本文中討論的一些接口和實現頭的一些連接: