天天看點

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

黃挺,螞蟻金服進階技術專家,螞蟻金服分布式架構 SOFA 的開源負責人。目前在螞蟻金服中間件團隊負責應用架構與服務化相關的工作。

本文根據黃挺在 CNUTCon 全球運維大會的主題分享整理,完整的分享 PPT 擷取方式見文章底部。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

大家好,我是來自于螞蟻金服的黃挺,花名魯直,目前在螞蟻金服負責微服務團隊,也是 SOFA 開源的負責人。

來到這個場子的朋友們肯定都知道,Service Mesh 在過去一兩年之中迅速成長為社群中非常熱門的話題,幾乎所有的大會中,都多多少少有一些關于 Service Mesh 的話題。

在一個月之前,我的同僚敖小劍老師在上海的 QCon 中也分享了螞蟻金服在 Service Mesh 上的探索,包括在前面的場次中,來自華為的巨震老師也分享了華為在 Service Mesh 上的一些思考。

在今天的分享中,我不會去花太多時間介紹什麼是 Service Mesh,更多地聚焦在螞蟻金服将 Service Mesh 用在解決多語言的問題上的一些實踐,希望在場的各位可以從這些實踐中有所收獲。

這個是我今天介紹的主要的内容:

首先,我會大家簡單介紹一下多語言在螞蟻金服發展的一些情況,鋪墊一下背景,交代各個語言在螞蟻金服的使用情況,并且之前在多語言通信上面遇到了哪些問題。

然後,我會給大家簡單介紹下 SOFAMesh,SOFAMesh 是螞蟻金服産出的 Service Mesh 的解決方案。

接着我會介紹我們在 SOFAMesh 之上架構的多語言通信的方案以及在這個方案的實施過程中遇到的一些技術要點。

螞蟻金服多語言發展

不知道在場的同學有沒有聽說過 SOFA,SOFA 是螞蟻金服大約 10 年前開始研發的一套分布式中間件,包括了微服務體系,分布式事務,消息中間件,資料通路代理等等元件,這套元件一直以來都是完全用 Java 來建構的,是以基于 SOFA 建構的 SOFA 應用也都是用 Java 寫的,在螞蟻金服,目前大概有接近 2000 個 SOFA 應用,順帶提一下,這套 SOFA 中間件目前已經部分開源在 Github 上面。從這個資料我們也可以顯而易見地得出以下的結論,Java 在螞蟻金服,至少在線上的應用上,占據了絕對主導的地位。

随着無線技術的發展以及 NodeJS 技術的興起,在 2013 年,螞蟻金服開始引入了 NodeJS,研發了 EggJS,目前也已經在 Github 上開源,在螞蟻金服,我們主要将 EggJS 作為服務于無線以及 PC 的 BFF 層來使用,後端的所有的微服務還都是用基于 Java 的 SOFA 來研發,EggJS 要調用後端的 SOFA 服務,并且對 PC 和無線端提供接口,必然就要遵守 Java 世界的 SOFA 之前定下的種種“規矩”,事實上,螞蟻金服的 NodeJS 團隊完全用 EggJS 适配了所有的 SOFA 中間件的用戶端,保證在 EggJS 上,也可以使用所有的 SOFA 中間件,可以和之前基于 Java 研發的 SOFA 應用進行通信。但是,由于 Java 在螞蟻中間件上的主導地位,導緻 SOFA 中間件的某些特性的實作,完全依賴于 Java 特有的語言特性,是以,NodeJS 團隊在追趕 SOFA 中間件的過程中,也非常的痛苦,在後面的例子中,我會有一些具體的例子,大家看了之後肯定會感同身受。

再到最近幾年,随着 AI 的興起,在螞蟻金服也越來越多地出現 CPP,Python 等系統,而由于 CPP 和 Python 等等語言,在螞蟻金服并沒有一個獨立的基礎設施團隊去研發對應的中間件,是以,他們和基于 Java 的 SOFA 應用的互通就降級成了直接采用 HTTP 來通信,這種方式雖然也可以 Work,但是在通信基礎之上的服務調用的能力卻完全沒有,和原本的 SOFA 的基礎設施也完全沒法連接配接在一起。

