Subscribe to receive notifications of new posts:

如何構建 Pingora 以將 Cloudflare 連線至網際網路代理

09/14/2022

14 min read
How we built Pingora, the proxy that connects Cloudflare to the Internet

介紹

今天,我們滿懷欣喜之情來談論 Pingora,這是我們使用 Rust 在內部構建的全新 HTTP 代理程式 ,其每天可處理超過 1 萬億個請求,能提高我們的效能,並為 Cloudflare 客戶實現眾多新功能,同時只需之前代理基礎結構三分之一的 CPU 和記憶體資源。

隨著 Cloudflare 的擴展,我們已經因發展而不再需要 NGINX。多年來,NGINX 一直備受追捧,但隨著時間的推移,其在規模上存在局限性,這意味著需要構建新的代理來實現。我們無法再獲得所需的效能,NGINX 也不具備極其複雜的環境所需的各項功能。

許多 Cloudflare 客戶和使用者將 Cloudflare 全球網路用作 HTTP 用戶端(如 Web 瀏覽器、應用程式、IoT 裝置等)與伺服器之間的代理。過去,關於瀏覽器和其他使用者代理程式如何連線至我們的網路,我們已經討論了很多,並且已經開發了許多技術並實施了新的通訊協定(請參閱 QUICHTTP2 最佳化),以使該連線網段更具效率。

如今,我們專注於同等事項的另一部分:代理傳送我們的網路與網際網路伺服器間流量的服務。此代理服務為我們的 CDN、Workers 擷取、Tunnel、Stream、R2,以及許多其他功能和產品提供支援。

我們來深入瞭解我們為什麼選擇取代舊式服務,以及如何開發 Pingora,這是我們針對 Cloudflare 的客戶使用案例和規模設計的全新系統。

為什麼要構建另一個代理

多年來,NGINX 的使用面臨局限性。對於某些局限性,我們已最佳化或解決。但其他一些則更難以克服。

架構局限性會損害效能

NGINX 工作者(處理序)架構在我們的使用案例中具有操作上的不足,這會損害效能和效率。

首先,在 NGINX 中,每個請求只能由單一工作者提供服務。這會導致所有 CPU 內核間的負載不平衡,進而減慢速度

由於這種請求-處理序固定效應,執行 CPU 密集型封鎖 IO 任務的請求可能會減慢其他請求的速度。正如這些部落格文章所證實的那樣,我們花費了大量時間來解決這些問題。

對於我們的使用案例而言,最關鍵的問題是連線重複使用表現較差。機器與原始伺服器建立 TCP 連線,以代理傳送 HTTP 請求。連線重複使用可透過重複使用連線集區中之前建立的連線,跳過新連線所需的 TCP 和 TLS 握手,來加速請求的 TTFB(第一個位元組接收時間)。

然而, NGINX 連線集區會針對每個工作者。當請求登陸某個工作者時,它只能重複使用該工作者中的連線。若新增更多 NGINX 工作者進行擴展,則連線重複使用率會變得更糟,因為連線分散在所有處理序更孤立的集區中。這會導致 TTFB 速度變慢,需要維護的連線也會更多,進而消耗我們和我們客戶的資源(和資金)。

正如如過去的部落格文章中所述,我們提供了其中一些問題的因應措施。但如果能解決根本問題:工作者/處理序模型,這些問題將迎刃而解。

新增某些類型的功能困難重重

NGINX 是非常好的 Web 伺服器、負載平衡器或簡單的閘道。但 Cloudflare 所做的遠不止於此。我們曾在 NGINX 的基礎上構建需要的所有功能,這並非易事,同時盡量不與 NGINX 上游程式碼庫產生太大差異。

例如,在請求重試/容錯移轉時,我們有時希望將請求傳送至具有不同請求標頭集的不同原始伺服器。但 NGINX 不允許這樣操作。在這種情況下,我們要花費時間和精力來解決 NGINX 因應條件約束。

與此同時,我們必須使用的程式設計語言並沒有幫助緩解困難。NGINX 純粹採用 C 語言,這在設計上並非記憶體安全。使用此類第三方程式碼庫非常容易出錯。即使對於經驗豐富的工程師來說,也很容易遇到記憶體安全問題,而我們希望盡可能避免這些問題。

我們使用另一種語言 Lua 來作為 C 語言的補充。其風險更小,但效能也更低。此外, 在處理複雜的 Lua 程式碼和業務邏輯時,我們經常發現自己缺少靜態類型

而且,NGINX 社群不太活躍,開發往往「閉門造車」。

