世界上最大的Web服務商Dropbox是如何從Nginx遷移到Envoy的?

banq發表於2020-07-31

在此部落格文章中,我們將討論基於Nginx的舊的交通基礎設施,其痛點以及透過遷移到Envoy所獲得的好處。我們將在許多軟體工程和運營方面將Nginx與Envoy進行比較。我們還將簡要介紹遷移過程,遷移過程的當前狀態以及在此過程中遇到的一些問題。
當將大部分Dropbox流量移至Envoy時,我們必須實現無縫地遷移一個系統,該系統已經處理了數千萬個開啟的連線,每秒數百萬個請求和數以兆計的頻寬。這實際上使我們成為了世界上最大的Envoy使用者之一。 

基於Nginx的傳統流量基礎架構
我們的Nginx配置大部分是靜態的,並結合使用Python2,Jinja2和YAML進行渲染。對其進行任何更改都需要完全重新部署。所有動態部分,例如上游管理和統計資訊匯出器,都用Lua編寫。任何足夠複雜的邏輯都移到了用Go語言編寫的下一個代理層
Nginx為我們服務了近十年。但這並不能適應我們當前的開發最佳實踐:

  • 我們的內部和(私有)外部API逐漸從REST遷移到gRPC,這需要來自代理的各種轉碼功能。
  • 協議緩衝區實際上已成為服務定義和配置的標準。
  • 所有軟體,無論使用哪種語言,都使用Bazel構建和測試。
  • 我們的工程師大量參與開源社群中的基本基礎結構專案。

另外,在操作上,Nginx的維護成本非常高:
  • 配置生成邏輯太靈活,無法在YAML,Jinja2和Python之間進行分配。
  • 監視是Lua,日誌解析和基於系統的監視的組合。
  • 越來越依賴第三方模組會影響穩定性,效能以及後續升級的成本。
  • Nginx部署和流程管理與其餘服務完全不同。它完全依賴於其他系統的配置:syslog,logrotate等,而不是與基本系統完全分離。

綜上所述,這是10年來的首次,我們開始尋找Nginx的潛在替代產品。

為什麼不使用Bandaid?
在內部,我們在很大程度上依賴於基於Golang的代理Bandaid。它可以與Dropbox基礎架構很好地整合,因為它可以訪問內部Golang庫的廣闊生態系統:監視,服務發現,速率限制等。我們考慮過從Nginx遷移到Bandaid,但是有一些問題使我們無法這樣做:

  • Golang比C / C ++佔用更多資源。資源的低利用率對於我們在Edge上尤其重要,因為我們無法輕鬆地“自動擴充套件”那裡的部署。
    • CPU開銷主要來自GC,HTTP解析器和TLS,後者的最佳化程度低於Nginx / Envoy使用的BoringSSL。
    • “按請求分類”模型和GC開銷大大增加了像我們這樣的高連線服務中的記憶體需求。
  • Go的TLS堆疊不支援FIPS。
  • Bandaid在Dropbox之外沒有社群,這意味著我們只能依靠自己進行功能開發。

考慮到所有這些,我們決定開始將流量基礎結構遷移到Envoy。

基於Envoy的新基礎設施
讓我們一個個地研究主要的開發和運營維度,以瞭解為什麼我們認為Envoy對我們來說是一個更好的選擇,以及從Nginx遷移到Envoy所獲得的收益。

