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

螞蟻金服分散式架構發表於2018-12-10

本文作者:黃挺,螞蟻金服高階技術專家,螞蟻金服分散式架構 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 的能與不能,能的地方是否對你有足夠大的吸引力,不能的地方你是否又有辦法補上。


完整 PPT 地址:

http://www.sofastack.tech/posts/2018-11-22-01

或點選文字底部“閱讀全文”直接訪問


相關連結

SOFA 文件: http://www.sofastack.tech/

SOFA: https://github.com/alipay

SOFARPC: https://github.com/alipay/sofa-rpc

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


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

長按關注,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay




相關文章