如何在 Istio 中支持 Dubbo、Thrift、Redis 以及任何七層協議?

趙化冰,騰訊雲高級工程師,Istio Member,ServiceMesher管理委員,Istio 項目貢獻者, Aerika 項目創建者 ,熱衷於開源、網絡和雲計算。目前主要從事服務網格的開源和研發工作。

唐陽,知乎基礎架構工程師。Istio 項目貢獻者,Argo 項目貢獻者,專註於開源,雲原生與微服務。目前負責知乎服務網格的研發工作。

備註:本文根據騰訊雲趙化冰和知乎唐陽在 IstioCon 2021 中的演講 「How to Manage Any Layer-7 Traffic in an Istio Service Mesh?」 整理而成。

大家好,今天我們想和大家分享的主題是如何擴展 Istio 以支持任何七層協議?作為雲原生領域中一個人氣非常高的開源項目, Istio 目前已經基本成為了 Service Mesh 的事實標準。騰訊雲上也提供了基於 Istio 進行增強,和 Istio API 完全兼容的 Service Mesh 管理服務 TCM(Tencent Cloud Mesh),以幫助我們的用戶以較小的遷移成本和維護代價快速利用到 Service Mesh 提供的流量管理和服務治理能力。今天非常高興能夠有這個機會來和大家一起分享一下我們在此過程中的一些經驗。

Service Mesh 提供了一個對應用透明的基礎設施層,可以解決我們在分佈式應用/微服務中遇到的常見挑戰,例如:如何找到服務提供者?如何保證服務之間的通信安全?如何得知服務之間的調用關係?如何進行流量管理如灰度發佈?等等。Service Mesh 的實現方式是伴隨應用部署一個 Sidecar Proxy,該 Sidecar Proxy 會攔截應用的出向和入向流量, 對這些流量進行分析和處理,以達到在不修改應用代碼的情況下對服務進行流量管理、安全加密,遙測數據收集的目的。為了實現這些服務治理能力,Sidecar Proxy 不只需要在 OSI 網絡模型的三、四層上對流量進行處理,更重要的是需要在七層上進行處理。在七層上,Istio 缺省只支持了 HTTP 和 gPRC 兩種協議。但我們在微服務中經常還會使用到的其他七層協議,當將這些微服務應用遷移到 Service Mesh 時,我們希望使用一致的方式對所有的這些七層協議進行統一管理,以充分利用 Service Mesh 基礎設施提供的雲原生能力。

在今天的分享中,我將會介紹幾種將 Istio 流量管理能力擴展到其他七層協議的方法,並對比分析這幾種方法各自的優缺點。我會介紹如何利用 Aeraki 開源項目來在 Istio 中管理任何七層協議,包括 Dubbo、Thrift、Redis 等。為了讓大家了解 Aeraki 是如何工作的,會展示一個採用 Aeraki 實現 Thrift 服務 Traffic Splitting 的例子。來自知乎的唐陽還會為我們展示如何使用 Aeraki 的一些有趣的真實案例。

Service Mesh 中常見的七層協議

如下圖所示,一個典型的微服務應用中通常會使用到這些七層協議:

  • 同步調用:不同服務之間會採用 RPC (遠程方法調用)進行相互調用。常見的 RPC 調用協議包括 gRPC,Thrift,Dubbo,HTTP 也可以看做一種 RPC (只支持 GET/SET/POST 這幾種標準方法) 。一些大的公司為了滿足自己特定業務場景的需求,往往還會採用一些私用的 RPC 協議。
  • 異步消息:除了 RPC 之外,異步消息也是微服務通信的一種常見模式,包括 Kafka,RabbitMQ,ActiveMQ 等。
  • 各種數據庫和緩存系統:例如 Redis, MySQL,MongoDB 等等。

那麼當將這樣一個微服務應用加入到 Service Mesh 以後,我們希望能夠通過 Service Mesh 得到哪些管理能力呢?

理想情況下,我們希望 Service Mesh 能夠管理微服務中用到的所有七層協議的流量,包括 RPC、Messaging、Cache、DB等。例如:

  • 基於請求的負載均衡:可以將來自同一個 TCP 鏈接的多個獨立的請求分發到不同的後端服務器,以實現更智能,更合理的負載均衡。
  • 基於七層 Header 的流量路由:根據七層 Header 中的屬性進行路由,例如根據 Dubbo 請求中的服務名或者 Redis 請求的 Key 進行路由。
  • 對客戶端的請求響應注入延遲或者錯誤,以測試應微服務用的彈性。
  • 提供應用級安全,例如基於 HTTP Header 中的 JWT Token 進行認證,或者對 Redis 服務器進行認證。
  • 請求層面的遙測數據,包括請求成功率、請求耗時、調用跟蹤等等。