1.效能
Nginx的架構是事件驅動和多程式的。它支援SO_REUSEPORTEPOLLEXCLUSIVE和工作者到CPU的固定。儘管它是基於事件迴圈的,但它不是完全非阻塞的。這意味著某些操作(例如開啟檔案或訪問/錯誤日誌記錄)可能會導致事件迴圈停止(即使啟用了aio,aio_write和執行緒池。)這會導致尾部延遲增加,從而可能導致數秒的延遲在旋轉磁碟驅動器上。
Envoy具有類似的事件驅動的體系結構,只是它使用執行緒而不是程式。它還具有SO_REUSEPORT支援(帶有BPF過濾器支援),並且依賴libevent進行事件迴圈實現(換句話說,沒有像ePOLLEXCLUSIVE這樣的epoll(2)功能。)Envoy在事件迴圈中沒有任何阻塞的IO操作。甚至日誌記錄也以非阻塞方式實現,因此不會引起停頓。
從理論上講,Nginx和Envoy應該具有相似的效能特徵。我們的測試結果表明,在大多數測試工作負載下,Nginx和Envoy的效能相似:每秒高請求(RPS),高頻寬以及混合的低延遲/高頻寬gRPC代理。
,結果有幾個顯著的差異:

  • Nginx顯示出更高的長尾延遲。這主要是由於在I / O繁重的情況下事件迴圈停滯所致,尤其是與SO_REUSEPORT一起使用時,因為在這種情況下,可以代表當前阻塞的worker接受連線
  • 不帶統計資訊收集的Nginx效能只是Envoy的一部分,但我們的Lua統計資訊收集在高RPS測試中將Nginx減慢了3倍。考慮到我們對lua_shared_dict的依賴,這是可以預期的。

我們確實瞭解統計資料收集的效率低下。我們考慮過在使用者空間中實現類似於FreeBSD的counter(9)的功能:CPU固定,按工時無鎖的計數器以及一個提取例程,該例程迴圈遍歷所有工作人員,彙總各自的統計資訊。但是我們放棄了這個想法,因為如果我們想檢測Nginx內部(例如所有錯誤情況),那就意味著要支援一個巨大的補丁程式,這將使後續的升級真正成為現實。
由於Envoy不會受到這兩個問題的困擾,因此在遷移到Envoy之後,我們可以釋放多達60%的伺服器(以前由Nginx獨佔)。

2.可觀察性
可觀察性是任何產品最基本的操作需求,但對於代理這樣的基礎架構而言尤其如此。在遷移期間,這一點尤為重要,以便監視系統可以檢測到任何問題,而沮喪的使用者也可以報告任何問題。
非商業Nginx帶有一個“ 存根狀態 ”模組,具有7個統計資訊。
這絕對是不夠的,因此我們新增了一個簡單的log_by_lua處理程式,該處理程式根據Lua中可用的標頭和變數新增每個請求的統計資訊:狀態程式碼,大小,快取命中率等。
除了每個請求的Lua統計資訊外,我們還有一個非常脆弱的error.log解析器,負責上游,http,Lua和TLS錯誤分類。
最重要的是,我們有一個單獨的匯出器來收集Nginx內部狀態:自上次重新載入以來的時間,工作人員數量,RSS / VMS大小,TLS證照使用期限等。
而一個典型的Envoy設定為我們提供了數千種不同的指標(以prometheus格式),描述了代理流量和伺服器的內部狀態。
這包括具有不同彙總的大量統計資訊:
  • 每個群集/每個上游/每個虛擬主機的HTTP統計資訊,包括連線池資訊和各種時序直方圖。
  • 每個偵聽器的TCP / HTTP / TLS下游連線統計資訊。
  • 從基本版本資訊和正常執行時間到記憶體分配器狀態和不贊成使用的功能使用情況計數器,各種內部/執行時狀態。

除統計資料外,Envoy還支援可插入的跟蹤提供程式。這不僅對擁有多個負載平衡層的Traffic團隊有用,對於希望從邊緣到應用伺服器端到端跟蹤請求延遲的應用程式開發人員也很有用。
最後但並非最不重要的一點是,Envoy能夠透過gRPC流式傳輸訪問日誌。這消除了我們的流量團隊支援syslog到配置單元橋接的負擔。此外,在Dropbox生產中啟動通用gRPC服務比新增自定義TCP / UDP偵聽器更容易(更安全!)。
像其他所有操作一樣,Envoy中訪問日誌的配置透過gRPC管理服務即訪問日誌服務(ALS)進行。管理服務是將Envoy資料平面與生產中的各種服務整合的標準方法。這將我們帶入下一個主題。

