揭祕 Uber API 網關的架構

作者 | Uber 官方博客

譯者 | 平川

策劃 | 萬佳

近年來,API 網關成了微服務架構中不可或缺的一部分。API 網關爲 Uber 所有的應用程序提供一個統一入口,並提供了一個從後端微服務訪問數據、邏輯或功能的接口。同時,它還提供了一個集中的地方來實現許多高級職責,包括路由、協議轉換、速率限制、負載削減、豐富頭信息並傳播、數據中心親緣性限定、安全審計、用戶訪問阻塞、移動客戶端生成等。

在本文中,我們將更深入地研究自助式 API 網關係統的技術組件。

在最抽象的層面,網關是通過 API 提供數據服務的另一種服務。網關有多種形式,覆蓋範圍很廣,從作爲 API 網關的低級負載均衡器,到功能非常豐富的應用程序級負載均衡器(操作 API 中的請求和響應負載)。在 Uber,我們開發了一個功能豐富的 API 網關,能夠跨多個協議對輸入和輸出數據的有效載荷進行復雜的操作。

1API 管理

一個功能豐富的 App 是通過與衆多提供不同功能的後端服務交互來實現的。所有這些交互都要經過一個通用的應用網關層。API 管理指的是這些網關 API 的創建、編輯、刪除和版本控制。

工程師在 UI 中配置 API 的參數,並將功能性的 API 發佈到互聯網上供所有 Uber App 消費。配置管理着 API 的行爲:路徑、請求數據類型、響應類型、允許的最大調用數、允許的 App、通信協議、要調用的特定微服務、允許的頭、可觀察性、字段映射驗證等等。

一旦配置發佈,網關基礎設施就會將這些配置轉換爲有效的功能性 API,服務於我們的應用流量。網關基礎設施還爲使用這些 API 的 App 生成客戶端 SDK。

與網關係統的所有交互都通過 UI 發生,UI 會引導用戶一步一步地完成創建端點的過程。UI 簡化了流程,並對 API 的各個方面做了各種驗證。此外,這也是配置請求超時、監控和告警的地方。

管理系統提供了一些輔助功能,比如新的配置更改發佈前的審查門,以及存儲會話用於共享或恢復 API 管理。以下截圖是可用於添加中間件的 UI 步驟的概覽:

2 請求生命週期中的組件

爲了說明網關的各種組件,瞭解單個請求如何通過網關運行時是很重要的。傳入請求包含一個路徑,該路徑映射到爲其提供服務的處理程序。在請求的生命週期中,它流經以下組件:協議管理器、中間件、數據驗證、處理程序和後端客戶端。請求生命週期中的所有組件被實現爲一個棧。

下面詳細介紹了每個組件,它們在請求對象進入時對其進行操作,而相同的組件在響應對象傳出時以相反的順序運行。

協議管理器 是棧的第一層。它包含網關支持的每種協議的反序列化器和序列化器。這一層提供了實現 API 的能力,它可以接收相關協議的任何類型的有效負載,包括 JSON、Thrift 或 Protobuf。它還可以方便地接收傳入的 JSON 請求,並使用原編碼的響應進行應答。

中間件層 是在調用端點處理程序之前實現可組合邏輯的抽象。中間件實現了橫切關注點,如身份驗證、授權、速率限制等。每個端點可以選擇配置一個或多箇中間件。除了可選中間件之外,該平臺還包括一組必備的會針對每個請求執行的中間件。一箇中間件不需要同時實現 requestMiddleware 和 responseMiddleware 方法。如果中間件執行失敗,調用將使棧的其餘部分短路,來自中間件的響應將返回給調用者。在某些情況下,中間件可能沒有操作,這取決於請求上下文。

端點處理程序層 負責請求驗證、有效負載轉換以及將端點請求對象轉換爲客戶端請求對象。當操作響應對象時,endpointHandler 將後端服務響應轉換爲端點響應,對響應對象執行某些轉換,基於模式進行響應驗證並序列化。

客戶端 向後端服務發送請求。客戶端是協議感知的,根據配置過程中選擇的協議生成。用戶可以配置客戶端的內部功能,如請求和響應轉換、模式驗證、斷路和重試、超時和截止日期管理以及錯誤處理。