要實現以上這些流量管理和服務治理能力,Service Mesh 需要分析和處理 TCP 數據包中的七層協議的 Header。即 Service Mesh 必須具有七層協議的管理能力,而不只是在 TCP 層面上進行處理。

然而在 Istio 中,對於除了 HTTP 和 gRPC 之外的協議,我們只能在 OSI 三到六層對這些協議進行處理。這意味着我們只能基於三層的 IP 地址,四層的 TCP 端口或者六層的 SNI(Server Name Indication)對這些協議進行路由。只能收集到 TCP 層面的指標,例如 TCP 收發包數量或者打開/關閉的 TCP 鏈接數量。只能採用 mTLS 進行鏈路層面的認證和權限控制。換而言之,對於這些協議,我們依然需要在應用代碼中處理流量控制、可觀測性、安全認證這些本應該由 Service Mesh 基礎設施來統一處理的共性問題。這違背了我們將微服務遷移到 Service Mesh 的初衷:將微服務通信和治理的共性問題從應用代碼下沉到 Service Mesh 基礎設施層。

如何擴展 Istio 的協議管理能力?

如果我們希望能夠在 Istio 中管理這些七層協議,我們應該如何實現呢?假設我們有一個 BookInfo 微服務,但該微服務採用了一種稱為 AwesomeRPC 的協議而不是 HTTP 來實現服務間的遠程調用。

我們來看一下如何才能夠在 Istio 中實現 AwesomeRPC 協議的流量管理,例如根據請求 header 中的 user name 字段將來自 ProductPage 的請求路由到不同版本的 Reviews 中,以實現一個灰度發佈的場景。

我們想到的最顯而易見的方式就是直接修改 Istio 代碼。首先我們需要在 Istio 的 VirtualService CRD 中支持 AwesomeRPC 協議。增強後的 VirtualService CRD 如下圖中最左的規則配置所示。 AwesomeRPC 和 HTTP 路由的語義類似,都是根據 Header 中某些屬性的值進行路由。因此我們只需要將 HTTP 協議類型改為 AwesomeRPC,可以直接採用 VirtualService 中的 HTTPRoute 結構來表示 AwesomeRPC 的路由規則。然後我們需要在 Pilot 代碼中根據 AwesomeRPC 的服務定義和 VirtualService 定義的路由規則生成 Envoy 所需的真實配置,並通過 xDS 下發給數據面的 Envoy。當然,以上的前提是我們已經通過 Envoy 的 Filter 擴展機制編寫了 AwesomeRPC 的 Filter 插件,實現 AwesomeRPC 的編解碼,Header 解析,動態路由等數據面所需的功能。

採用這種方式,在 Envoy Filter 已經實現了的情況下,在控制面增加一個新的七層協議的過程是相對比較簡單的。但是由於我們修改了 Istio 的源碼,因此需要自己維護一個 Istio 的私有分支,這導致了額外的維護代價,並且很難跟上 Istio 快速的迭代步伐。

如果不希望維護自己的 Istio 代碼分支,一種可行的替代方式是採用 Istio EnvoyFilter CRD:EnvoyFilter 是 Istio 提供的一種靈活強大的配置機制。我們可以使用 EnvoyFilter為 Pilot 生成的缺省 Envoy 配置打一個補丁,添加、修改或者刪除缺省 Envoy 配置中的部分內容,以按我們的要求修改 Envoy 在 Istio Service Mesh 中的缺省行為。

如下圖所示,由於 Pilot 並不理解 AwesomeRPC 協議,對於 Pilot 來說, AwesomeRPC 服務只是一個 TCP 服務。在 Pilot 生成的缺省配置中,AwesomeRPC 服務對應的 Outbound Listener 的 FilterChain 中採用了一個 TCP Proxy 來處理其流量。我們在 EnvoyFilter 的 Match 部分中選中該 TCP Proxy,並在 Operation 部分將其替換為一個配置了 Traffic Splitting 規則的 AwesomeRPC Filter。Pilot 會根據 EnvoyFilter 修改其生成的缺省 Envoy 配置,然後下發到數據面的 Envoy 上。這樣我們就通過 EnvoyFilter 在 Istio 中實現了對 AwesomeRPC 協議的支持。