3.整合
Nginx的整合方法最好描述為“ Unix-ish”。配置是非常靜態的。它嚴重依賴檔案(例如配置檔案本身,TLS證照和票證,允許列表/阻止列表等)和眾所周知的行業協議(透過HTTP 記錄到syslog和auth子請求)。對於小型安裝而言,這樣的簡單性和向後相容性是一件好事,因為可以使用幾個Shell指令碼輕鬆實現Nginx的自動化。但是隨著系統規模的擴大,可測試性和標準化變得越來越重要。
Envoy對於如何將交通資料平面與其控制平面以及因此與其他基礎架構整合在一起的觀點更加堅定。透過提供通常稱為xDS的穩定API,它鼓勵使用protobufsgRPC。Envoy透過查詢一個或多個這些xDS服務來發現其動態資源。
這對於Dropbox尤其有用,Dropbox的所有服務已在內部透過基於gRPC的API進行互動。我們已經實現了自己的xDS控制平面版本,該版本將Envoy與我們的配置管理,服務發現,秘密管理和路由資訊整合在一起。
我們本土的Envoy控制平面實現了越來越多的xDS API。它被部署為生產中的常規gRPC服務,並充當我們基礎結構構建塊的介面卡。它透過一組通用的Golang庫來執行此操作,以與內部服務進行對話,並透過穩定的xDS API向Envoy公開它們。整個過程不涉及任何檔案系統呼叫,訊號,cron,logrotate,syslog,日誌解析器等。

4.配置
Nginx具有簡單易讀的配置的不可否認的優勢。但是,隨著配置變得越來越複雜,並且開始生成程式碼,這種勝利就失去了。
有兩個基本問題:
  • 沒有關於配置格式的宣告性描述。如果我們想以程式設計方式生成和驗證配置,則需要自己進行發明。
  • 從C程式碼的角度來看,語法上有效的配置仍然可能無效。例如,某些與緩衝區相關的變數具有值限制,對齊限制以及與其他變數的相互依賴性。為了從語義上驗證配置,我們需要透過nginx -t執行它。

另一方面,Envoy具有用於配置的統一資料模型:其所有配置都在協議緩衝區中定義。這不僅解決了資料建模問題,而且還將鍵入資訊新增到配置值中。鑑於protobuf是Dropbox生產中的頭等公民,並且是描述/配置服務的通用方式,因此整合變得非常容易。 
我們針對Envoy的新配置生成器基於protobufs和Python3。所有資料建模均在原始檔案中完成,而所有邏輯均在Python中進行。

5.可擴充套件性
將Nginx擴充套件到標準配置所不能提供的範圍之外,通常需要編寫C模組。Nginx的開發指南對可用的構建塊進行了紮實的介紹。就是說,這種方法是相對重量級的。實際上,需要相當資深的軟體工程師來安全地編寫Nginx模組。
就模組開發人員可用的基礎架構而言,他們可以期待基本容器,例如雜湊表/佇列/ rb-tree,(非RAII)記憶體管理以及請求處理所有階段的掛鉤。還有一些外部庫,例如pcre,zlib,openssl,當然還有libc。
為了提供更輕量級的功能擴充套件,Nginx提供了PerlJavascript介面。可悲的是,它們的能力都相當有限,大部分都侷限於請求處理的內容階段。
由社群採用最常用的擴充套件方法是基於第三方升UA- nginx的-模及各種OpenResty庫。這種方法幾乎可以掛接到請求處理的任何階段。我們使用log_by_lua收集統計資訊,並使用balancer_by_lua進行動態後端重新配置。
Envoy的主要擴充套件機制是透過C ++外掛。該過程的記錄不如Nginx的那樣,但它更簡單。部分原因是:
  • 乾淨且註釋良好的介面。C ++類充當自然的擴充套件和文件點。例如,簽出HTTP過濾器介面
  • C ++ 14語言和標準庫。從諸如模板和lambda函式之類的基本語言功能,到型別安全的容器和演算法。通常,編寫現代C ++ 14與使用Golang並沒有多大區別,或者甚至連說Python也沒有什麼不同。
  • C ++ 14及其標準庫以外的功能。abseil庫提供,這些包括來自較新C ++標準的直接替換,具有內建靜態死鎖檢測和除錯支援的互斥鎖,更多/更有效的容器等等

