91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Dropbox是怎么將接入層從Nginx遷移到Envoy的

發布時間:2021-08-25 15:11:44 來源:億速云 閱讀:154 作者:小新 欄目:web開發

這篇文章主要介紹了Dropbox是怎么將接入層從Nginx遷移到Envoy的,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

在我們大部分的 Dropbox 流量轉到 Envoy 后,我們還必須無縫地將一個已經處理了數千萬個建立的連接,每秒數百萬個請求,并擁有數個 TB  帶寬的復雜系統遷移到上面。這實際上已經讓我們成為了世界上最大的 Envoy 用戶之一。

免責聲明:盡管我們試圖保持客觀性,但是本文中有很多對比是針對 Dropbox 以及我們軟件開發的方式進行的:我們的技術棧選型是 Bazel,gRPC 和  C++/Golang。

另外請注意,下文提到的 Nginx 指的是其開源版本,而不是具有附加功能的商業版本。

舊的一套基于 Nginx 的流量基礎設施

我們的 Nginx 配置絕大部分都是靜態的,然后通過結合 Python2、Jinja2 以及 YAML  等渲染。任意一處變動都需要一次完整的重新部署才能生效。所有動態的部分,比如 upstream 管理以及 stats exporter,這些是用 Lua  編寫的。更復雜的邏輯放到了用 Go 編寫的,下面一層的代理層。

Nginx 在我們的環境里良好運行了近十年。但是它已經無法再適應我們現在的開發優秀實踐:

  • 我們內部和(私有)外部的 API 逐漸從 REST 遷移到 gRPC,這需要各種代理層面的轉碼功能支持;

  • Protocol buffer 已經成為服務定義和配置的事實標準;

  • 所有軟件,無論使用哪種語言,均是通過 Bazel 構建和測試;

  • 我們的工程師開始重度參與開源社區里一些重要的基礎設施項目。

此外,在運維方面,Nginx 的維護成本非常高:

  • 配置生成邏輯太過靈活,而且拆分到了 YAML、Jinja2 和 Python等多處;

  • 監控是一個 Lua 腳本、日志解析以及基于系統的基礎監控的組合;

  • 越來越依賴于第三方模塊,而這會帶來穩定性、性能以及后續升級成本等多方面的影響;

  • Nginx 的部署及流程管理同其他服務完全不同。它完全依賴于其他一些系統的配置:syslog、logrotate 等,并沒有和基礎系統完全分離。

正是出于上述種種原因,10 年來我們首次開始尋找 Nginx 的潛在替代產品。

為什么不用 Bandaid?