基于以上的一些現狀,可以看到我們在發展過程中的主要的兩個問題,一個是基礎設施上的重複投入的消耗,很多 SOFA 中間件的特性,除了用 Java 寫了一遍之外,還得用 NodeJS 再寫一遍。另一個是以 Java 為中心,以 Java 為中心其實在隻有 Java 作為開發語言的時候并沒有什麼問題,但是當其他的語言需要和你進行通信的時候,就會出現巨大的問題,事實上,很多架構上的特性的研發同學在不經意之間,就直接就用了 Java 的語言特有的特性去進行研發,這種慣性和隐性的思維會對其他語言造成巨大的壁壘。

基于以上的問題,我們希望能夠産出一個方案,一方面,可以盡量做到一次實作,到處可用。另一方面,需要能夠保證語言的中立性,最好是能夠天然地就可以讓架構或者中間件的研發的同學去在做架構設計以及編碼的時候,考慮到需要支援多語言。

SOFAMesh

其實在這之前,我們已經嘗試在資料通路層去解決類似的多語言适配的問題,螞蟻金服有一個 OceanBase 的資料庫,當各個語言需要通路 OceanBase 資料庫的時候,采用的就是一個本地的 Proxy,這個 Proxy 會負責 Fail Over,容災等等場景,而對各個語言隻要保證 SQL 上的相容就可以了,這讓我們意識到,Proxy 的模式可能是解決多語言的一個方式,然後,在業界就出現了 Service Mesh,如果隻是從技術上講,Service Mesh 的 Sidecar 本質上也就是一個 Proxy,隻是每一個服務執行個體都加上一個 Sidecar,這些 Sidecar 組成了一個網絡,在加上一個控制平面,大家把他叫做 Service Mesh。通過 Service Mesh,我們可以将大量原來需要在語言庫中實作的特性下沉到 Sidecar 中,進而達到一次實作,到處可用的效果;另外,因為 Sidecar 本身不以 Library 的形式內建到特定語言實作的服務中,是以也就不會說某些關鍵特性采用特定語言的特性來實作,可以保證良好的中立性。

看起來 Service Mesh 似乎是一個非常完美的解決方案,但是如果我們探尋一下 Service Mesh 的本質的話,就會發現 Service Mesh 并非完美解決方案,這種不完美主要是展現在 Service Mesh 本質上是一種抽象,它抽象了什麼東西,它把原來的服務調用中的一些高可用的能力全部抽象到了基礎設施層。在這張 PPT 中,我放了三張圖檔,都是一棵樹,從左到右,越來越抽象,從圖中也可以非常直覺地看出來,從右到左,細節越來越豐富。不管是什麼東西,抽象就意味着細節的丢失,丢失了細節,就意味着在能力上會有所欠缺,是以,在 Service Mesh 的方案下,雖然看起來我們可以通過将能力下層到基礎設施層,但是一旦下層下去,某些方面的能力就會受損。

是以,我們希望能夠演化出這樣一套多語言通信的方案,它能夠以 Service Mesh 為基礎,但是我們也會做适當地妥協去彌補因為上了 Service Mesh 之後的一些能力的缺失。首先我們希望有一個語言中立的高效的通信協定,每個語言都能夠非常簡單地了解這個協定,這個是在一個跨語言的 RPC 通信中避免不了的,無論是否采用 Service Mesh。然後,我們希望将大部分的能力都下沉到 Sidecar 裡面去,包括服務發現,藍綠釋出,灰階釋出,限流熔斷,服務鑒權等等能力,然後通過統一的控制平面去控制 Sidecar。然後,因為 Service Mesh 化之後的一些能力缺失,再通過一些輕量化的用戶端去實作,這些能力包括序列化,鍊路追蹤,限流,Metrics 等等。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

在 Service Mesh 的選型上,我們是基于 Istio 來做,但是用自己研發的基于 Golang 的 Sidecar 來替換掉 Envoy,一方面這個是因為 Golang 是一個雲原生領域的語言,另一方面,也因為 Envoy 在協定的擴充設計上并不好。目前我們的 Sidecar SOFAMesh 和 SOFAMosn 都已經在 Github 上面開源。

前面分析了我們在多語言上走過的一些路,以及我們期望 Service Mesh 能夠為我們去解決的一些問題,也簡單講了一下某些能力是無法完全通過 Service Mesh 去解決的。

