雲原生網絡代理 MOSN 多協議機制解析

多協議機制產生的背景與實踐痛點

MOSN 簡介

雲原生網絡代理 MOSN 定位是一個全棧的網絡代理,支持包括網絡接入層(Ingress)、API Gateway、Service Mesh 等場景,目前在螞蟻金服內部的核心業務集羣已經實現全面落地,並經受了 2019 年雙十一大促的考驗。今天要向大家介紹的是雲原生網絡代理 MOSN 核心特性之一的多協議擴展機制,目前已經支持了包括 SOFABolt、Dubbo、TARS 等多個協議的快速接入。

MOSN:https://github.com/mosn/mosn

MOSN 官網:https://mosn.io

產生背景與實踐痛點

首先介紹一下多協議機制產生的背景。

前面提到,螞蟻金服 2019 年雙十一核心鏈路百分之百 Mesh 化,是業界當時已知的最大規模的 Service Mesh 落地,爲什麼我們敢這麼做?因爲我們具備能夠讓架構平滑遷移的方案。"兼容性"是任何架構演進升級都必然要面對的一個問題,這在早已實踐微服務化架構的螞蟻金服內部同樣如此。爲了實現架構的平滑遷移,需要讓新老節點的外在行爲儘可能的表現一致,從而讓依賴方無感知,這其中很重要的一點就是保持協議兼容性。

因此,我們需要在 Service Mesh 架構下,兼容現有微服務體系中的通信協議——也就是說需要在 MOSN 內實現對目前螞蟻金服內部通信協議的擴展支持。

基於 MOSN 本身的擴展機制,我們完成了最初版本的協議擴展接入。但是在實踐過程中,我們發現這並不是一件容易的事情:

  • 相比編解碼,協議自身的處理以及與框架集成纔是其中最困難的環節,需要理解並實現包括請求生命週期、多路複用處理、鏈接池等等機制;
  • 社區主流的 xDS 路由配置是面向 HTTP 協議的,無法直接支持私有協議,存在適配成本;

基於這些實踐痛點,我們設計了 MOSN 多協議框架,希望可以降低私有協議的接入成本,加快普及 ServiceMesh 架構的落地推進。

常見的協議擴展思路初探

前面介紹了背景,那麼具體協議擴展框架要怎麼設計呢?我們先來看一下業界的思路與做法。

協議擴展框架 - Envoy

注:圖片來自 Envoy 分享資料

第一個要介紹的是目前發展勢頭強勁的 Envoy。從圖上可以看出,Envoy 支持四層的讀寫過濾器擴展、基於 HTTP 的七層讀寫過濾器擴展以及對應的 Router/Upstream 實現。如果想要基於 Envoy 的擴展框架實現 L7 協議接入,目前的普遍做法是基於 L4 filter 封裝相應的 L7 codec,在此基礎之上再實現對應的協議路由等能力,無法複用 HTTP L7 的擴展框架。

協議擴展框架 - Nginx

第二個則是老牌的反向代理軟件 Nginx,其核心模塊是基於 Epoll/Kqueue 等 I/O 多路複用技術之上的離散事件框架,基於事件框架之上構建了 Mail、Http 等協議模塊。與 Envoy 類似,如果要基於 Nginx 擴展私有協議,那麼也需要自行對接事件框架,並完整實現包括編解碼、協議處理等能力。

協議擴展框架 - MOSN

最後回過頭來,我們看一下 MOSN 是怎麼做的。實際上,MOSN 的底層機制與 Envoy、Nginx 並沒有核心差異,同樣支持基於 I/O 多路複用的 L4 讀寫過濾器擴展,並在此基礎之上再封裝 L7 的處理。但是與前兩者不同的是,MOSN 針對典型的微服務通信場景,抽象出了一套適用於基於多路複用 RPC 協議的擴展框架,屏蔽了 MOSN 內部複雜的協議處理及框架流程,開發者只需要關注協議本身,並實現對應的框架接口能力即可實現快速接入擴展。

三種框架成本對比

最後對比一下,典型微服務通信框架協議接入的成本,由於 MOSN 針對此類場景進行了框架層面的封裝支持,因此可以節省開發者大量的研發成本。

SOFABolt 協議接入實踐

初步瞭解多協議框架的設計思路之後,讓我們以 SOFABolt 協議爲例來實際體驗一下協議接入的過程。

SOFABolt 簡介

