這篇文章已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
為 Envoy v2 建置 Kubernetes 邊緣 (Ingress) 控制平面
Kubernetes 已成為基於容器的微服務應用程式的事實標準執行環境,但僅此協作框架並未提供運行分散式系統所需的所有基礎架構。微服務通常透過第 7 層協定 (例如 HTTP、gRPC 或 WebSockets) 進行通訊,因此,能夠在此層進行路由決策、操作協定中繼資料和觀察至關重要。然而,傳統的負載平衡器和邊緣 Proxy 主要專注於 L3/4 流量。這就是 Envoy Proxy 發揮作用的地方。
Envoy proxy 由 Lyft 工程團隊從頭開始設計為 通用資料平面,適用於當今以 L7 為中心的分散式世界,廣泛支援 L7 協定、用於管理其組態的即時 API、一流的可觀察性以及在小記憶體佔用空間內的高效能。然而,Envoy 廣泛的功能集和操作彈性也使其組態非常複雜——從其豐富但冗長的 控制平面 語法中可見一斑。
透過開源 Ambassador API Gateway,我們希望解決創建新控制平面的挑戰,該控制平面專注於在 Kubernetes 叢集中將 Envoy 部署為面向前方的邊緣 Proxy 的使用案例,並以符合 Kubernetes 運營商習慣的方式進行。在本文中,我們將逐步介紹 Ambassador 設計的兩個主要迭代,以及我們如何將 Ambassador 與 Kubernetes 整合。
2019 年之前的 Ambassador:Envoy v1 API、Jinja 範本檔案和熱重新啟動
Ambassador 本身作為 Kubernetes 服務部署在容器中,並使用新增至 Kubernetes 服務的註解作為其 核心組態模型。這種方法 使應用程式開發人員能夠管理路由 作為 Kubernetes 服務定義的一部分。我們明確決定採用這種途徑,因為當前 Ingress API 規範 中的 限制,並且我們喜歡擴展 Kubernetes 服務的簡潔性,而不是引入另一種自訂資源類型。Ambassador 註解的範例可在此處查看
kind: Service
apiVersion: v1
metadata:
name: my-service
annotations:
getambassador.io/config: |
---
apiVersion: ambassador/v0
kind: Mapping
name: my_service_mapping
prefix: /my-service/
service: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
將這個簡單的 Ambassador 註解組態轉換為有效的 Envoy v1 組態並非易事。根據設計,Ambassador 的組態並非基於與 Envoy 組態相同的概念模型——我們刻意希望聚合和簡化操作與組態。因此,在一組概念與另一組概念之間進行轉換涉及 Ambassador 內相當多的邏輯。
在 Ambassador 的第一個迭代中,我們建立了一個基於 Python 的服務,該服務監控 Kubernetes API 以了解服務物件的變更。當偵測到新的或更新的 Ambassador 註解時,這些註解會從 Ambassador 語法轉換為體現我們核心組態模型和概念的中間表示 (IR)。接下來,Ambassador 將此 IR 轉換為代表性的 Envoy 組態,該組態作為檔案儲存在與正在運行的 Ambassador k8s 服務相關聯的 Pod 中。然後,Ambassador「熱重新啟動」Ambassador Pod 中運行的 Envoy 程序,這觸發了新組態的載入。
這個初始實作有許多好處。所涉及的機制從根本上來說很簡單,Ambassador 組態到 Envoy 組態的轉換是可靠的,並且基於檔案的熱重新啟動與 Envoy 的整合是可靠的。
然而,此版本的 Ambassador 也存在顯著的挑戰。首先,儘管熱重新啟動對於我們大多數客戶的使用案例都有效,但它速度不快,並且一些客戶(尤其是那些具有龐大應用程式部署的客戶)發現它限制了他們可以變更組態的頻率。熱重新啟動也可能丟棄連線,尤其是像 WebSockets 或 gRPC 串流這樣的長連線。
然而,更關鍵的是,IR 的第一個實作允許快速原型設計,但它非常原始,以至於證明很難做出實質性的變更。雖然這從一開始就是一個痛點,但隨著 Envoy 轉向 Envoy v2 API,它成為一個關鍵問題。很明顯,v2 API 將為 Ambassador 提供許多好處——正如 Matt Klein 在他的部落格文章「通用資料平面 API」中概述的那樣——包括存取新功能和解決上述連線丟失問題的方案,但也很明顯,現有的 IR 實作無法實現飛躍。
Ambassador >= v0.50:Envoy v2 API (ADS)、使用 KAT 進行測試和 Golang
與 Ambassador 社群 諮詢後,Datawire 團隊在 2018 年進行了 Ambassador 內部結構的重新設計。這項重新設計由兩個關鍵目標驅動。首先,我們希望整合 Envoy 的 v2 組態格式,這將支援諸如 SNI、速率限制 和 gRPC 身份驗證 API 等功能。其次,由於 Envoy 組態的複雜性日益增加(尤其是在大規模應用程式部署中運作時),我們也希望對 Envoy 組態進行更強大的語義驗證。
初始階段
我們首先按照多通道編譯器的思路重組了 Ambassador 內部結構。類別階層結構更緊密地反映了 Ambassador 組態資源、IR 和 Envoy 組態資源之間關注點的分離。Ambassador 的核心部分也經過重新設計,以促進 Datawire 以外社群的貢獻。我們決定採用這種方法有幾個原因。首先,Envoy Proxy 是一個快速發展的專案,我們意識到我們需要一種方法,讓看似微小的 Envoy 組態變更不會導致 Ambassador 內部需要數天的重新工程。此外,我們希望能夠提供組態的語義驗證。
當我們開始更緊密地使用 Envoy v2 時,很快就發現了一個測試挑戰。隨著 Ambassador 中支援的功能越來越多,Ambassador 在處理不太常見但完全有效的功能組合時出現了越來越多的錯誤。這促使創建了一個新的測試要求,這意味著需要重新設計 Ambassador 的測試套件,以自動管理許多功能組合,而不是依賴人工來單獨編寫每個測試。此外,我們希望測試套件速度快,以便最大限度地提高工程生產力。
因此,作為 Ambassador 重新架構的一部分,我們引入了 Kubernetes 接受測試 (KAT) 框架。KAT 是一個可擴展的測試框架,它
- 將一堆服務(以及 Ambassador)部署到 Kubernetes 叢集
- 針對啟動的 API 運行一系列驗證查詢
- 對這些查詢結果執行一堆斷言
KAT 專為效能而設計——它預先批量處理測試設定,然後使用高效能用戶端非同步地逐步運行步驟 3 中的所有查詢。KAT 中的流量驅動程式使用 Telepresence 在本地運行,這使得調試問題變得更容易。
將 Golang 引入 Ambassador 堆疊
在 KAT 測試框架到位後,我們很快遇到了 Envoy v2 組態和熱重新啟動的一些問題,這為切換到使用 Envoy 的聚合發現服務 (ADS) API 而不是熱重新啟動提供了機會。這完全消除了組態變更時重新啟動的要求,我們發現這可能會導致在高負載或長連線下連線中斷。
然而,當我們考慮轉向 ADS 時,我們面臨一個有趣的問題。ADS 並不像人們想像的那麼簡單:在向 Envoy 發送更新時存在明確的排序依賴性。Envoy 專案有排序邏輯的參考實作,但僅在 Go 和 Java 中,而 Ambassador 主要使用 Python。我們猶豫了一下,並決定最簡單的前進方式是接受我們世界的多語言性質,並在 Go 中完成我們的 ADS 實作。
我們還透過 KAT 發現,我們的測試已經達到 Python 在處理許多網路連線時效能受到限制的程度,因此我們也在此處利用了 Go,主要使用 Go 編寫 KAT 的查詢和後端服務。畢竟,當您已經冒險一試時,再增加一個 Golang 依賴又有什麼關係呢?
憑藉新的測試框架、產生有效 Envoy v2 組態的新 IR 和 ADS,我們認為我們已經完成了 Ambassador 0.50 中的主要架構變更。唉,我們又遇到了一個問題。在 Azure Kubernetes 服務上,Ambassador 註解變更不再被偵測到。
透過與反應迅速的 AKS 工程團隊合作,我們能夠確定問題所在——即,AKS 中的 Kubernetes API 伺服器透過 Proxy 鏈公開,需要用戶端進行更新以了解如何使用 API 伺服器的 FQDN 進行連線,該 FQDN 透過 AKS 中的 mutating webhook 提供。不幸的是,官方 Kubernetes Python 用戶端中不支援此功能,因此這是我們選擇切換到 Go 而不是 Python 的第三個地方。
這引發了一個有趣的問題:「為什麼不拋棄所有 Python 程式碼,而只是用 Go 完全重寫 Ambassador 呢?」這是一個有效的問題。重寫的主要問題是,Ambassador 和 Envoy 在不同的概念層級運作,而不是僅用不同的語法表達相同的概念。確定我們已經在新語言中表達了概念橋樑並非易事,也不是在沒有已經非常出色的測試覆蓋率的情況下可以進行的事情
在這一點上,我們使用 Go 來覆蓋非常特定、良好封裝的功能,這些功能的正確性比我們驗證完整的 Golang 重寫更容易驗證。在未來,誰知道呢?但對於 0.50.0,這種功能劃分讓我們既可以利用 Golang 的優勢,又可以讓我們對 0.50 中已經進行的所有變更保持更大的信心。
經驗教訓
在建構 Ambassador 0.50 的過程中,我們學到了很多。我們的一些主要收穫
- Kubernetes 和 Envoy 是非常強大的框架,但它們也是極其快速發展的目標——有時沒有比閱讀原始程式碼和與維護者交談更好的替代方法(幸運的是,他們都很容易接觸到!)
- Kubernetes/Envoy 生態系統中支援最好的程式庫是用 Go 語言編寫的。雖然我們喜歡 Python,但我們不得不採用 Go,這樣我們就不必被迫自己維護太多元件。
- 重新設計測試工具在有時是推動您的軟體前進所必需的。
- 重新設計測試工具的真正成本通常在於將您的舊測試移植到新的工具實作中。
- 為邊緣 Proxy 使用案例設計(和實作)有效的控制平面一直具有挑戰性,並且來自 Kubernetes、Envoy 和 Ambassador 周圍開源社群的回饋非常有用。
將 Ambassador 遷移到 Envoy v2 組態和 ADS API 是一個漫長而艱難的旅程,需要大量的架構和設計討論以及大量的程式碼編寫,但早期結果的回饋是積極的。Ambassador 0.50 現已上市,因此您可以試運行一下,並在我們的 Slack 頻道 或 Twitter 上與社群分享您的回饋。