有關細節,這是HTTP過濾器模組規範示例
透過簡單地實現Envoy stats介面,我們能夠僅用200行程式碼將Envoy與Vortex2 (我們的監視框架)整合在一起。
Envoy 透過moonjit 獲得了Lua支援,moonjit是具有改進的Lua 5.2支援的LuaJIT分支。與Nginx的第三方Lua整合相比,它的功能和掛鉤要少得多。由於開發,測試和故障排除解釋程式碼的額外複雜性的成本,這使Envoy的Lua吸引力大大降低。專門從事Lua開發的公司可能會不同意,但是在我們的案例中,我們決定避免使用它,而僅將C ++用於Envoy的可​​擴充套件性。
Envoy與其他Web伺服器的不同之處在於它對WebAssembly(WASM)的新興支援-一種快速,可移植且安全的擴充套件機制。WASM不能直接使用,而可以用作任何通用程式語言的編譯目標。Envoy實現了WebAssembly for Proxies規範(還包括參考RustC ++ SDK),該規範描述了WASM程式碼和通用L4 / L7代理之間的邊界。代理伺服器程式碼和擴充套件程式碼之間的這種分隔允許安全的沙箱操作,而WASM低階緊湊二進位制格式允許接近本機的效率。最重要的是,在Envoy中,代理wasm擴充套件與xDS整合在一起。這樣可以進行動態更新,甚至可以進行潛在的A / B測試。
藉助WASM,服務提供商可以安全有效地在其邊緣執行客戶的程式碼。客戶從可移植性中受益:他們的擴充套件可以在實現代理代理ABI的任何雲上執行。另外,它允許您的使用者使用任何語言,只要它可以編譯為WebAssembly。這使他們能夠安全有效地使用更廣泛的非C ++庫集。
當前,我們不在Dropbox上使用WebAssembly。但是,當Go for SDK for proxy-wasm可用時,這可能會改變。

6. 構建和測試
預設情況下,Nginx是使用基於外殼的自定義配置系統和基於make的構建系統構建的。這是簡單而優雅的方法,但是將其整合到B Azel構建的monorepo中需要花費大量的精力,才能獲得增量,分散式,密封和可複製構建的所有優點。
在測試方面,Nginx 在單獨的儲存庫中有一組Perl驅動的整合測試,沒有任何單元測試。
鑑於我們對Lua的大量使用以及缺乏內建的單元測試框架,我們求助於使用模擬配置和基於Python的簡單測試驅動程式進行測試.
最重要的是,我們透過預處理所有生成的配置(例如,用127/8替換所有IP地址,切換到自簽名TLS證照等)並在結果上執行nginx -c來驗證所有語法的語法正確性。
在Envoy方面,主要的構建系統已經是Bazel。因此,將其與我們的monorepo整合起來很簡單:Bazel輕鬆地允許新增外部依賴項。Bazel是我們開發人員經歷過的最好的事情之一。它的學習曲線非常陡峭,並且是一筆大筆的前期投資,但在投資上卻有很高的回報:增量構建遠端快取分散式構建/測試等。
使用Envoy,我們可以靈活地使用帶有一組預先編寫的模擬的單元測試(基於gtest / gmock)或Envoy的整合測試框架,或同時使用兩者。對於每一個小小的變化,都不再需要依靠緩慢的端到端整合測試。
亞秒級的測試往返對生產率產生複合影響。它使我們能夠付出更多的努力來增加測試範圍。能夠在單元測試和整合測試之間進行選擇,使我們能夠平衡Envoy測試的覆蓋範圍,速度和成本。