3 配置組件

協議管理器、中間件、處理程序和客戶端有許多行爲可以通過配置控制。管理 API 的用戶不需要修改任何代碼,而只要修改配置,就可以決定網關上端點的預期行爲。爲了便於配置,這些是通過 UI 進行管理的,其後臺有一個 Git 存儲庫。

每個組件的配置都是從 Thrift 和 / 或 YAML 文件中獲取。YAML 文件提供了組件信息,並充當它們之間的粘合劑。Thrift 文件定義有效負載和協議語義。

網關 thrift 文件大量使用了 thrift IDL 中的註解特性,以便爲各種特性和協議提供唯一事實來源。在下面的小節中,我們將深入研究每個組件的配置。

https://thriftrw.readthedocs.io/en/latest/api.html#thriftrw.idl.Annotation?fileGuid=SmCGZSKhUZsFEdLd

 協議管理器

協議管理器需要理解請求協議上下文中數據的格式和類型。響應也應該知道類似的參數。

下面三行 YAML 配置提供了協議類型、Thrift 文件路徑和協議管理器用於處理傳入請求的方法:

上面的配置表明,新 API 的類型是 “HTTP” 協議,關於模式和協議的所有其他細節都在 apiSample.thrift 文件中提供。

Thrift 文件 apiSample.thrift 功能豐富,描述了 JSON 請求和響應有效負載的數據類型、HTTP 路徑和 HTTP 謂詞。HTTP 協議是在 Thrift 模式中使用 Thrift 註解特性定義的。

並非所有 API 調用都會成功。下面的示例模式提供了從處理程序到適當的 HTTP 協議的錯誤響應。這是通過如下所示的註解來完成的:

還有許多其他註解可以幫助協議管理器使用 thrift 註解管理 HTTP 請求的行爲。

 中間件

中間件是棧中最靈活、功能最豐富的組件。它允許網關平臺向 API 網關用戶公開更高階的特性。我們將在解鎖特性一節中詳細介紹中間件支持的特性。在這裏,我們將重點關注 YAML 文件中的中間件配置。

在上面的配置中,身份驗證中間件被添加到 API。身份驗證中間件將從 header.x-user-uuid 的值接收配置的路徑參數。上面配置的第二個中間件是 transformRequest 中間件,它負責將 region 從傳入請求複製到後端服務調用中的 regionID。在開發新的中間件時,它爲 API 開發人員需要提供的所有可配置參數定義了一個模式。

 處理程序

支持處理程序的主要配置是以驗證和傳入請求到後端客戶端請求參數的映射爲中心。

上面的配置提供了處理程序需要的輸入,用於識別請求應該映射到哪個後端客戶端。如果傳入請求字段與後端服務完全匹配,那麼上面的配置就足夠了。如果字段的名稱不同,則必須使用 transformRequest 中間件來映射它們。

 客戶端

後端客戶端的配置分爲 YAML 文件和 thrift 文件。在下面的示例中有一個使用 TChannel 協議的新的後端服務,該服務的請求和響應是在 backendSample.thrift 文件中定義的,它有兩個可以調用的方法。

https://github.com/uber/tchannel?fileGuid=SmCGZSKhUZsFEdLd

再次提醒下,方法 Backend::method 也可以是真正的 HTTP API,藉助註解,使用與 Thrift 規範類似的路徑 /Backend/method 表示。

4 可運行的工件

上一節中描述的所有組件的 YAML 和 Thrift 配置對於完整描述一個 API 配置是必需的。自助服務網關負責確保這些組件配置一起提供一個網關運行時。

網關有兩種類型:一種接收配置,並基於配置動態地提供 API(很像 Kong、Tyk 和反向代理 Envoy、Nginx);另一種基於輸入配置使用代碼生成步驟生成一個構建工件。在 Uber,我們選擇了後者,即使用代碼生成方法來創建一個可運行的構建工件。

生成模式對象:所有模式文件都通過處理器運行,輸出 thriftrw 和 protoc 的原生 Go 語言代碼。這是序列化 / 反序列化和客戶端接口代碼生成所需要的。