正如我們上面提到的,在 Dropbox 內部,我們嚴重依賴一個 Golang 實現的代理(稱為 Bandaid)。之所以它可以和 Dropbox  的整個基礎設施很好地集成,原因是在于它可以訪問到我們內部 Golang 庫的廣闊生態:監控、服務發現、限流等。我們考慮過從 Nginx 遷移到 Bandaid  的方案,但是這里面有一些問題阻礙了我們這樣做:

  • Golang 比 C/C++ 占用更多的資源。資源的低使用率對于跑在邊緣端的我們至關重要,因為我們無法輕易地“自動擴容”那里的部署。

  • CPU 開銷主要來自 GC、HTTP 解析器和 TLS ,后者的優化程度低于 Nginx/Envoy 使用的 BoringSSL。

  • “每個請求一個 goroutine”的模型和 GC 開銷大大增加了像我們這樣高連接服務的內存需求。

  • Go 的 TLS 棧不支持 FIPS(譯者注:見 go #11658)。

  • 除了 Dropbox,Bandaid 沒有一個社區的支持,這意味著我們只能依靠我們自己來開發功能。

綜合上述原因,我們決定將所有流量基礎設施遷移到 Envoy。

全新的基于 Envoy 的流量基礎設施

讓我們逐一看看開發和運維幾個主要的維度,從中了解為什么我們認為 Envoy 對我們來說是一個更好的選擇,以及我們從 Nginx 遷移到 Envoy  之后獲得了哪些收益。

性能

Nginx 的架構是一個事件驅動和多進程的模式。它支持 SO_REUSEPORT,EPOLLEXCLUSIVE 以及 Worker  綁核。盡管它是基于事件循環的,但它并不是完全非阻塞的。這意味著某些操作(例如打開一個文件或記錄 access/error  日志)可能會導致事件循環中止(即使啟用了 aio,aio_write 和線程池),這樣會使得尾部延遲增加,從而導致旋轉磁盤驅動器上出現幾秒鐘的延遲。

Envoy 是一個類似的事件驅動式的架構,只是它使用的是線程而不是進程。它也同樣支持 SO_REUSEPORT(自帶一個 BPF 過濾器的支持),并且依賴  libevent 實現事件循環(換句話說,沒有用到像 EPOLLEXCLUSIVE 這樣的 epoll(2) 功能)。Envoy 在事件循環里并沒有任何阻塞  IO 的操作。甚至日志記錄也是以非阻塞方式實現的,因此它不會出現停頓的現象。

從理論上講,Nginx 和 Envoy 應該具有相似的性能特征。但是“希望”不是我們的做事風格,因此我們的第一步便是針對經過類似調整的 Nginx 和  Envoy 設置運行各種工作負載測試。

我們的測試結果表明,在大多數測試工作負載下,Nginx 和 Envoy 擁有相近的性能表現:單秒高請求(RPS),高帶寬以及混合的低延遲/高帶寬的  gRPC 代理。

可以說,進行一個良好的性能測試非常困難。Nginx 有提供一個用于性能測試的準則,但是這些準則尚未形成規范。Envoy 也有提供一個基準測試指南,甚至在  envoy-perf 項目下也提供了一些工具,但遺憾的是后者似乎不怎么維護了。

我們轉而使用內部的測試工具。之所以稱其為“綠巨人(hulk)”,是因為它在粉碎我們的服務方面享有盛譽。

這么說吧,我們發現測試結果里有幾處顯著的差異:

  • Nginx 表現出更高的長尾延遲。這主要是在遭遇繁重的 I/O 時事件循環停滯所致,尤其是與 SO_REUSEPORT  一起使用的情況,因為在這種情況下,它可以代表當前被阻塞的 Worker 程序繼續接受連接請求;

  • 在沒有開啟統計數據收集的情況下,Nginx 的功能和 Envoy 相當,但是我們的 Lua 統計數據收集模塊在高 RPS 測試中拖慢了 Nginx 3  倍。考慮到我們對 lua_shared_dict 的依賴,它在 Nginx 的各個 worker  之間是以一個互斥鎖進行同步,性能方面的損失也是意料之中的情況。

我們也知道統計數據收集模塊效率很低下。我們有考慮過在用戶空間里實現一個類似于 FreeBSD 的 counter(9) 的功能:綁定 CPU 核心,為每個  worker 分配一個無鎖的計數器以及一個取值的例程,該例程會循環遍歷所有 worker  并聚合匯總他們各自的統計信息。但是我們最終放棄了這個想法,因為如果我們想要監控 Nginx  內部狀態(比如所有錯誤情況),那就意味著我們要維護一個巨大的補丁程序,這將使得后續升級變成一個真正的地獄。

由于 Envoy 不會受到這兩個問題的困擾,因此在遷移到 Envoy 之后,我們可以釋放多達 60% 的服務器資源(之前被 Nginx 獨占)。

可監察性

可監察性是任何產品的最基本的運維需求,尤其是對于像代理這樣的基礎設施而言。在遷移期間,這一點尤為關鍵,這樣一來任何問題都可以被監控系統檢測到,而不用等到沮喪的用戶報障時才發現。

非商業版本的 Nginx 自帶一個“stub status”模塊,提供了 7 個統計信息:

Active connections: 291  server accepts handled requests 16630948 16630948 31070465  Reading: 6 Writing: 179 Waiting: 106

這當然是不夠的,因此我們添加了一個簡單的 log_by_lua 處理程序,該處理程序根據 Lua 中可用的 HTTP header  和變量添加每個請求的統計信息:狀態代碼,大小,緩存命中率等。以下是一個簡單的吐出統計數據的函數的例子:

function _M.cache_hit_stats(stat) if _var.upstream_cache_status then     if _var.upstream_cache_status == "HIT" then         stat:add("upstream_cache_hit")     else         stat:add("upstream_cache_miss")     end end end

除了每個請求的 Lua 統計信息外,我們還有一個非常脆弱的 error.log 解析器,它負責 upstream,http,Lua 和 TLS  的錯誤分類。

在這些之上,我們有一個單獨的 exporter ,用于收集 Nginx 的內部狀態:自上次 reload 的時間,worker 數量,RSS/VMS  大小,TLS 證書使用期限等。

典型的 Envoy 配置為我們提供了數千種不同的指標(采用 Prometheus 格式),用于描述代理流量和服務器的內部狀態:

$ curl -s http://localhost:3990/stats/prometheus | wc -l  14819

這里面囊括了五花八門的來自不同地方匯總的大量統計信息:

  • 每個集群/每個 upstream /每個 vhost 的 HTTP 統計信息,包括連接池信息和各種時序直方圖;

  • 每個監聽器的 TCP/HTTP/TLS 下游的連接統計信息;

  • 從基本版本信息和正常運行時間到內存分配統計以及棄用功能使用情況計數,各種內部/運行時狀態的統計信息。

Envoy 的管理接口真心贊。它不僅通過 /certs,/clusters 和 /config_dump  端點提供額外的結構化的統計信息,而且還提供了非常重要的運維功能:

  • 通過 /logging 提供即時更改錯誤日志配置的能力。這讓我們能夠在幾分鐘內排查相當模糊的問題;

  • /cpuprofiler,/heapprofiler,/contention 在不可避免的性能排障期間肯定會有用武之地;

  • /runtime_modify 端點允許我們更改配置參數集而不用推送新的配置,而這些配置可用于功能開關等。

除了統計數據外,Envoy 還支持可插拔的 tracing  實現。這不僅對擁有多個負載均衡層的接入層團隊有用,對于希望從邊緣到應用服務器端到端跟蹤請求延遲的應用程序開發人員也很有用。

從技術上講,Nginx 也支持通過第三方的 OpenTracing 集成進行跟蹤,但是該功能開發尚不成熟。

最后,同等重要的是,Envoy 能夠通過 gRPC 流式傳輸 access 日志。這減輕了我們接入層團隊不得不支持打通 syslog 到 hive  的負擔。此外,在 Dropbox 生產中啟動通用 gRPC 服務比添加自定義的 TCP/UDP 監聽器更容易(也更安全!)。

和其他所有操作類似,Envoy 里面 access 日志的配置是通過 gRPC 管理服務,即訪問日志服務(ALS)設置。管理服務是將 Envoy  數據平面與生產中的各種服務集成的標準方式。這也是我們下一節要介紹的內容。

集成

Nginx 提供的集成方案,一個最佳描述便是 “Unix-ish”。配置是非常靜態的。而且它嚴重依賴于文件(例如配置文件本身,TLS 證書和 Ticket  、白名單/黑名單等)以及一些知名的行業協議(記錄日志到 syslog 以及通過 HTTP  發送認證子請求)。對于小型部署而言,這樣做的簡單性和向后兼容性是一件好事,因為我們可以通過編寫幾個 Shell 腳本輕松實現 Nginx  的自動化。但是隨著系統規模的擴大,可測試性和標準化變得更加重要。

Envoy 對于是否應該將接入層數據平面和它的控制平面,以及因此也即是與其他基礎設施集成在一起,持更加堅定的態度。它通過提供一個穩定的 API(通常稱為  xDS ),鼓勵用戶使用 protobuf 和gRPC。Envoy 通過查詢一個或多個這樣的 xDS 服務來發現其動態資源。

如今,xDS API 的發展已然超越 Envoy:通用數據平面 API(UDPA)的宏偉目標是“成為事實上的 L4/L7 負載均衡器標準”。

根據我們的經驗,這一雄心壯志是靠譜的。我們已經在內部負載測試中使用了開放請求成本匯總(ORCA),并且正在考慮將 UDPA 用于非 Envoy  的負載均衡,例如我們基于 Katran 的 eBPF/XDP 4層負載均衡代理。

這對于 Dropbox 尤其適合,Dropbox 的所有服務都已經實現了在內部通過基于 gRPC 的 API 進行交互。我們已經實現了自主版本的 xDS  控制平面,它將 Envoy 同我們的配置管理、服務發現、私密信息管理以及路由信息集成在一起。

以下是一些可用的 xDS 服務,它們的 Nginx 替代品以及我們如何使用它們的一些示例:

  • 如上所述,訪問日志服務(ALS)讓我們可以動態地配置訪問日志目標,編碼和格式。試想一下 Nginx 的 log_format 和 access_log  的動態版本;

  • 端點發現服務(EDS)提供相關集群成員的信息。這類似于 Nginx 配置中動態更新 upstream 里的 server 條目列表(例如,對于 Lua  來說便是 balancer_by_lua_block)。在這里,我們將其代理到內部的服務發現;

  • 私密信息發現服務(SDS)提供了各種與 TLS 相關的信息,這些信息將涵蓋各種 ssl_ *  指令(以及相應的ssl_*_by_lua_block)。我們將這個接口調整為適用于我們場景的私密信息分發服務;

  • 運行時發現服務(RTDS)提供了運行時標志。我們在 Nginx 中實現這一功能的方式很不可靠,它是通過在 Lua  里檢查各種文件是否存在來實現的。這種方法很快會在各個服務器之間變得不一致。 Envoy 的默認實現也是基于文件系統的,但是我們將 RTDS xDS API  指向了我們的分布式配置存儲。這樣一來,我們便可以一次控制整個集群(通過帶有類似 sysctl  界面的工具),并且在不同服務器之間不會出現意外不一致的情況;

  • 路由發現服務(RDS)將路由映射到虛擬主機,并允許對 HTTP 頭部和過濾器進行額外的配置。用 Nginx 術語來說,它們類似于帶有  set_header/proxy_set_header 和 proxy_pass 的動態 location  配置。在更底下的代理層,我們直接在我們的服務定義配置中自動生成這些配置;

關于 Envoy 如何與現有生產系統集成的示例,這里有一個將 Envoy 與自定義服務發現集成的一個經典例子。還有一些開源的 Envoy  控制面板實現,例如 Istio 和更少復雜度的 go-control-plane。

我們自己的 Envoy 控制平面實現了越來越多的 xDS API。它在生產中以普通的 gRPC  服務的形式部署,并充當我們基礎設施構建塊的一個適配器。它通過一組通用的 Golang 庫來實現與內部服務進行對話,并通過穩定的 xDS API 向 Envoy  公開它們。整個過程不涉及任何文件系統調用,信號量,cron,logrotate,syslog,日志解析器等。

配置

Nginx 在配置方面具有簡單易讀這樣一項不可否認的優勢。但是,隨著配置變得越來越復雜,并且配置開始變成是代碼生成式的,這項優勢就沒了。

正如上面所提到的,我們的 Nginx 配置是通過 Python2,Jinja2 和 YAML 的混合生成的。 你們中的一些人可能已經在  erb,pug,Text::Template 甚至 m4 里面看到或甚至編寫了這樣類似的變體:

{% for server in servers %} server { {% for error_page in server.error_pages %} error_page {{ error_page.statuses|join(' ') }} {{ error_page.file }}; {% endfor %} ... {% for route in service.routes %} {% if route.regex or route.prefix or route.exact_path %} location {% if route.regex %}~ {{route.regex}}{%         elif route.exact_path %}= {{ route.exact_path }}{%         else %}{{ route.prefix }}{% endif %} {     {% if route.brotli_level %}     brotli on;     brotli_comp_level {{ route.brotli_level }};     {% endif %}     ...

我們的 Nginx 配置的生成方式有一個大問題:配置生成中涉及的所有語言都允許代入 和/或 邏輯。YAML 有 anchor,Jinja2 有  loop/ifs/macroses,然后,Python 當然是圖靈完備的。如果沒有一個干凈的數據模型,復雜性會迅速擴散到這三者。

這個問題自然是可以解決的,但是有兩個基本問題:

  • 這里沒有關于配置格式的聲明性描述。如果我們想通過編程的方式生成和驗證配置,那么需要自己重新造輪子;

  • 語法上有效的配置,從 C  代碼的角度來看仍然可能是無效的。例如,某些與緩沖區相關的變量具有值限制,對齊限制以及與其他變量的相互依賴性。為了從語義上驗證配置,我們需要運行 nginx -t  來校驗。

另一方面,Envoy 自帶一套用于配置的統一數據模型:它的所有配置都放到 protobuffer  中定義。這不僅解決了數據建模問題,而且還將輸入的信息添加到了配置值里。鑒于 protobuf 是Dropbox  生產環境里的頭等公民,并且是描述/配置服務的通用方式,因此,集成就變得更加簡單了。

我們針對 Envoy 設計的新的配置生成器是基于 protobuf 和 Python3 實現的。所有數據建模均在原始文件中完成,而所有邏輯均在  Python 中執行。下面是一個例子:

from dropbox.proto.envoy.extensions.filters.http.gzip.v3.gzip_pb2 import Gzip from dropbox.proto.envoy.extensions.filters.http.compressor.v3.compressor_pb2 import Compressor  def default_gzip_config( compression_level: Gzip.CompressionLevel.Enum = Gzip.CompressionLevel.DEFAULT, ) -> Gzip:     return Gzip(         # Envoy's default is 6 (Z_DEFAULT_COMPRESSION).         compression_level=compression_level,         # Envoy's default is 4k (12 bits). Nginx uses 32k (MAX_WBITS, 15 bits).         window_bits=UInt32Value(value=12),         # Envoy's default is 5. Nginx uses 8 (MAX_MEM_LEVEL - 1).         memory_level=UInt32Value(value=5),         compressor=Compressor(             content_length=UInt32Value(value=1024),             remove_accept_encoding_header=True,             content_type=default_compressible_mime_types(),         ),     )

注意上述代碼里的 Python3 類型注解!結合 mypy-protobuf protoc 插件,它們可以在 config  生成器里提供端到端的輸入。如果你用的 IDE 支持自動檢查的話,它將會立即高亮顯示輸入信息不匹配。

在某些情況下,經過類型檢查的 protobuf 在邏輯上仍然可能是無效的。在上面的示例中,gzip window_bits 只能取 9 到 15  之間的值。這類限制可以在 protoc-gen-validate protoc 插件的幫助下輕松完成定義:

google.protobuf.UInt32Value window_bits = 9 [(validate.rules).uint32 = {lte: 15 gte: 9}];

最后,使用官方定義的配置模型的一個潛在好處是,它有機地引領了文檔與配置定義并排配置。 以下是一個 gzip.proto 的示例:

// Value from 1 to 9 that controls the amount of internal memory used by zlib. Higher values.            // use more memory, but are faster and produce better compression results. The default value is 5.             google.protobuf.UInt32Value memory_level = 1 [(validate.rules).uint32 = {lte: 9 gte: 1

可擴展性

要想讓 Nginx 提供超出標準配置所提供的功能范疇之外的特性的話,通常需要編寫一個 C 模塊。 Nginx  的開發指南對于可用的構建塊提供了詳盡的介紹。這也就是說,這種方式是相對重量級的。實際上,要想安全地編寫一個 Nginx  模塊的話,需要一位相當資深的軟件工程師。

關于可供模塊開發人員選用的基礎設施的話,他們可以期待一些像哈希表、隊列、rb樹這樣的基礎容器,(非RAII)內存管理、以及可用于所有請求處理階段的鉤子。另外,還有一些外部庫,比如  pcre、zlib、openssl,當然,還有libc。

為了提供更輕量級的功能擴展,Nginx 提供了 Perl 和 JavaScript  的接口。可悲的是,它們所提供的功能都相當有限,絕大部分都局限于請求處理的內容階段。

社區采納的最常用的擴展方式是基于第三方的 lua-nginx 模塊和各種 Openresty 類庫。這一方案幾乎可以外掛到請求處理的任意階段。我們使用  log_by_lua 進行統計信息的收集,然后使用 balancer_by_lua 進行動態地后端再配置。

從理論上講,Nginx 提供了使用 C++ 開發模塊的能力。然而實際上,對于所有原語,它都缺少適當的 C++  接口/包裝器,這樣就顯得不太值得了。盡管如此,仍有一些社區對此進行嘗試。這些還遠遠沒有達到生產就緒。

Envoy 的主要擴展機制是通過 C++ 插件。這個流程在文檔方面不如 Nginx,但是它更為簡單。部分原因是:

  • 整潔且擁有良好注釋的界面。C++ 類充當自然的擴展和文檔點。比如,看看 HTTP 過濾接口。

  • C++14 語言和標準庫。從 template 和 lambda 函數等這些基本的語言功能,到類型安全的容器和算法。通常,編寫現代 C++14 與使用  Golang 并沒有多大區別,或者有些人甚至會說 Python。

  • C++14 及其標準庫以外的功能。由 abseil 庫提供的,其中包括來自較新版本的 C++ 標準的直接替換,內置了靜態的死鎖檢測和 debug  支持的互斥鎖,以及額外的/更有效的容器,等等。

具體地,這里有一份 HTTP 過濾模塊的經典例子。

通過簡單地實現 Envoy stats 接口,我們僅用200行代碼就可以將 Envoy 與 Vortex2(我們的監視框架)集成在一起。

Envoy 還通過 moonjit 添加了對 Lua 的支持,moonjit 是一個對 Lua 5.2 做了諸多改進支持的 LuaJIT fork。與  Nginx 的第三方 Lua 集成相比,它所提供的功能和開放的鉤子要少得多。由于在開發、測試和解釋后代碼的排障等諸多方面存在的額外復雜性成本,這使得 Lua 對  Envoy 的吸引力大大降低。專門從事 Lua 開發的公司可能會不同意這一觀點,但是在我們的案例中,我們決定避免使用它,而只是將 C++ 用于 Envoy  的可擴展性。

Envoy 與其他 Web 服務器的區別就在于它提供了對 WebAssembly(WASM)的新興支持 —— 一種快速的,可移植且安全的擴展機制。WASM  不能直接使用,但是可以作為任何通用編程語言的編譯目標。Envoy 實現了一個適用于代理的 WebAssembly 規范(還包括相關的 Rust 和 C++  SDK),該規范描述了 WASM 代碼和通用的 L4/L7 代理之間的邊界。代理和擴展之間代碼的分隔提供了一個安全的沙箱,而 WASM  低級凝練的二進制格式又為之提供了接近原生的效率。在此之上,在 Envoy 里,proxy-wasm 擴展是和 xDS  集成在一起的。這樣便可以進行動態地更新,甚至可以進行潛在的 A/B 測試。

在 KubeCon’s 19 上,《通過 WebAssembly 擴展 Envoy》這個演講很好地概述了 Envoy 里集成的 WASM  及其潛在用途。它還暗示已經達到了原生 C++ 代碼性能水平的 60-70%。

使用 WASM,服務提供方可以安全有效地在其邊緣環境運行客戶的代碼。客戶獲益的則是可移植性:他們的擴展可以在實現 proxy-wasm ABI  的任何云上運行。此外,它允許用戶使用任意一種語言,只要它可以編譯為 WebAssembly。這使得他們能夠安全有效地使用更廣泛的非 C++ 類庫。

Istio 正在向 WebAssembly 的開發傾注大量資源:他們已經有了基于 WASM 的遙測擴展的實驗版本以及用于共享擴展的  WebAssemblyHub 社區。

當前,Dropbox 并未用到 WebAssembly。但是,等到 proxy-wasm 的 GO SDK 可用時,這一情況也許會發生變化。

構建和測試

默認情況下,Nginx 使用的是一套自定義的基于 shell 的配置系統和基于 make 的構建系統構建的。這是簡單而優雅的,但是將其集成到 Bazel  構建的 monorepo 的話需要花費大量的精力才能獲得增量、分布式、封閉和可重現構建這些所有優點。

Google 開源了他們用 Bazel 構建的 Nginx 版本,該版本由 Nginx,BoringSSL,PCRE,ZLIB 和 Brotli  庫/模塊組成。

在測試方面,Nginx 在一個單獨的倉庫里有一組 Perl 驅動的集成測試,沒有任何單元測試。

鑒于我們對 Lua 的大量使用以及缺乏內置的單元測試框架,我們求助于使用模擬配置和基于 Python 的簡單測試驅動程序進行測試:

class ProtocolCountersTest(NginxTestCase): @classmethod def setUpClass(cls):     super(ProtocolCountersTest, cls).setUpClass()     cls.nginx_a = cls.add_nginx(         nginx_CONFIG_PATH, endpoint=["in"], upstream=["out"],     )     cls.start_nginxes()  @assert_delta(lambda d: d == 0, get_stat("request_protocol_http2")) @assert_delta(lambda d: d == 1, get_stat("request_protocol_http1")) def test_http(self):     r = requests.get(self.nginx_a.endpoint["in"].url("/"))     assert r.status_code == requests.codes.ok

最重要的是,我們通過預處理所有生成的配置(例如用 127/8 替換所有 IP 地址,切換到自簽名 TLS 證書等)并在結果上運行 nginx -c  來驗證所有語法的語法正確性。

在 Envoy 方面,其主流的構建系統已經是 Bazel。因此,將它和我們的 monorepo 集成起來并不繁瑣:Bazel  支持輕松添加外部依賴項。

我們還使用 copybara 腳本來同步 Envoy 和 udpa 的  protobuf。當你需要進行簡單的轉換而無需永遠維護大型補丁集時,Copybara 十分方便。

通過 Envoy,我們可以靈活地選擇使用一組預先編寫好的 mock 來進行單元測試(基于 gtest/gmock ),也可以選擇使用 Envoy  的集成測試框架,或者同時使用兩者。至此,針對每一項細小的改動,我們不再需要依靠緩慢的端到端集成測試了。

gtest 是 Chromium 和 LLVM 等項目所采用的一套相當有名的單元測試框架。如果你想進一步了解  googletest,這里有兩份不錯的介紹資料:googletest 和 googlemock。

開源的 Envoy 開發要求每次更改達到 100% 的單元測試覆蓋率。它會通過 Azure CI 流水線針對每個 PR 自動觸發測試。

使用 google/becnhmark 對性能敏感的代碼進行微基準測試也是一種常見做法:

$ bazel run --compilation_mode=opt test/common/upstream:load_balancer_benchmark -- --benchmark_filter=".*LeastRequestLoadBalancerChooseHost.*" BM_LeastRequestLoadBalancerChooseHost/100/1/1000000          848 ms          449 ms            2 mean_hits=10k relative_stddev_hits=0.0102051 stddev_hits=102.051 ...

改用 Envoy 之后,我們開始完全依賴單元測試來進行內部模塊開發:

TEST_F(CourierClientIdFilterTest, IdentityParsing) { struct TestCase { std::vector<std::string> uris; Identity expected; }; std::vector<TestCase> tests = { {{"spiffe://prod.dropbox.com/service/foo"}, {"spiffe://prod.dropbox.com/service/foo", "foo"}}, {{"spiffe://prod.dropbox.com/user/boo"}, {"spiffe://prod.dropbox.com/user/boo", "user.boo"}}, {{"spiffe://prod.dropbox.com/host/strange"}, {"spiffe://prod.dropbox.com/host/strange", "host.strange"}}, {{"spiffe://corp.dropbox.com/user/bad-prefix"}, {"", ""}}, }; for (auto& test : tests) { EXPECT_CALL(*ssl_, uriSanPeerCertificate()).WillOnce(testing::Return(test.uris)); EXPECT_EQ(GetIdentity(ssl_), test.expected); } }

實施亞秒級的往返測試在生產力方面會帶來復合效果。它讓我們能夠把更多精力放在增加測試覆蓋范圍。由于可以在單元測試和集成測試之間進行自由選擇,這使得我們能夠在  Envoy 測試的覆蓋范圍、速度和成本之間獲得一個平衡。

學習上手 Bazel  對我們開發人員來說是一次絕佳的經歷。它的學習曲線非常陡峭,而且前期需要付出大量的投資,但是它卻有很高的回報:增量式構建,遠程緩存,分布式構建/測試等。

Bazel  很少討論的好處之一是,它讓我們能夠查詢甚至擴展依賴圖譜。依賴關系圖的編程接口,以及跨語言的通用構建系統,這是一項非常強大的功能。它可以用作代碼提示器,代碼生成,漏洞跟蹤以及部署系統等這類服務的基礎構建塊。

安全性

Nginx 的代碼面非常小,它的外部依賴很少。通常,對生成的二進制文件來說只能看到 3 個外部依賴項:zlib(或者更快的變體之一),一個 TLS 庫以及  PCRE。Nginx 自研實現了所有相關的協議解析器、事件庫,甚至還重新實現了某些 libc 函數。

在某些情況下,Nginx 被認為非常安全,以至于它被用作 OpenBSD 里默認的 Web 服務器。后來,兩個開發社區發生了沖突,也因此有了  httpd。您可以在 BSDCon《介紹OpenBSD的新 httpd 》中了解此舉背后的動機。

這種極簡主義在實踐中得到了回報。Nginx 在11年多的時間里僅報告了30個安全漏洞。

另一方面,Envoy 的代碼量更大,尤其是考慮到 C++ 代碼比用于 Nginx 的基本 C  代碼更加密集時,這一點更為明顯。它還包含來自外部依賴項的數百萬行代碼。從事件通知到協議解析器的所有內容都推給了第三方庫。這會增加攻擊面并造成最終產出的可執行文件愈加膨脹。

為了解決這個問題,Envoy 高度依賴現代安全實踐。它使用 AddressSanitizer,ThreadSanitizer 和  MemorySanitizer。它的開發人員甚至超綱地采用了模糊測試。

任何對于全球IT基礎架構至關重要的開源項目都可以接入到  OSS-Fuzz,這是一個用于自動化模糊測試的免費平臺。要了解更多信息,請參閱“OSS-Fuzz/架構”。

實際上,盡管如此,所有這些預防措施都不能完全抵消增加的代碼所留下的痕跡。結果便是,在過去的兩年里,Envoy 已經發布了 22 條安全公告。

Envoy 的《安全發布策略》一文對此進行了詳細描述,針對某些指定的漏洞還有詳細的檢查報告。Envoy 還是 Google  漏洞獎勵計劃(VRP)的成員之一。VRP 向所有安全研究人員開放,根據它們設計的規則,對發現和報告的漏洞提供獎勵。

為了應對不斷增長的漏洞風險,我們使用了來自上游發行版供應商 Ubuntu 和 Debian  的最佳可執行文件來強化安全措施。我們為所有邊緣環境曝光的可執行文件定義了特殊加固后的構建配置文件。它包括 ASLR,堆棧保護器以及符號表的加固:

build:hardened --force_pic build:hardened --copt=-fstack-clash-protection build:hardened --copt=-fstack-protector-strong build:hardened --linkopt=-Wl,-z,relro,-z,now

在絕大多數環境里,fork 的 Web 服務器(如  Nginx)在堆棧保護器方面會存在問題。由于主進程和工作進程共享同一套堆棧來進行灰度,而在灰度驗證失敗時,工作進程會被殺死,因此,我們可以通過大約 1000  次嘗試逐位對灰度的實例進行暴力破解。使用線程作為并發原語的 Envoy 不受此攻擊的影響。

我們還希望盡可能的給第三方依賴的安全性加固。我們在 FIPS 模式下使用  BoringSSL,該模式包括啟動自檢和可執行文件的完整性檢查。我們還考慮在某些邊緣環境的灰度服務器上運行啟用 ASAN 的可執行文件。

功能性

這是帖子中最主觀的部分,請做好準備。

Nginx 最初是設計成一個 Web 服務器,致力于以最少的資源消耗來提供靜態文件服務。它的功能性在這里是最重要的:靜態文件服務,緩存(包括驚群保護)以及  range 緩存。

但是,作為代理的話,Nginx 缺乏現代基礎架構所需的功能。它的后端沒有 HTTP/2。盡管可以代理 gRPC 服務,但是它不支持連接多路復用。而且不支持  gRPC  轉碼。最重要的是,Nginx的“核心開放(open-core)”模型限制了可以納入開源版本代理服務的功能集。結果便是,某些重要功能(如統計信息)在它的“社區”版本里是不可用的。

相比之下,Envoy 已經演變為一個 ingress/egress 代理,經常用于以 gRPC 為主要負載的環境。盡管它的 Web  服務功能還是相當初級的:沒有文件服務,而且緩存功能仍然在開發中,brotli或者預壓縮這些功能也都不支持。針對這些場景,我們仍然提供了一個小的 fallback  Nginx 實例,Envoy 會把它當成上游集群。

等到 Envoy 的 HTTP 緩存功能生產就緒時,我們便可以將大多數靜態服務用例遷移過去,使用 S3 取代文件系統作為長期存儲。

Envoy 還提供了許多 gRPC 相關能力的原生支持:

  • gRPC 代理。這是一項基本功能,允許我們在應用程序里端到端地使用 gRPC(比如 Dropbox 桌面客戶端)

  • HTTP/2 到后端。這項功能使得我們可以大大減少流量層之間的 TCP 連接數,從而減少內存消耗和 keepalive 的流量。

  • gRPC -> HTTP 的橋接(+反向代理)這些讓我們能夠使用現代的 gRPC 棧對外暴露舊版的 HTTP/1 應用程序服務。

  • gRPC-WEB。這項功能讓我們即使在中間層(防火墻,IDS等)尚不支持 HTTP/2 的環境里也可以端對端地使用gRPC。

  • gRPC JSON 轉碼器。這讓我們能夠將所有入站流量(包括 Dropbox 公共 API )從 REST 轉到 gRPC。

此外,Envoy 還可以用作一個出站代理。我們通過它統一了另外幾個用例場景:

  • Egress 代理:自從 Envoy 添加了對 HTTP CONNECT 方法的支持以后,它便可以用作 Squid 代理的替代產品。我們已經開始用  Envoy 替換出站的 Squid  實例。這樣做不僅極大地提高了可見性,而且還通過統一使用通用的數據平面和可觀察性技術棧省掉了一攬子運維方面的糟心事(不再需要為了統計信息而去解析日志)

  • 第三方軟件服務發現:我們并沒有使用 Envoy 來構建服務網格,取而代之的是,我們依靠的是軟件里的 Courier gRPC  庫。但是,當遇到需要一次性花費最小的精力將開源服務與我們的服務發現連接的場景時,我們實際還是選擇了 Envoy。比如,Envoy  在我們的分析棧里用作服務發現的輔助工具。Hadoop 可以動態地發現它的 name 及 journal 節點。 Superset  可以發現airflow,presto 以及 hive 的后端。Grafana 可以發現它的 MySQL 數據庫。

社區

Nginx 的開發是相當中心化的。它的絕大部分開發活動都是核心團隊內部進行的。nginx-devel 郵件列表上有一些外部活動,然后官方 Bug  跟蹤上偶爾有一些與開發相關的討論。

FreeNode 上有一個 #nginx 頻道。我們可以隨時加入到里面進行更多互動性質的社區對話。

Envoy 的開發則是開放和去中心化的:通過 GitHub issue/pull request,郵件列表和社區會議進行協同。

Slack 上的社區活動也很活躍。你可以在這里收到邀請。

開發風格和工程社區這些方面很難量化定性,因此,我們不妨一起來看一個開發 HTTP/3 的特定例子。

F5 最近發表了 Nginx QUIC 和 HTTP/3  實現。這塊代碼是干凈的,沒有任何外部依賴。但是開發過程本身并不透明。在此之前的半年,Cloudflare 提出了自己的 Nginx HTTP/3  實現。結果便是,該社區現在擁有兩套單獨的用于 Nginx 的 HTTP/3 實驗版本。

再來看看 Envoy ,HTTP/3 的實現這塊也正在開發中,它是基于 chromium 的  “quiche”(QUIC,HTTP等)庫。該項目的進展可以通過 這個 GitHub issue  跟蹤。在補丁開發完成前,設計文檔便已經對外公布了。剩下的工作里,想要從社區參與中獲益的部分會帶有 “help wanted” 標簽。

如你所見,后者顯然更加開放透明,而且非常鼓勵協作開發。對我們來說,這意味著我們能夠從上游獲得大量的 Envoy 相關的大大小小的變動 &mdash;&mdash;  包括從運維改進和性能調優到新的 gRPC 轉碼功能以及負載均衡功能的一些變動。

遷移后的現狀

我們已經讓 Nginx 和 Envoy 并排運行了半年多,然后通過 DNS  逐步將流量從一個切換到另一個。到目前為止,我們已經成功將許多各式各樣的工作負載遷移到了 Envoy:

  • Ingress 高吞吐量服務。Dropbox 桌面客戶端的所有文件數據都是通過 Envoy 背后端到端 的 gRPC 提供服務。改用 Envoy  后由于在邊緣環境的連接復用效果更好,我們還略微提高了用戶的性能。

  • Ingress 高RPS服務。這是 Dropbox 桌面客戶端的所有文件元數據。同樣地,我們獲得了端到端的 gRPC  帶來的好處,而且去掉了連接池,這意味著我們不受每個連接一次請求的限制。

  • 通知和遙測服務。這里是用來處理所有實時通知的服務,因此這些服務器一般會有數百萬個 HTTP  連接(每個活躍的客戶端會有一個連接)。如今我們不再需要使用昂貴的長輪詢方式來實現通知服務,取而代之的是,我們可以通過 streaming gRPC  來實現。

  • 兼有高吞吐量/高RPS的混合服務。API 流量(元數據和數據本身)。這使得我們開始考慮一些公共的 gRPC API。我們甚至可以直接在邊緣環境對現有的基于  REST 的 API 進行轉碼。

  • Egress 高吞吐量代理。在我們的使用場景,Dropbox 和 AWS 的通信主要是 S3 這塊。最終我們會在生產網絡里下線所有的 Squid  代理,然后只留下一個 L4/L7 的數據平面。

遷移的最后一件事便是 www.dropbox.com 自己。在完成遷移后,我們便可以開始停用我們在邊緣環境部署的 Nginx  服務。一個時代即將結束。

我們遇到的一些問題

當然,整個遷移的過程并非完美無瑕。不過至少沒有導致任何明顯的中斷。整個遷移過程中最困難的部分是我們的 API 服務這塊。大量不同的設備通過我們的公共  API 和 Dropbox 進行通信&mdash;&mdash;從 curl/wget 驅動的 Shell 腳本,以及具有自定義 HTTP/1.0  堆棧的嵌入式設備,到每個可能訪問到這里的 HTTP 庫。 Nginx 是經過實踐檢驗的行業事實標準。可以理解,大多數庫都隱式地依賴于它的某些行為。除了我們的  api 用戶在依賴的 Nginx 行為這塊,在轉到 Envoy 后遇到了一些行為不一致的地方以外,我們在使用 Envoy 和它提供的類庫的過程中還發現了許多  bug。在社區的幫助下,所有這些問題都很快得到了解決并且反饋到了上游。

下面只是其中一些“異常的”/沒有出現在 RFC 里的行為的摘要:

  • 合并 URL 路徑里的斜杠。URL 規范化及斜杠合并在 Web 代理里是一項非常常見的功能。Nginx 默認啟用了斜杠歸一和斜線合并,但是 Envoy  不支持后者。我們向上游提交了一個補丁,添加了該項功能,并允許用戶通過merge_slashes 選項選擇性地加入。

  • 虛擬主機名字里的端口。Nginx 允許接收兩種形式的 Host header:要么是 example.com,又或者是  example.com:port。我們有幾個曾經依賴于這一行為的 API 用戶。首先,我們通過在配置里復制虛擬主機(帶和不帶端口)來解決此問題,但是后來我們又在  Envoy 端添加了一個忽略匹配端口的選項:strip_matching_host_port。

  • 傳輸編碼區分大小寫。出于某種不知名的原因,一小部分子集的 API 客戶端使用了  Transfer-Encoding:Chunked(請注意大寫的“C”)header。這在技術上是可行的,因為 RFC7230 聲明了  Transfer-Encoding/TE header 不區分大小寫。修復辦法也很簡單,然后也已經提交到了上游的 Envoy 。

  • 一個請求里同時帶有 [Content-Length](https://github.com/envoyproxy/envoy/issues/11398)  和 [Transfer-Encoding:chunked](https://github.com/envoyproxy/envoy/issues/11398)  這兩個 header。有些請求在 Nginx 下面是好的,但是遷移到 Envoy 以后就炸了。 RFC7230 有點棘手,通常大家會覺得 Web  服務器的確應該在處理這些請求時報錯,因為它們很可能是“走私的(smuggled)”。另一方面,可能又會有人說,代理應該去掉 Content-Length  header 然后再轉發該請求。對此,我們的做法是:我們擴展了 http-parse 然后讓使用該庫的用戶來自主選擇是否要支持這類請求,然后目前我們正在努力為  Envoy 自身引入這項功能的支持。

另外,值得一提的是,我們還遇到了一些常見的配置問題:

  • 熔斷器配置錯誤。根據我們的經驗,如果入站代理采用 Envoy 的話,尤其是工作在 HTTP/1 以及 HTTP/2  這樣的混合環境里,那么熔斷器的配置不當可能會在流量高峰或后端中斷期間導致意外停機。如果不使用 Envoy 作為 mesh  代理,不妨考慮放松這塊的配置。值得一提的是,在默認情況下,Envoy 的熔斷器限制非常嚴格 &mdash;&mdash;&mdash;&mdash; 特別要注意這一點!

  • 緩沖區。 Nginx 允許將磁盤用作請求的緩沖區。這在用戶采用的是無法理解 chunked 傳輸編碼的傳統的 HTTP/1.0  后端的情況下很有用。Nginx 可以將它們先緩沖到磁盤上然后轉換成帶有 Content-Length 的請求。Envoy 有一個 Buffer  過濾器,但是由于無法將數據保存到磁盤,能夠緩沖多少內容受限于我們機器的內存大小。

下一步

  • HTTP/3 的黃金時代即將來臨。幾個主流瀏覽器均已加入了對 HTTP 3.0 的支持(目前由一個參數或者命令行選項來控制是否開啟)。Envoy  也已經在進行實驗性的支持。升級 Linux 內核支持 UDP 加速后,我們會在我們的邊緣環境上進行試驗。

  • 內部基于 xDS 的負載均衡以及異常值檢測。目前,我們正在考慮使用負載上報服務(LRS)和端點發現服務(EDS)的組合作為構建模塊,以便為 Envoy 和  gRPC 創建一套通用的后備型、負載感知式的負載均衡。

  • 基于 WASM 的 Envoy 擴展。等到 Golang 的 proxy-wasm SDK 可用時,我們便可以開始在 Go 里面編寫 Envoy  的擴展,這將使我們能夠訪問各種內部的 Golang 庫。

  • 替換 Bandaid。將所有的 Dropbox 代理層統一成一個單個的數據平面聽上去很有吸引力。為此,我們需要將許多 Bandaid  功能(尤其是在負載均衡方面)遷移到 Envoy。這還有很長的路要走,但這是我們目前的計劃。

  • Envoy 移動端。最終,我們想看看能否將 Envoy  用于移動端的應用程序。站在接入層的角度,在所有的移動端平臺上支持一套具有統一的監控和現代化功能(HTTP/3,gRPC,TLS1.3  等)的單個服務棧非常耀眼。 

感謝你能夠認真閱讀完這篇文章,希望小編分享的“Dropbox是怎么將接入層從Nginx遷移到Envoy的”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

雅江县| 揭西县| 宣汉县| 渝中区| 锦州市| 教育| 鲁甸县| 苗栗县| 拉萨市| 双峰县| 钦州市| 莱州市| 北川| 红河县| 永顺县| 伊川县| 蒲城县| 上林县| 东港市| 临高县| 津南区| 湖口县| 富蕴县| 洪泽县| 唐河县| 安阳县| 景德镇市| 浦城县| 馆陶县| 务川| 临洮县| 邵阳县| 武平县| 玉龙| 东方市| 法库县| 湟源县| 五常市| 兰州市| 东乡县| 德江县|