SOFAMesh 解決多語言問題中的技術要點

在了解了我們要通過 SOFAMesh 去解決的問題以及解決的方式之後,我們來看下螞蟻金服在具體實施這套方式的時候遇到了什麼樣的問題。剛才我們也講到了,我們用 SOFAMesh 解決多語言通信的問題的方案中,首先需要一個語言中立的高效地通信協定,是以,我們就先來講講通信協定。通信協定我對它的定位是整個服務調用中的靈魂,如果它沒有良好的擴充性和語言中立性,我們就沒法解決好多語言調用的問題,在整個 SOFAMesh 的方案中,通信協定除了需要能夠被各個語言的用戶端 JAR 包了解之外,還需要能夠被 Sidecar 很好地了解。

1、通信協定

我們可以先看看一下早期的 SOFARPC 的通信協定的設計,我們的通信協定包含三個部分,一個是協定頭,這個協定頭隻包含了一些簡單的資訊,比如協定的 Magic Number 之類的,然後是協定的元資訊,這些元資訊包含需要調用的接口名,需要調用的方法名之類的,接着是協定體的部分,包含了通信中需要攜帶的資料,在請求中,這部分攜帶的資料是經過了序列化之後的方法參數,在響應中,這部分攜帶的資料是經過了序列化之後的方法的傳回值,并且在 SOFARPC 的這個版本的通信協定的設計的時候,第二個部分和第三個部分是放在了一起做的 Hessian 的序列化。抛開協定體裡面的資料的序列化之外,我們可以非常清楚地看出,如果讓另外一個語言去了解這個通信協定,是非常困難的,因為讓這個語言的用戶端包需要将協定的頭的元資訊取出來的時候,它必須将第二和第三個部分作為一個整體來進行反序列化,必然是非常耗時的,另外,因為在 Service Mesh 的方案裡面,作為 Sidecar,也需要一些通信過程中的中繼資料的資訊來完成一些功能,是以 Sidecar 也需要對第二部分加上第三部分進行反序列化,這個對于 Sidecar 的性能來說,也是一個非常非常耗時的操作,當你需要增加一些服務的中繼資料到協定裡面去的時候,也非常困難,需要修改整個 SOFARequest 這個 Java 對象。從這個協定也可以看出,早期的 SOFARPC 的設計是非常以 Java 為中心的。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

我們再來看下 Dubbo 的協定設計,在 Dubbo 的協定設計裡面,也基本上分成了三個部分,一個部分是協定版本,一個部分是協定的服務中繼資料資訊,然後是協定體部分,Dubbo 的通信協定設計比早期的 SOFARPC 的通信協定的設計好的地方在于,Dubbo 的通信協定的第二個部分和第三個部分是分開來的,是以,當你需要讀取第二個部分的服務的中繼資料資訊的時候,不需要同時地去讀取第三個部分,這樣,無論是多語言用戶端包還是 Sidecar,都會比較容易處理,并且性能上會比較好;但是 Dubbo 的協定設計一個敗筆在于把 Hessian 作為了超一等公民來對待的,它的整個協定就是建構在 Hessian 的基礎上的,它用 Hessian 把協定頭給序列化了,它用 Hessian 把協定的服務中繼資料資訊也給序列化了,它還用 Hessian 來序列化協定體。這樣,當其他的語言需要去了解這個 Dubbo 協定的時候,必須要先了解 Hessian,而 Hessian 的多語言的支援做地并不是非常好,比如 Golang,就沒有一個比較好的 Hessian 的庫,給其他語言去了解 Dubbo 協定設定了障礙。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

在最近 SOFARPC 的版本中,我們重新設計整個通信協定,說是重新設計,不如說是簡化,其實最主要的變化就是在原來的設計中,我們的第二部分的服務中繼資料資訊以及第三部分的協定體是放在一個對象中,然後進行 Hessian 的序列化的;而現在是單獨拿出來了,并且使用類似于 URL 的 Query String 這樣簡單的 KV 結構來序列化,這樣當其他的語言需要讀取服務中繼資料的資訊的時候,非常簡單地就可以将服務中繼資料的資訊給提取出來,當需要增加服務中繼資料的字段的時候,也隻需要在 KV 結構裡面增加即可,擴充性上也非常友善。并且,Sidecar 也可以非常容易地将這些中繼資料資訊提取出來,用來完成服務發現,限流,熔斷等等之類的事情。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