決定自己開闢道路

在過去幾年中,隨著我們不斷擴大客戶群和功能集,我們對三種選擇持續進行評估:

  1. 繼續投入 NGINX 並可能建立分支,使其 100% 符合我們的需求。我們擁有所需的專業知識,但考慮到上述架構局限性,這需要付出巨大的努力,才能以完全支援我們需求的方式重建。
  2. 遷移至另一個第三方代理程式碼庫。肯定有很好的專案,例如 envoy其他方案。但這條路徑意味著可能會在幾年內重複同樣的迴圈。
  3. 從頭開始構建內部平台和架構。這種選擇需要在工程工作方面進行最大的前期投資。

在過去幾年中,我們每個季度都會評估每一種選擇。沒有顯而易見的公式來判斷哪種選擇最好。幾年來,我們不斷走阻力最小的路徑,繼續增強 NGINX。然而,在某些時候,構建專屬代理的投資報酬率似乎很值得。我們呼籲從頭開始構建代理,並開始設計我們夢想中的代理應用程式。

Pingora 專案

設計決策

為了讓代理能夠針對每秒數百萬個請求,快速、高效、安全地提供服務,我們首先必須做出一些重要的設計決策。

我們選擇 Rust 作為該專案的語言,因為它能夠以記憶體安全的方式,來執行 C 語言可執行的操作,而不會影響效能。

雖然有一些絕佳的現成第三方 HTTP 程式庫,如 hyper,但我們選擇構建自己的專屬程式庫,因為我們希望最大限度地提高處理 HTTP 流量的靈活性,並確保可以按照自己的節奏來進行創新。

在 Cloudflare,我們需要處理整個網際網路上的流量。還需要支援種種稀奇古怪、不符合 RFC 的 HTTP 流量。這是 HTTP 社群和 Web 中常見的困境,即如何在嚴格遵循 HTTP 規範的同時,適應潛在舊式用戶端或伺服器廣泛生態系統的細微差別,這兩者之間存在矛盾。選擇哪一方面都可能十分棘手。

在 RFC 9110 中,HTTP 狀態代碼被定義為一個三位整數,通常應在 100 至 599 的區間。Hyper 就是這樣一種實作。不過,許多伺服器也支援使用介於 599 和 999 之間的狀態代碼。https://github.com/hyperium/http/issues/144這種衝突曾引發過激烈的爭論。雖然 hyper 團隊最終接受了這一變更,但他們也有充分的理由拒絕此類要求,而這只是我們需要支援的眾多不合規行為的冰山一角。

為了滿足 Cloudflare 在 HTTP 生態系統中佔主導地位的要求,我們需要一個強大、寬鬆、可定製的 HTTP 程式庫,以適應網際網路上的狂野法則,並支援種種不合規的使用案例。保證這一點的最佳方法是實作內部代理。

下一項設計決策圍繞工作負載排程系統。我們選擇了多執行緒,而非多處理序,以便輕鬆共用資源,尤其是連線集區。我們還決定,需要用竊取機制來避免上述某些類別的效能問題。事實證明,Tokio 非同步執行時間非常符合我們的需求。

最後,我們希望該專案直觀且對開發人員友好。我們構建的並非最終產品,而應當是可擴展的平台,允許在其上構建更多功能。我們決定實作一個類似於 NGINX/OpenResty、基於「請求生命週期」事件的可程式設計介面。例如,「請求篩選條件」階段允許開發人員在收到請求標頭時執行程式碼,進而修改或拒絕請求。透過這種設計,我們就能清晰地將業務邏輯和一般代理邏輯分開。之前使用 NGINX 的開發人員也能輕鬆轉向 Pingora 並迅速提高工作效率。

Pingora 在生產中速度更快

我們快進到現在。Pingora 在處理幾乎所有需要與原始伺服器互動的 HTTP 請求(例如,快取未命中的情況),並且我們在此期間收集了大量效能資料。

首先,我們來看看 Pingora 如何加速客戶流量。Pingora 上的總體流量顯示,TTFB 中位數減少了 5 毫秒,第 95 百分位減少了 80 毫秒。這並不是因為我們執行程式碼的速度更快。畢竟舊式服務也能處理亞毫秒級的請求。

成本節省源自我們的新架構,它可以跨所有執行緒間共用連線。這意味著連線重複使用率提升,進而在 TCP 和 TLS 握手上所花費的時間相應地縮短。

