本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

為 Envoy v2 構建 Kubernetes 邊緣(Ingress)控制平面

Kubernetes 已成為基於容器的微服務應用程式的事實執行時,但這個編排框架本身並未提供執行分散式系統所需的所有基礎設施。微服務通常透過 HTTP、gRPC 或 WebSocket 等第 7 層協議進行通訊,因此在此層進行路由決策、操作協議元資料和進行觀察的能力至關重要。然而,傳統的負載均衡器和邊緣代理主要關注 L3/4 流量。這就是 Envoy Proxy 發揮作用的地方。

Envoy 代理由 Lyft 工程團隊從頭開始設計,作為適用於當今分散式、以 L7 為中心的世界的通用資料平面,它廣泛支援 L7 協議,提供用於管理其配置的即時 API,一流的可觀測性,以及在小記憶體佔用下的高效能。然而,Envoy 龐大的功能集和操作靈活性也使其配置高度複雜——從其豐富但冗長的控制平面語法中可見一斑。

透過開源的 Ambassador API 閘道器,我們希望解決建立新控制平面的挑戰,該控制平面專注於在 Kubernetes 叢集中以符合 Kubernetes 運營商習慣的方式將 Envoy 部署為面向前方的邊緣代理。在本文中,我們將介紹 Ambassador 設計的兩次重大迭代,以及我們如何將 Ambassador 與 Kubernetes 整合。

Ambassador 2019 年之前:Envoy v1 API、Jinja 模板檔案和熱重啟

Ambassador 本身作為 Kubernetes 服務部署在一個容器中,並使用新增到 Kubernetes Services 的註解作為其核心配置模型。這種方法使應用程式開發人員能夠將路由作為 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 以瞭解 Service 物件的更改。當檢測到新的或更新的 Ambassador 註解時,這些註解從 Ambassador 語法轉換為中間表示 (IR),該 IR 包含了我們的核心配置模型和概念。接下來,Ambassador 將此 IR 轉換為一個代表性的 Envoy 配置,該配置作為檔案儲存在與正在執行的 Ambassador k8s Service 關聯的 Pod 中。然後,Ambassador“熱重啟”在 Ambassador Pod 中執行的 Envoy 程序,這觸發了新配置的載入。

最初的實現帶來了許多好處。所涉及的機制非常簡單,Ambassador 配置到 Envoy 配置的轉換是可靠的,並且基於檔案的 Envoy 熱重啟整合是值得信賴的。

然而,這個版本的 Ambassador 也存在顯著的挑戰。首先,儘管熱重啟對於我們大多數客戶的用例是有效的,但它並不快,一些客戶(特別是那些擁有龐大應用程式部署的客戶)發現它限制了他們更改配置的頻率。熱重啟還會導致連線中斷,特別是像 WebSocket 或 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 等功能,SNI速率限制gRPC 身份驗證 API。其次,由於 Envoy 配置日益複雜(尤其是在大規模應用程式部署中操作時),我們還希望對 Envoy 配置進行更健壯的語義驗證。

初期階段

我們首先按照多遍編譯器的思路重構了 Ambassador 的內部結構。類層次結構更緊密地反映了 Ambassador 配置資源、IR 和 Envoy 配置資源之間的關注點分離。Ambassador 的核心部分也經過重新設計,以促進 Datawire 之外社群的貢獻。我們決定採用這種方法有幾個原因。首先,Envoy Proxy 是一個發展非常快的專案,我們意識到我們需要一種方法,即看似微小的 Envoy 配置更改不會導致 Ambassador 內部進行數天的重新設計。此外,我們希望能夠提供配置的語義驗證。

當我們開始更密切地使用 Envoy v2 時,很快就發現了一個測試挑戰。隨著 Ambassador 支援的功能越來越多,Ambassador 在處理不常見但完全有效的功能組合時出現了越來越多的 bug。這促使我們提出了一個新的測試要求,即 Ambassador 的測試套件需要重新設計,以自動管理許多功能組合,而不是依賴人工逐個編寫每個測試。此外,我們希望測試套件執行速度快,以最大限度地提高工程生產力。

因此,作為 Ambassador 重構的一部分,我們引入了 Kubernetes 驗收測試 (KAT) 框架。KAT 是一個可擴充套件的測試框架,它:

  1. 在 Kubernetes 叢集中部署一組服務(以及 Ambassador)
  2. 針對已啟動的 API 執行一系列驗證查詢
  3. 對這些查詢結果執行一系列斷言

KAT 專為效能而設計——它預先批次設定測試,然後使用高效能客戶端非同步執行步驟 3 中的所有查詢。KAT 中的流量驅動器使用 Telepresence 在本地執行,這使得除錯問題變得更容易。

向 Ambassador 堆疊引入 Golang

隨著 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 Service 上,Ambassador 註解的更改不再被檢測到。

與響應迅速的 AKS 工程團隊合作,我們找到了問題所在——即 AKS 中的 Kubernetes API 伺服器透過代理鏈暴露,這要求客戶端更新以瞭解如何使用 API 伺服器的 FQDN 進行連線,FQDN 透過 AKS 中的變異 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,這樣我們就不必自己維護太多的元件。
  • 重新設計測試工具有時是推動軟體向前發展的必要條件。
  • 重新設計測試工具的真正成本通常在於將舊測試移植到新的工具實現中。
  • 為邊緣代理用例設計(和實現)一個有效的控制平面一直充滿挑戰,而來自 Kubernetes、Envoy 和 Ambassador 開源社群的反饋非常有用。

將 Ambassador 遷移到 Envoy v2 配置和 ADS API 是一段漫長而艱難的旅程,需要大量的架構和設計討論以及大量的編碼,但早期的反饋結果是積極的。Ambassador 0.50 現已釋出,您可以試執行並向社群分享您的反饋意見,可以透過我們的 Slack 頻道Twitter