7.安全
Nginx的程式碼面非常小,具有最小的外部依賴性。通常只對生成的二進位制檔案看到3個外部依賴項:zlib(或其更快的變體之一),TLS庫和PCRE。Nginx具有所有協議解析器,事件庫的自定義實現,甚至還可以重新實現某些libc函式。
在某些時候,Nginx被認為非常安全,以至於它被用作OpenBSD中的預設Web伺服器。後來,兩個開發社群陷入了混亂,導致建立了   httpd。您可以在BSDCon的“ 介紹OpenBSD 的新httpd ”中瞭解此舉背後的動機。
這種極簡主義在實踐中得到了報應。Nginx 在過去11年中僅報告了30個漏洞和暴露
另一方面,Envoy擁有更多的程式碼,尤其是當您考慮到C ++程式碼比用於Nginx的基本C語言要密集得多時。它還包含來自外部依賴項的數百萬行程式碼。從事件通知到協議解析器的所有內容均已解除安裝到第三方庫。這會增加攻擊面,並使生成的二進位制檔案膨脹。
為了解決這個問題,Envoy高度依賴現代安全實踐。它使用AddressSanitizerThreadSanitizerMemorySanitizer。它的開發人員甚至超越了這個範圍,採用了模糊測試
為了應對增加的漏洞風險,我們使用了來自上游OS供應商UbuntuDebian的最佳二進位制強化安全實踐。我們為所有邊緣曝光的二進位制檔案定義了特殊的強化構建配置檔案。它包括ASLR,堆疊保護器和符號表強化。
我們還希望在可能的情況下加強對第三方的依賴。我們在FIPS模式下使用BoringSSL,該模式包括啟動自檢和二進位制檔案的完整性檢查。我們還考慮在某些邊緣Canary伺服器上執行支援ASAN的二進位制檔案。

8.特點
Nginx最初是一個Web伺服器,專門用於以最少的資源消耗提供靜態檔案。它的功能是最重要的:靜態服務,快取(包括防雷群保護)和範圍快取。
但是,在代理方面,Nginx缺少現代基礎架構所需的功能。後端沒有HTTP / 2。gRPC代理可用,但沒有連線多路複用。不支援gRPC轉碼。最重要的是,Nginx的“開放核”模型限制了可以納入代理的開源版本的功能。結果,某些重要功能(如統計資訊)在“社群”版本中不可用。
相比之下,Envoy已經發展成為一個入口/出口代理,經常用於過載gRPC的環境。它的Web服務功能是基本的:沒有檔案服務,仍在進行中的快取brotli或預壓縮。對於這些用例,我們仍然有一個小的後備Nginx設定,Envoy將其用作上游群集。
Envoy還對許多與gRPC相關的功能提供了本機支援:

  • gRPC代理。這是一項基本功能,使我們能夠為應用程式(例如Dropbox桌面客戶端)端對端使用gRPC。
  • HTTP / 2到後端。此功能使我們可以大大減少流量層之間的TCP連線數,從而減少記憶體消耗和保持活動的流量。
  • gRPC→HTTP橋(+ 反向。)這些使我們能夠使用現代gRPC堆疊公開舊版HTTP / 1應用程式。
  • gRPC-WEB。此功能使我們即使在中間盒(防火牆,IDS等)尚不支援HTTP / 2的環境中也可以端到端使用gRPC。
  • gRPC JSON轉碼器。這使我們能夠將所有入站流量(包括Dropbox公共API)從REST轉換為gRPC。

此外,Envoy還可以用作出站代理。我們使用它來統一其他幾個用例:
  • 出口代理:由於Envoy 新增了對HTTP CONNECT方法的支援,因此可以用作Squid代理的替代產品。我們已經開始用Envoy替換出站Squid安裝。這不僅極大地提高了可視性,而且還透過使用通用資料平面和可觀察性統一堆疊來減少操作麻煩(不再為統計資訊解析日誌)。
  • 第三方軟體服務發現:我們依靠軟體中的Courier gRPC庫,而不是使用Envoy作為服務網格。但是,在需要一次性花費很少的精力將開源服務與服務發現連線起來的情況下,我們確實使用了Envoy。例如,Envoy在我們的分析堆疊中用作服務發現輔助工具。Hadoop可以動態發現其名稱和日記節點。Superset可以發現氣流,預存和配置單元后端。Grafana可以發現其MySQL資料庫。