https://github.com/thriftrw?fileGuid=SmCGZSKhUZsFEdLd

生成自定義序列化:移動應用程序的 API 契約需要自定義與 i64、枚舉類型和多個協議相關的序列化。

依賴關係的 DAG:端點、後端客戶端和中間件的代碼是靜態生成的。代碼生成存在固有的依賴性。客戶端是獨立的,可以立即生成。中間件的功能可能依賴於零個或多個客戶端。端點可能依賴於零個或多箇中間件,以及零個或一個客戶端。這個 DAG(有向無環圖)是在構建時解析的。

由於客戶端是獨立於端點生成的,所以端點可以是 HTTP,而後端服務可以是 gRPC。綁定在邊緣網關構建這一步完成。

API 生成:在最後一步中,對 DAG 進行迭代以生成所有端點。單個生成步驟如下:加載模板,將端點請求生成到客戶端請求映射,反之亦然,注入依賴關係,並使用請求 - 響應轉換來還原(hydrate)idl 對象。

代碼生成的整個工作被抽象爲一個 Uber OSS 庫 Zanzibar。

https://github.com/uber/zanzibar?fileGuid=SmCGZSKhUZsFEdLd

5 解鎖特性

集中式系統的一個優點是構建的特性可以使所有在線用戶受益。藉助像 Edge Gateway 這樣功能豐富的網關,我們有多種途徑可以用來構建特性,供所有訪問 Uber 內部服務的 API 使用。

以下是一些已經開發出來的特性,以及一些仍在開發中的特性。

 審計管道

Edge Gateway 會生成包含豐富元數據的訪問日誌,並將其持久化以供審計。保留所有產品所有 API 訪問模式的審計記錄至關重要。當有人試圖使用自動化系統惡意訪問我們的 API 時,它讓我們可以進行安全審計,並幫助我們構建一個涵蓋各種產品的概要文件(跨版本、地理位置和應用程序)。

該管道讓我們可以跨特定的 SDK 版本、應用程序、地理位置或互聯網提供商快速捕獲 Bug、問題和異常。我們的所有應用程序都啓用了審計管道。

 身份驗證

每個外部 API 請求都需要 Authenticated(AuthN)和 / 或 Authorized(AuthZ)。該平臺爲 AuthX 提供了幾個可重用的實現,用戶可以從他們的端點中選擇它們作爲中間件。這使得用戶可以不必關注這些 AuthN/AuthZ 如何實現,並確保一個端點使用至少一個預備好的實現。平臺所有者可以對這些實現進行無縫更新,並自動應用於所有端點。

 斷路器

每個用於調用後端服務的客戶端都包含一個斷路器。當後端服務延遲或錯誤率增加(可配置)時,斷路器將啓動,以防出現任何級聯中斷。這也爲恢復已經惡化的服務提供了空間。

 速率限制

終端所有者可以選擇對 API 進行速率限制。在提供的實現中,有一部分例子是基於 userID、用戶代理、IP、請求中某些屬性的組合進行速率限制。也可以根據路徑 / 查詢參數、頭或正文中的特定字段強制進行限制。這讓我們可以提供比簡單的用戶級 API 訪問更細粒度的應用程序可感知的限流策略。每個端點都可以動態地獨立分配配額,而不需要重新部署。

 文檔

所有 YAML 和 Thrift 中的配置完整地描述了一個 API。這提供了一個選項,讓我們可以以一致的方式爲所有網關 API 自動生成文檔。

 移動客戶端生成

Uber 的所有移動應用程序都基於 Thrift IDL 生成服務和模型,從而實現與服務器的交互。CI 作業從網關獲取所有端點 IDL,併爲各種模型運行自定義代碼生成。移動代碼生成還依賴於各種自定義 Thrift 註解,如異常狀態代碼、URL 路徑和 HTTP 方法。一個進行生成代碼審查的 CI 作業可以防止對端點模式做任何向後不兼容的更改。

 響應字段裁剪