下面我們來看一個採用 Thrift 協議的真實案例。Thrift 是 Apache 基金會下一個輕量級、支持多語言的開源 RPC 框架。Envoy 中已經支持 Thrift,但 Istio 中只對 Thrift 提供了有限的支持,並不能實現 Traffic Splitting 等高級流量管理功能。如果我們希望在 Istio 中提供下圖中右下角所示 Thrif 服務的 Traffic Splitting 流量控制,我們可以通過 EnvoyFilter 來實現。

(本示例相關源碼可以從 //github.com/aeraki-framework/thrift-envoyfilter-example 下載)

首先,我們需要創建一個圖中左邊所示的 EnvoyFilter 來處理客戶端的出向流量,該 EnvoyFilter 的 Match 條件選中了 $(thrift-sample-server-vip)_9090 這個 Outbound Listener 中 的 tcp_proxy,在 Patch 部分將其替換為一個 thrift_proxy。在該 thrift_proxy 中,我們按照 Traffic Splitting 的要求為其配置了相應的路由:將 30% 的流量路由到 Server v1版本,70% 的流量路由到 Server v2 版本。我們也需要為 Thrift Server 端創建一個如圖右上所示的 EnvoyFilter 來處理服務器端的入向流量。相比客戶端的 EnvoyFilter 而言,服務器端的 EnvoyFilter 配置要簡單一些,因此我們不需要在服務器端配置任何路由規則,只需要將 tcp_proxy 替換為 thrift_proxy 即可。這個 thrift_proxy 雖然沒有路由規則,但提供了大量七層的服務通信和治理能力,包括請求層面的負載均衡、產生請求層面的 Metrics 數據等。

從上面的介紹和示例可以看到, EnvoyFilter CRD 好比是 Istio 中的一把瑞士軍刀,可以對 Pilot 生成的 Envoy 配置進行非常靈活的定製,以達到對七層協議進行管理的目的。但是 EnvoyFilter 也帶來了一些難以處理的問題:

  • EnvoyFilter 將 Envoy 的底層實現細節直接暴露給了運維人員:運維人員必須非常了解 Envoy 的配置細節,而這些配置細節往往和 Envoy Filter 內部的實現機制緊密相關,例如 Filter 的名稱和 Filter 內部的配置格式等。這導致創建 EnvoyFilter 成為了一種和代碼細節高度耦合的工作,難以直接交付給運維人員。更為合理的方式則應該是採用一種面向用戶的高級配置語言來屏蔽這些實現細節,例如 Istio 中的 VirtualService 和 DestinationRule。
  • EnvoyFilter 中的匹配條件依賴於 Pilot 生成的 Envoy 配置中的結構組成和元素命名,例如 Listener 的名稱,FilterChain 的構成等。而這些結構和命名在不同的 Istio 版本之間可能發生變化,導致原本能夠正常工作的 EnvoyFilter 在新版本中出現問題。
  • EnvoyFilter 中的匹配條件還依賴於一些和特定 K8s 集群相關的內容,例如 Service Cluster IP,這意味着一個 EnvoyFilter 不能用於多個不同集群中的相同服務。當 Service 被重建時,由於 Cluster IP 會發生變化,相應的 EnvoyFilter 也必須進行改動,修改 Match 條件中的 Cluster IP。
  • 我們需要為每個 Service 創建相應的 EnvoyFilter,當 Mesh 中管理的服務較多時,手動創建成百上千的 EnvoyFilter 的工作是非常繁瑣而且及易出錯的。
  • 對 Istio 而言,EnvoyFilter 中的 Patch 部分基本上是一個黑盒,因此 Istio 只能對 EnvoyFilter 的正確性進行非常有限的驗證。這導致 EnvoyFilter 的調試非常困難,當 Envoy 未能按照你的設想工作時,你很難知道到底是 EnvoyFilter 的什麼地方出現了問題。

由於上述的種種問題,我們可以看到,雖然可以使用 EnvoyFilter 來在 Istio 中實現七層協議的管理,但是在一個生產系統,特別是一個中大型的 Service Mesh 中管理和維護這些 EnvoyFilter 是非常困難的。

Aeraki:在 Istio 中管理任何七層協議

由於難以手動對 EnvoyFilter 進行管理和維護 ,我們創建了Aeraki (發音:[Air-rah-ki])項目來自動化這個流程。Aeraki 是希臘語中「微風」的意思,我們希望 Aeraki 這股微風能幫助 Istio 在雲原生的旅程中航行得更遠。

Aeraki 的基本工作原理如下圖所示:Aeraki 從 Istio 中拉取服務數據,根據 ServiceEntry 和 Aeraki 流量規則生成 Envoy 配置,並採用 EnvoyFilter 將生成的配置推送到 Istio 中。簡而言之,你可以把 Aeraki 看做 Istio 中管理的七層協議的 Operator

相比於直接修改 Istio 代碼和採用 EnvoyFilter 這兩種擴展 Istio 流量管理能力的方式,採用 Aeraki 為我們帶來了以下的好處:

  • 不需要修改 Istio 代碼,因此節省了單獨維護一個 Istio 的私有代碼分支的額外工作量,可以快速跟隨 Istio 的版本迭代進行升級。
  • Aeraki 作為一個獨立組件部署在 Mesh 的控制面,可以很方便地作為一個插件和 Istio 進行集成,對 Istio 的流量管理能力進行擴展。
  • 協議相關的缺省配置由 Aeraki 自動生成,並且這些配置可以根據 Istio 版本和 K8s 集群相關信息自動進行調整。節約了大量 EnvoyFilter 的手動創建和維護工作。
  • Aeraki 在 Envoy 配置之上進行了抽象,提供了一層面向用戶的配置 CRD 來對這些七層協議進行管理。這些高級 CRD 隱藏了 Envoy 的配置細節,屏蔽了不同 Istio 版本生成的缺省 Envoy 配置的差異,對於運維非常友好。對於 Thrift 和 Dubbo 這樣的 RPC 協議,由於其語義和 HTTP 類似,Aeraki 直接採用了 Istio VirtualService 和 DestinationRule;對於非 RPC 協議,Aeraki 則定義了一些新的 CRD 來進行管理,例如 RedisService 和 RedisDestination。我們後面將進一步介紹如何使用這些配置 CRD 來定製規則,例如實現 Traffic Splitting。

和 Istio 類似,Aeraki 也採用了端口名稱來識別協議類型。端口取名需要遵循 「tcp-七層協議名-xxx」 的命名規則。例如,一個 Thrift 服務應取名為 「tcp-thrift-service」。需要注意的是,我們必須保留端口名中的「tcp-」前綴,因為對於 Istio 而言,這是一個 TCP 協議的服務。Aeraki 則會根據端口名中的七層協議來生成相應的 Envoy 配置,並替換 Istio 缺省生成的 tcp_proxy。

我們來看看如何採用 Aeraki 來實現上面 Thrift 服務的 Traffic Splitting 用例。首先我們需要在 Thrift Service 定義的 Port 命名中聲明該 Service 的七層協議類型:「tcp-thrift-hello-server」,然後創建一個 VirtualService 將 Thrift 請求按照指定比例路由到不同的服務版本中。Aeraki 將根據服務定義和 VirtualService 生成所需的 Envoy 配置,並通過 EnvoyFilter 發送給 Istio。

可以看到,相對於手動創建 EnvoyFilter,採用 Aeraki 來管理 Thrift 要簡單得多。如果不需要特殊的流量規則,則會更簡單,只需要按照命名規範在 Port 名稱中聲明 Thrift 協議即可,Aeraki 會生成所需的 Envoy 配置,無需任何額外的工作。

想自己試試 Aeraki 的 Thrift、Dubbo、Redis 服務管理能力?非常簡單,只需在一個連接到 K8s 集群的命令行終端上運行下面兩行代碼,就可以安裝一個帶有 Aeraki 插件的 Istio 集群以及相應的 Demo 程序,歡迎大家嘗試!

`git clone https:``//github``.com``/aeraki-framework/aeraki``.git``aeraki``/demo/install-demo``.sh`

也可以訪問 Aeraki 的在線 Demo,查看從 Thrift、Dubbo、Redis 等服務收集到的監控指標面板://aeraki.zhaohuabing.com:3000/d/pgz7wp-Gz/aeraki-demo?orgId=1&refresh=10s&kiosk

使用 Aeraki 增強 Service Mesh

下面我們來看一下使用 Aeraki 的七層協議管理能力來增強 Service Mesh 的一些案例。

屏蔽開發/生產環境的差異

我們在開發、測試和生產環境中通常需要訪問不同的後端資源,例如需要連接到不同的 Redis 緩存或者不同的 mySQL 數據庫。一般來說,我們需要修改隨應用程序發佈的配置文件中的後端資源地址,以達到在不同環境中切換後端資源的目的。通過 Aeraki 的幫助,我們可以用 Service Mesh 來屏蔽不同後端資源的配置差異,使得應用程序可以用相同的方式訪問不同環境中的後端資源。

如下圖所示,我們在 Dev、Staging 和 Prod 三個環境中都需要訪問 Redis 服務,這三個 Redis 服務有不同的 IP 地址和訪問密碼,部署方式也可能不同:在開發環境中,為了節約資源和簡化部署,我們可能使用單個 Redis 實例;在測試和生產環境中,我們會使用 Redis 集群來保證 Redis 服務的高可用和擴展性,我們也可能直接使用雲服務商提供的 Redis 託管服務。當在這三個環境中進行切換時,我們需要配置不同的 IP 地址和訪問密碼,如果 Redis 部署的方式不同,我們甚至可能需要修改客戶端代碼來切換 Redis 單實例模式和集群模式,這極大影響了我們開發、測試和上線的效率。

通過 Aeraki 提供的 RedisService 和 RedisDestination CRD,我們可以屏蔽這些不同 Redis 服務提供者之間的差異,允許客戶端以統一的方式訪問後端的 Redis 服務。

在採用 Aeraki 之前,我們在不同的環境中需要配置不同的 IP 地址和 Redis 訪問密碼。採用 Aeraki 之後,在客戶端可以採用相同的代碼和配置,通過修改 Aeraki CRD 來切換不同環境中的 Redis 配置,大大減少在不同環境之間進行切換的成本。即使 Redis 從單實例改為了 Redis 集群,客戶端也可以採用相同的方式進行訪問。

採用流量鏡像進行對比測試

有一些數據庫或者數據庫代理採用相同的網絡協議。例如 TiDB、Oceanbase、Aurora、Kingshard等都兼容 MySQL 協議;Twemproxy、Codis、Tendis、Pika等都採用了 Redis 協議。由於業務需求,我們有時需要從一個實現遷移到另一個實現上。在遷移之前,我們需要進行對比測試,以對比不同實現的性能、功能及兼容性。

例如下面的場景:我們最初只用了一個單實例 Redis 來做緩存,隨着線上業務的不斷擴展,該 Redis 實例已經出現了訪問瓶頸,我們希望切換為採用 Twemproxy 來對 Redis 進行水平擴展。通過採用 Aeraki 來將線上的 Redis 流量鏡像到 Twemproxy 測試環境,我們可以採用真實的業務數據對 Twemproxy 進行充分的測試,以評估其對線上業務的影響。

採用全流量故障注入測試系統彈性

Istio 可以實現 HTTP 和 gRPC 的故障注入,但這還不夠。在一個分佈式系統中,應用服務、數據庫、緩存、消息系統等都可能由於網絡或者其他原因出現不可用的情況。採用 Aeraki,我們可以對系統中的所有這些可能的故障點進行完整的模擬,以測試系統的彈性,保證我們的系統在一部分出現問題後可以自愈或者通過降級保證系統基本可用,而不至於整個系統崩潰。

小結

Service Mesh 中有大量的七層協議流量,包括 RPC、Database、Cache、Messaging 等類型的七層協議,但 Istio 只提供了 HTTP 和 gRPC 的七層管理能力,對其他七層協議的支持非常有限。Aerkai 開源項目通過非侵入的方式為 Istio 提供了任意七層協議的支持能力,並提供了面向用戶的高級配置 CRD,可以很方便地對這些協議的流量進行管理,實現灰度發佈等高級流量管理能力。目前 Aeraki 已經支持了 Thrift、Dubbo、Redis、Kafka、Zookeeper,並即將支持更多的協議。Aeraki 的定位是做成一個非侵入式 Istio 功能增強工具集,除了協議擴展之外,還會關註解決在 Istio 使用過程中遇到的其他常見問題,包括效率優化、配置簡化、第三方服務發現接入、功能擴展等。如果您希望了解更多關於 Aeraki 的內容,歡迎訪問 Github 主頁 //github.com/aeraki-framework/aeraki

**招聘信息

騰訊雲 Service Mesh 團隊正在火熱招聘中,Base 成都、北京、深圳或者西安,要求候選者熟悉 Kubernetes/Istio/Envoy。歡迎大家發送簡歷到 [email protected] 或者微信聯繫 zhao_huabing。

參考鏈接:

【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多乾貨!!