遷移現狀
我們已經將Nginx和Envoy並排執行了半年多,並透過DNS逐步將流量從一個切換到另一個。到目前為止,我們已經將各種各樣的工作負載遷移到Envoy:

  • 入口高吞吐量服務。Dropbox桌面客戶端的所有檔案資料都透過Envoy透過端到端gRPC提供。透過切換到Envoy,由於從邊緣進行更好的連線重用,我們還略微提高了使用者的效能。
  • 入口高RPS服務。這是Dropbox桌面客戶端的所有檔案後設資料。我們獲得了端到端gRPC的相同好處,並且刪除了連線池,這意味著我們不受每個連線一次請求的限制。
  • 通知和遙測服務。在這裡,我們處理所有實時通知,因此這些伺服器具有數百萬個HTTP連線(每個活動客戶端一個)。現在可以透過流gRPC而不是昂貴的長輪詢方法來實現通知服務。
  • 高吞吐量/高RPS混合服務。API流量(後設資料和資料本身)。這使我們可以開始考慮公共gRPC API。我們甚至可以直接在Edge上轉換為對現有的基於REST的API進行程式碼轉換。
  • 出口高吞吐量代理。在我們的案例中,是Dropbox與AWS的通訊,主要是S3。這將使我們最終能夠從生產網路中刪除所有Squid代理,從而使我們只有一個L4 / L7資料平面。 

要遷移的最後一件事是www.dropbox.com本身。遷移之後,我們可以開始停用我們的邊緣Nginx部署。一個時代將會過去。

遇到的問題
當然,遷移並非完美無缺。但這並沒有導致任何明顯的中斷。遷移中最困難的部分是我們的API服務。Dropbox透過我們的公共API與很多不同的裝置進行通訊。除了我們的api使用者所依賴的Nginx和Envoy行為之間的許多不一致之外,Envoy及其庫中還存在許多錯誤。我們在社群的幫助下迅速解決了所有這些問題並將其上游。
這只是一些“異常的”/non-RFC行為的要點:

  • 合併URL中的斜槓。URL標準化和斜槓合併是Web代理的非常常見的功能。Nginx預設啟用斜線歸一化和斜線合併,但是Envoy不支援後者。我們向上遊提交了補丁,以新增該功能,並允許使用者使用 merge_slashes選項選擇加入。
  • 虛擬主機名中的埠。Nginx允許接收兩種形式的 Host標頭: example.com或 example.com:port。我們有幾個曾經依賴於此行為的API使用者。首先,我們透過在配置中複製虛擬主機(有埠和無埠)來解決此問題,但後來在Envoy端新增了一個忽略匹配埠的選項: strip_matching_host_port
  • 傳輸編碼區分大小寫。出於某種未知原因,一個小型子集API客戶端使用了 Transfer-Encoding:Chunked(請注意大寫的“ C”)標頭。這在技術上是有效的,因為RFC7230宣告 Transfer-Encoding / TE標頭不區分大小寫。該修復程式很簡單,已提交給上游特使。
  • 同時具有Content-Length和Transfer-Encoding的請求:c hunked。以前曾經與Nginx一起使用的請求,但因Envoy遷移而中斷。RFC7230有點棘手,但是一般的想法是Web伺服器應該錯誤地處理這些請求,因為它們很可能是“走私的”。另一方面,下一個句子表示代理應該只刪除 Content-Length標頭並轉發請求。我們擴充套件了http-parse,以允許圖書館使用者選擇支援此類請求,並且目前正在努力將支援新增到Envoy本身。

還值得一提的是,我們遇到了一些常見的配置問題:
  • 斷路配置錯誤。根據我們的經驗,如果您將Envoy作為入站代理執行,尤其是在HTTP / 1&HTTP / 2混合環境中,則錯誤地設定斷路器會在流量高峰或後端中斷期間導致意外停機。如果您不使用Envoy作為網狀代理,請考慮放鬆它們。值得一提的是,預設情況下,Envoy中的斷路限制非常嚴格-請注意!
  • 緩衝。Nginx允許在磁碟上進行請求緩衝。這在您具有無法理解分塊傳輸編碼的傳統HTTP / 1.0後端的環境中尤其有用。Nginx可以透過將它們快取在磁碟上,將它們轉換為具有Content-Length的請求。Envoy有一個Buffer過濾器,但是由於無法將資料儲存在磁碟上,因此我們只能在記憶體中緩衝多少記憶體。


 

相關文章