因爲 API 的創建很容易,而且多個端點可以由相同的底層客戶端服務提供支撐。我們在創建 API 時,可以細粒度地選擇用戶體驗所需的特定字段,而不是使用完整的後端響應進行響應。

 數據中心親緣性

目前,擁有冗餘數據中心和區域是大型 Web 公司實際採用的架構。屬於不同業務單元或域的 API 託管在網關上,每個業務單元可以定義跨多個數據中心的工作負載分片。Edge Gateway 提供了一個緩存,業務單元可以向其中寫入數據,以配置與適當的數據中心相關聯的用戶、地區或版本。網關將遵照數據中心關聯信息重新路由來自特定用戶、設備或應用程序的傳入 API。

 短期用戶禁用

賬戶禁用是對付惡意行爲者的方法。對於暫時濫用系統的用戶,網關提供了一箇中心位置,用於在短時間內阻止特定用戶對 API 的訪問。這種方法類似於數據中心親緣性,網關可以提供一個外部緩存來存儲被阻塞的用戶(有一個 TTL)。欺詐和安全系統可以提供用戶、應用程序版本或其他標識符作爲阻塞依據。Edge Gateway 將確保這些短期禁令得以執行,以保護我們的用戶。

6 挑戰和教訓

在網關的開發過程中,我們不得不做出多方面的設計選擇。有些選擇讓我們獲得了非常令人興奮的結果,而有些卻沒有提供預期的投資回報。下面我們將簡要介紹幾項挑戰。

 語言

在開發網關時,我們選擇的語言是 Go 和 Java。我們的上一代網關使用的是 Node.js。雖然這種語言非常適合構建 IO 密集型網關層,但我們決定與 Uber 語言平臺團隊支持的語言保持一致。Go 提供了顯著的性能改進。在構建期間,泛型的缺乏導致生成了大量的代碼,達到了 Go 鏈接器的極限。在二進制編譯期間,我們必須關閉符號表和調試信息。在 Go(但在 Thrift 中不是)中,像 ID、HTTP 和保留關鍵字這樣的語言命名約定會導致失敗,以致將內部實現細節暴露給了最終用戶。

 序列化格式

我們的網關的協議管理器能夠實現多種協議。這個特性帶來了複雜的兼容性問題,比如 JSON 模式與 Thrift 模式中,表示 Union、Set、List 和 Map 的數據類型不匹配。我們必須自定義一些約定來實現映射。

 配置存儲

如上所述,用戶配置存儲在 Git 中。然而本質上,有些配置是動態的,比如 API 速率限制。以前,更改需要代碼生成和部署。這非常耗時,因此,我們現在將用戶配置的動態部分存儲在配置存儲中。

 網關 UI

在網關 UI 中開發單個 API 很容易,但開發批量編輯流就比較困難了。當 Thrift 文件引用其他 Thrift 文件並且嵌套可以任意深時,尤其如此。一旦用戶提供了配置並由構建系統接管,而構建系統又獨立於 UI 而發展,將構建失敗呈現到 UI 就變得非常困難。爲了顯示錯誤,在它們之間保持一致的契約至關重要。

 瞭解有效載荷

在開發大多數網關特性時,不需要對傳入或傳出的有效載荷進行反序列化。我們的協議互操作性用例迫使我們對有效載荷進行反序列化。這增加了構建系統的複雜性,也影響了運行時的性能。如果後端協議和移動協議相同,那麼限制網關只訪問協議謂詞和消息頭,而不反序列化消息體可能會有好處。然而,這會限制一些複雜的網關功能。

一個功能豐富的網關,就像我們描述的這個,是一項複雜的工作。如果你有興趣遵循相同的路徑,Zanzibar 可以提供一個可擴展的模塊,你可以由此入手。在 Uber,我們正基於 Envoy 開發一種 API 網關運行時,用於從應用程序到後端服務的 gRPC 請求,我們的自助服務 UI 在用戶體驗上沒有很大的變化。

https://www.envoyproxy.io/?fileGuid=SmCGZSKhUZsFEdLd

原文鏈接:

https://eng.uber.com/architecture-api-gateway/?fileGuid=SmCGZSKhUZsFEdLd


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PitW9_NPtR3Ibdy3ac0wcA