是以對于一個通信協定來說,要做到比較好地适配不用語言通信的場景,适配 Service Mesh 的場景,在協定的設計上,協定頭的資訊必須做到容易提取,容易擴充,否則,無論是在 Sidecar 裡面,還是在多語言用戶端裡面,處理起來都會非常困難。

前面我們已經說了通信協定的設計的重要性,但是除了通信協定,對于序列化協定的選擇也非常關鍵,為了能夠保證的語言的中立,必須避免序列化協定和特定的語言綁定在一起,另外,在一家公司中,一旦標明了一個序列化協定,想要替換掉,是非常困難的,在螞蟻金服,一直以來序列化協定都是采用了 Hessian,雖然 Hessian 号稱也是多語言的,但是實際上語言的支援有限,并且 Hessian 最近這幾年的發展也比較慢,另外,Hessian 也有一些特性是專門針對 Java 語言做的,比如一些父子類的字段的覆寫關系的處理等等,這些特性在其他的語言中并不存在,會導緻不同語言之間的相容性問題。是以,我們在做多語言的時候,讓 SOFARPC 支援了 PB 協定,在 Bolt 的通信協定中,協定體裡面的資料可以采用 PB 做序列化,PB 相對于 Hessian 來說在多語言的支援上要好上非常多。在這塊,我們的處理的辦法也是比較溫和的,對于用 Java 研發的 SOFA 系統來說,它可以即暴露提供 PB 協定的接口,又提供 Hessian 協定的接口,這樣,一些原來用 Hessian 做序列化調用這個系統的系統就不用做任何修改。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

2、服務發現

前面講了在通信協定的設計的重要性,接下來我們就來講一講服務發現上的一些問題,假設一個 RPC 沒有服務發現的能力,基本上它就算是一個玩具,之是以一個 RPC 架構能夠滿足大規模分布式場景下的要求,服務發現的能力是非常基本的。

我們可以先看下左邊的這張圖,左邊的這張圖是 SOFARPC 目前的服務發現的模型,和大家看到的國内的一些開源的 RPC 架構基本上類似,因為最早 SOFARPC 就是為了友善 Java 而設計的,是以也是設計成盡量讓服務調用和本地調用一樣,而服務發現的粒度也是接口的次元,也就是說當一個應用要調用另一個應用釋出的服務的時候,它是按照服務的接口資訊從服務注冊中心上去尋找服務的提供方的位址的,并且服務的提供方也是按照接口來将服務注冊到服務注冊中心下的。在螞蟻金服裡面,大部分的情況下,同一個應用的不同的執行個體提供了的服務的是一模一樣的,但是也有一些情況下,同一個應用的不同的執行個體提供了不同的服務。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

這樣在社群的服務發現的方案裡面就會存在問題,在 Istio 裡面,服務的注冊是直接注冊成一個 K8s 的 Service,雖然在 K8s 裡面叫做 Service,但是實際上就是一個應用,當服務的調用方需要去調用這個服務的時候,是直接擷取對應的應用的 Service 的裡面的位址,本質上,這種服務發現的方式和基于 DNS 的服務發現的方式非常類似,當同一個應用的不同的執行個體釋出的服務是不對等的時候,用戶端就可能尋址到一台沒有對應的服務的機器,進而造成問題。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

我們在解決這個問題中,考慮了兩個方案,一種方式是基于應用提供出來的 Actuator 的資訊,定時地抓取應用釋出的服務的資訊,并且根據這些服務生成 DNS 記錄,服務的調用方去通路這些服務的時候,根據服務的元資訊拼裝出對應的 DNS 的位址,然後去調用。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

這種方式的問題是依賴于應用提供的 Actuator 的能力的,但是并不是所有的應用都有 Actuator,如果沒有的話,還是需要一定程度的改造,另一個是 DNS 的記錄的時效性的問題,大家知道,在容器時代,容器都是朝生夕滅的,意味着 DNS 的記錄會被頻繁的修改,而如果服務發現的資訊更新地不及時地話,調用就非常容易出問題。