相較於舊服務,Pingora 將所有客戶的每秒新連線用量降低至三分之一。針對其中一個主要客戶,其連線重複使用率從 87.1% 提高至 99.92%,從而將與其原本的新連線數減少了 160倍。以下數據可直觀地呈現這種變化︰轉向 Pingora 之後,我們每天能夠為客戶和使用者節省 434 年的握手時間。

更多功能

有了工程師熟悉的開發人員友好型介面,又消除了之前的條件約束,這讓我們能夠更快地開發出更多功能。憑藉新通訊協定等核心功能,我們可以為客戶提供更多產品構建區塊。

例如,我們能夠在無重大障礙的情況下,向 Pingora 新增 HTTP/2 上游支援。這讓我們很快就能為客戶提供 gRPC。要將這項功能新增至 NGINX,不僅涉及的工程量更大,並且可能無法實現

最近,我們發佈了 Cache Reserve,Pingora 在其中使用 R2 儲存作為快取層。隨著我們向 Pingora 新增更多功能,我們能夠提供各種開創性的新產品。

更有效率

在生產環境中,面對同等流量的負載,相較於舊服務,Pingora 的 CPU 消耗量減少了約 70%,記憶體消耗量減少了 67%。成本節省源自以下幾大因素。

相較於舊的 Lua 程式碼,我們的 Rust 程式碼執行效率更高。最重要的是,二者在架構上也存在效率差異。例如,在 NGINX/OpenResty 中,當 Lua 程式碼想要存取 HTTP 標頭時,必須從 NGINX C 結構中讀取、分配一個 Lua 字串,然後將其複製到 Lua 字串。之後,Lua 還必須對新字串進行垃圾回收。而 Pingora 能直接執行字串存取。

多執行緒模型也讓跨請求共用資料變得更加高效。NGINX 雖然也提供記憶體共用,但由於實作局限性,每次共用記憶體存取都必須使用互斥鎖,而且只能將字串和數字放入共用記憶體中。在 Pingora 中,大多數共用項都能透過原子引用計數器後的共用引用直接存取。

如上所述,實現 CPU 節省的另一個重要因素是建立更少的新連線。Pingora 可透過已建立的連線來傳送和接收資料,不會造成高昂 TLS 握手成本。

更安全

快速、安全地發佈功能絕非易事,在我們這樣龐大的規模下更是困難重重。在分散式環境中每秒要處理數百萬個請求,很難預測可能會發生的每種極端情況。模糊測試和分析的緩解作用也有限。Rust 的記憶體安全語義保護挺身而出,在保護我們免受未定義行為影響的同時,也讓我們堅信自己的服務將會正常運作。

有了這些保證,我們可以更多地關注服務變更將如何與其他服務或客戶來源互動。我們還能以更快的節奏開發功能,而不會因記憶體安全和難以診斷的崩潰而受到拖累。

當崩潰確實發生時,工程師需要花時間診斷崩潰如何發生,以及導致崩潰的原因。自 Pingora 運作以來,我們已處理數百萬億個請求,但還沒有遇到過源於服務代碼的崩潰問題。

實際上,Pingora 崩潰非常罕見,每次出現的問題都跟 Pingora 自身沒什麼關係。最近,我們在服務崩潰後不久發現了一個內核錯誤。我們還在幾部機器上發現了硬體問題,而在過去,要排除由軟體引起的罕見記憶體錯誤,即使進行重大偵錯也無法實現。

結論

總而言之,我們已構建了一個更快、更高效和更通用的內部代理,並將其作為我們目前和未來產品的平台。

我們將重新將視線更多地集中到技術細節,包括我們面臨的問題、我們運用的最佳化、我們從構建 Pingora 中學到的經驗教訓,以及將其推廣到網際網路的重要部分。我們還將帶著開放原始碼方案再次回歸。

Pingora 是我們重寫系統的最新嘗試,但絕不會是最後一次。它也只是我們系統重新架構的其中一個構建區塊。有興趣加入我們協助打造更完善的網際網路嗎? 我們的工程團隊正在招賢納士

We protect entire corporate networks, help customers build Internet-scale applications efficiently, accelerate any website or Internet application, ward off DDoS attacks, keep hackers at bay, and can help you on your journey to Zero Trust.

Visit 1.1.1.1 from any device to get started with our free app that makes your Internet faster and safer.

To learn more about our mission to help build a better Internet, start here. If you're looking for a new career direction, check out our open positions.
繁體中文Rust (TW)NGINX (TW)Performance (TW)

Follow on X

Cloudflare|@cloudflare

Related posts