這裏先對 SOFABolt 進行一個簡單介紹,SOFABolt 是一個開源的輕量、易用、高性能、易擴展的 RPC 通信框架,廣泛應用於螞蟻金服內部。

SOFABolt:https://github.com/sofastack/sofa-bolt

基於 MOSN 的多協議框架,實際編寫了 7 個代碼文件,一共 925 行代碼(包括 liscence、comment 在內)就完成了接入。如果對於協議本身較爲熟悉,且具備一定的 MOSN/Golang 開發經驗,甚至可以在一天內就完成整個協議的擴展,可以說接入成本是非常之低。

Github:https://github.com/mosn/mosn/tree/master/pkg/protocol/xprotocol/bolt

下面讓我們進入正題,一步一步瞭解接入過程。

Step1:確認協議格式

第一步,需要確認要接入的協議格式。爲什麼首先要做這個,因爲協議格式是一個協議最基本的部分,有以下兩個層面的考慮:

  • 任何協議特性以及協議功能都能在上面得到一些體現,例如有無 requestId/streamId 就直接關聯到協議是否支持連接多路複用;
  • 協議格式與報文模型直接相關,兩者可以構成邏輯上的映射關係;而這個映射關係也就是所謂的編解碼邏輯;

以 SOFABolt 爲例,其第一個字節是協議 magic,可以用於校驗當前報文是否屬於 SOFABolt 協議,並可以用於協議自動識別匹配的場景;第二個字節是 type,用於標識當前報文的傳輸類型,可以是 Request / RequestOneway / Response 中的一種;第三個字節則是當前報文的業務類型,可以是心跳幀,RPC 請求/響應等類型。後面的字段就不一一介紹了,可以發現,理解了協議格式本身,其實對於協議的特性支持和模型編解碼就理解了一大半,因此第一步協議格式的確認了解是重中之重,是後續一切工作開展的前提。

Step2:確認報文模型

順應第一步,第二步的主要工作是確認報文編程模型。一般地,在第一步完成之後,應當可以很順利的構建出相應的報文模型,SOFABolt 例子中可以看出,模型字段設計基本與協議格式中的 header / payload 兩部分相對應。有了編程模型之後,就可以繼續進行下一步——基於模型實現對應的框架擴展了。

Step3:接口實現

接口實現 - 協議

協議擴展,顧名思義,是指協議層面的擴展,描述的是協議自身的行爲(區別於報文自身)。

目前多協議框架提供的接口包括以下五個:

  • Name:協議名稱,需要具備唯一性;
  • Encoder:編碼器,用於實現從報文模型到協議傳輸字節流的映射轉換;
  • Decoder:解碼器,用於實現從協議傳輸字節流到報文模型的映射轉換;
  • Heartbeater:心跳處理,用於實現心跳保活報文的構造,包括探測發起與回覆兩個場景;
  • Hijacker:錯誤劫持,用於在特定錯誤場景下錯誤報文的構造;

接口實現 - 報文

前面介紹了協議擴展,接下里則是報文擴展,這裏關注的是單個請求報文需要實現的行爲。目前框架抽象的接口包括以下幾個:

  • Basic:需要提供 GetStreamType、GetHeader、GetBody 幾個基礎方法,分別對應傳輸類型、頭部信息、載荷信息;
  • Multiplexing:多路複用能力,需要實現 GetRequestId 及 SetRequestId;
  • HeartbeatPredicate:用於判斷當前報文是否爲心跳幀;
  • GoAwayPredicate:用於判斷當前報文是否爲優雅退出幀;
  • ServiceAware:用於從報文中獲取 service、method 等服務信息;

舉個例子

這裏舉一個例子,來讓大家對 框架如何基於接口封裝處理流程 有一個體感:服務端心跳處理場景。當框架收到一個報文之後:

  • 根據報文擴展中的 GetStreamType 來確定當前報文是請求還是響應。如果是請求則繼續 2;
  • 根據報文擴展中的 HeartbeatPredicate 來判斷當前報文是否爲心跳包,如果是則繼續 3;
  • 當前報文是心跳探測(request + heartbeat),需要回復心跳響應,此時根據協議擴展中的 Heartbeater.Reply 方法構造對應的心跳響應報文;
  • 再根據協議擴展的 Encoder 實現,將心跳響應報文轉換爲傳輸字節流;
  • 最後調用 MOSN 網絡層接口,將傳輸字節流回復給發起心跳探測的客戶端;