另一個方式就是我們通過 Sidecar 來代理将服務注冊到服務注冊中心上面去,當一個 Python 或者 CPP 的系統啟動的時候,它需要主動告訴對應的 Sidecar,它需要釋出哪些服務,Sidecar 會将服務注冊到對應的服務注冊中心,當一個系統啟動的時候,他需要主動告訴對應的 Sidecar,它需要訂閱哪些服務,Sidecar 會從 Pilot 中将對應的服務的位址訂閱過來。

螞蟻金服SOFAMesh在多語言上的實踐 | CNUTCon 實錄

這種方式可以避免 DNS 記錄更新不及時的問題,但是同樣,有一定對應用的侵入性,但是我認為這種侵入在這種基于接口的服務發現的模型下是不可避免的,除非是基于應用的服務發現模型,這也是 Service Mesh 的抽象而引發出來的一些問題。但是我們可以讓 Sidecar 盡量簡單的 API 提供給應用來調用,因為這個注冊的行為是一次性的,不像真的服務發現那樣,需要維持長連結來保證用戶端得到及時的位址更新,是以我們在 Sidecar 中提供了 HTTP 接口應用來進行注冊服務和訂閱服務,而實際的注冊和訂閱的行為是通過 Sidecar 去做,Sidecar 會和 Pilot 維持長連結,保證服務發現的及時性。

3、輕量化用戶端

剛才說了服務發現,現在我們來看下另外的兩個 Case,可以更加說明輕量化用戶端的必要性。在螞蟻金服,因為單元化的架構,SOFARPC 需要有能力去提供基于使用者的 ID 的路由方式,也就是說,需要能夠根據使用者的 ID 的不同,将請求路由到不同的機房裡面去。但是大家知道,作為一個 RPC 架構,它是一個純技術的架構,沒法說直接了解什麼是使用者 ID,也沒法從 RPC 請求的參數裡面去識别出使用者 ID。

是以我們提供了注解的方式讓業務系統可以根據自己的情況去編寫使用者 ID 的提取規則,這樣,RPC 架構隻要在在尋址的時候,回調這個注解類,就可以拿到對應的使用者 ID,再和路由規則做對比,找到對應的機房。

但是這樣的方式,在多語言的實作的時候遇到了很大的問題,大家可以設想一下,你怎麼用 NodeJS 實作和 Java 的注解一樣的能力?你怎麼用 Golang 實作和 Java 注解一樣的能力?

但是這樣的能力又不能去掉,是以我們提供了一種折衷的辦法,通過 Velocity 的腳本來讓業務來編寫路由的規則,然後将 Velocity 的腳本翻譯成各個語言的版本,因為 Velocity 的腳本相對來說文法比較簡單,可以非常容易地就翻譯成各個語言,這些各個語言的版本會直接內建到對應的語言裡面去,通過這樣的方式來達到一次編寫,到處使用的目的。

除了一些涉及到業務邏輯的路由之外,還有一些能力是在 Service Mesh 中無法完全提供的,比如 Tracing 的能力,大家知道 Tracing 其實是一個很特殊的東西,一般上作為一個分布式鍊路追蹤的架構,至少需要三個資料需要在系統間傳遞,TraceId,SpanId 和 BaggageItems,當一個系統接收到上遊系統傳過來的 TraceId,SpanId 和 BaggageItem 的時候,它必須從請求中将資料反序列化出來,塞到線程上下文中,當從目前系統中送出請求的時候,又需要将線程上下文中的 TraceId,SpanId 和 BaggageItem 讀出來,序列化到請求中。是以 Service Mesh 能夠為 Tracing 提供的能力是根據協定擷取一些服務的中繼資料,并且能夠知道服務調用成功還是失敗,知道服務往哪裡調用了等等,但是還需要各個語言的系統來實作資料的傳遞動作。

4、總結

總結一下的話,做到多語言網絡通信這件事情,保持語言的中立特别重要,從研發同學的思維,到架構的設計,到代碼的實作,都得想着這個事情。另外,Service Mesh 雖然看起來很好,但是落地的時候,請準備好妥協的準備,并且也需要你知道 Service Mesh 的能與不能,能的地方是否對你有足夠大的吸引力,不能的地方你是否又有辦法補上。

原文釋出時間為:2018-11-22

本文作者:SOFA 黃挺

本文來自雲栖社群合作夥伴“

金融級分布式架構

”,了解相關資訊可以關注“

”。