當協議擴展與報文擴展都實現之後,MOSN 協議擴展接入也就完成了,框架可以依據協議擴展的實現來完成協議的處理,讓我們實際演示一下 SOFABolt 接入的 example。

Demo 地址:

https://github.com/mosn/mosn/tree/master/examples/codes/sofarpc-with-xprotocol-sample

MOSN 多協議機制設計解讀

通過 SOFABolt 協議接入的實踐過程,大家對如何基於 MOSN 來做協議擴展應該有了一個初步的認知。那麼 MOSN 多協議機制究竟封裝了哪些邏輯,背後又是如何思考設計的?接下來將會挑選幾個典型技術案例爲大家進行解讀。

協議擴展框架

協議擴展框架 - 編解碼

最先介紹的是編解碼機制,這個在前面 SOFABolt 接入實踐中已經簡單介紹過,MOSN 定義了編碼器及解碼器接口來屏蔽不同協議的編解碼細節。協議接入時只需要實現編解碼接口,而不用關心相應的接口調用上下文。

協議擴展框架 - 多路複用

接下來是多路複用機制的解讀,這也是流程中相對不太好理解的一部分。首先明確一下鏈接多路複用的定義:允許在單條鏈接上,併發處理多個請求/響應。那麼支持多路複用有什麼好處呢?

以 HTTP 協議演進爲例,HTTP/1 雖然可以維持長連接,但是單條鏈接同一時間只能處理一個請求/相應,這意味着如果同時收到了 4 個請求,那麼需要建立四條 TCP 鏈接,而建鏈的成本相對來說比較高昂;HTTP/2 引入了 stream/frame 的概念,支持了分幀多路複用能力,在邏輯上可以區分出成對的請求 stream 和響應 stream,從而可以在單條鏈接上併發處理多個請求/響應,解決了 HTTP/1 鏈接數與併發數成正比的問題。

類似的,典型的微服務框架通信協議,如 Dubbo、SOFABolt 等一般也都實現了鏈接多路複用能力,因此 MOSN 封裝了相應的多路複用處理流程,來簡化協議接入的成本。讓我們跟隨一個請求代理的過程,來進一步瞭解。

  1. MOSN 從 downstream(conn=2) 接收了一個請求 request,依據報文擴展多路複用接口 GetRequestId 獲取到請求在這條連接上的身份標識(requestId=1),並記錄到關聯映射中待用;
  2. 請求經過 MOSN 的路由、負載均衡處理,選擇了一個 upstream(conn=5),同時在這條鏈接上新建了一個請求流(requestId=30),並調用文擴展多路複用接口 SetRequestId 封裝新的身份標識,並記錄到關聯映射中與 downstream 信息組合;
  3. MOSN 從 upstream(conn=5) 接收了一個響應 response,依據報文擴展多路複用接口 GetRequestId 獲取到請求在這條連接上的身份標識(requestId=30)。此時可以從上下游關聯映射表中,根據 upstream 信息(connId=5, requestId=30) 找到對應的 downstream 信息(connId=2, requestId=1);
  4. 依據 downstream request 的信息,調用文擴展多路複用接口 SetRequestId 設置響應的 requestId,並回復給 downstream;

在整個過程中,框架流程依賴的報文擴展 Multiplexing 接口提供的能力,實現了上下游請求的多路複用關聯處理,除此之外,框架還封裝了很多細節的處理,例如上下游複用內存塊合併處理等等,此處限於篇幅不再展開,有興趣的同學可以參考源碼進行閱讀。

統一路由框架

接下來要分析的是「統一路由框架」的設計,此方案主要解決的是非 HTTP 協議的路由適配問題。我們選取了以下三點進行具體分析:

  • 通過基於屬性匹配(attribute-based)的模式,與具體協議字段解耦;
  • 引入層級路由的概念,解決屬性扁平化後帶來的線性匹配性能問題;
  • 通過變量機制懶加載的特定,按需實現深/淺解包;

統一路由框架 – 基於屬性匹配

首先來看一下典型的 RDS 配置,可以看到其中的 domains、path 等字段,對應的是 HTTP 協議裏的域名、路徑概念,這就意味着其匹配條件只有 HTTP 協議纔有字段能夠滿足,配置結構設計是與 HTTP 協議強相關的。這就導致瞭如果我們新增了一個私有協議,無法複用 RDS 的配置來做路由。

那麼如何解決配置模型與協議字段強耦合呢?簡單來說就是把匹配字段拆分爲扁平屬性的鍵值對(key-value pair),匹配策略基於鍵值對來處理,從而解除了匹配模型與協議字段的強耦合,例如可以配置 key: $httphost,也可以配置 key:$dubboservice,這在配置模型層面都是合法的。

但是這並不是說匹配就有具體協議無關了,這個關聯仍然是存在的,只是從強耦合轉換爲了隱式關聯,例如配置 key: $http_host,從結構來說其與 HTTP 協議並無耦合,但是值變量仍然會通過 HTTP 協議字段來進行求值。

統一路由框架 - 層級路由

在引入「基於屬性的匹配」之後,我們發現了一個問題,那就是由於屬性本身的扁平化,其內在並不包含層級關係。如果沒有層級關係,會導致匹配時需要遍歷所有可能的情況組合,大量條件的場景下匹配性能近似於線性的 O(n),這顯然是無法接受的。 舉例來說,對於 HTTP 協議,我們總是習慣與以下的匹配步驟:

  • 匹配 Host(:authority) ;
  • 匹配 Path ;
  • 匹配 headers/args/cookies ;

這其實構成了一個層級關係,每一層就像是一個索引,通過層級的索引關係,在大量匹配條件的情況下仍然可以獲得一個可接受的耗時成本。但是對於屬性(attribute),多個屬性之間並沒有天然的層級關係(相比於 host、path 這種字段),這依賴於屬性背後所隱式關聯的字段,例如對於 Dubbo 協議,我們希望的順序可能是:

  • 匹配 $dubbo_service;
  • 匹配 $dubbo_group;
  • 匹配 $dubbo_version;
  • 匹配 $dubboattachmentsxx;

因此在配置模型上,我們引入了對應的索引層級概念,用於適配不同協議的結構化層級路由,解決扁平屬性的線性匹配性能問題。

統一路由框架 - 淺解包優化

最後,介紹一下淺解包優化的機制。利用 MOSN 變量懶加載的特性,我們可以在報文解析時,先不去解析成本較高的部分,例如 dubbo 協議的 attachments。那麼在代理請求的實際過程中,需要使用到 attachments 裏的信息時,就會通過變量的 getter 求值邏輯來進行真正的解包操作。依靠此特性,可以大幅優化在不需要深解包的場景下 dubbo 協議代理轉發的性能表現,實現按需解包。

解讀總結

最後,對設計部分的幾個技術案例簡單總結一下,整體的思路仍然是對處理流程進行抽象封裝,並剝離可擴展點,從而降低用戶的接入成本。

在協議擴展支持方面:

  • 封裝編解碼流程,抽象編解碼能力接口作爲協議擴展點
  • 封裝協議處理流程,抽象多路複用、心跳保活、優雅退出等能力接口作爲協議擴展點

在路由框架方面:

  • 通過改爲基於屬性匹配的機制,與具體協議字段解耦,支持多協議適配;
  • 引入層級路由機制,解決屬性扁平化的匹配性能問題;
  • 利用變量機制懶加載特性,按需實現深/淺解包;

後續規劃及展望

更多流模式支持、更多協議接入

當前 MOSN 多協議機制,已經可以比較好的支持像 Dubbo、SOFABolt 這樣基於多路複用流模型的微服務協議,後續會繼續擴展支持的類型及協議,例如經典的 PING-PONG 協議、Streaming 流式協議,也歡迎大家一起參與社區建設,貢獻你的 PR。

社區標準方案推進

與此同時,我們注意到 Istio 社區其實也有類似的需求,希望設計一套協議無關的路由機制——“Istio Meta Routing API”。其核心思路與 MOSN 的多協議路由框架基本一致,即通過基於屬性的路由來替代基於協議字段的路由。目前該草案還處於一個比較初級的階段,對於匹配性能、字段擴展方面還沒有比較完善的設計說明,後續 MOSN 團隊會積極參與社區方案的討論,進一步推動社區標準方案的落地。

以上就是本期分享的全部內容,如果大家對 MOSN 有問題以及建議,歡迎在羣內與我們交流。

本期視頻回顧以及 PPT 查看地址

https://tech.antfin.com/community/live/1131

本文轉載自公衆號金融級分佈式架構(ID:Antfin_SOFA)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&mid=2247485968&idx=1&sn=d0574663fc1c165e6166f02da93a4db9&chksm=faa0e5cacdd76cdc79a4843817e9a2c7266136565cbd94e0da3a940eacf9a6440db87307c712&scene=27#wechat_redirect

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章