在用一項技術前,一定要知道它的優點和缺點,它的優點是否對你有足夠的吸引力,它的缺點不足你是否有辦法補上。黃挺在CNUTCon全球運維大會上的分享也很不錯。
黃挺,螞蟻金服高級技術專家,螞蟻金服分布式架構 SOFA 的開源負責人。目前在螞蟻金服中間件團隊負責應用框架與服務化相關的工作。
大家好,我是來自於螞蟻金服的黃挺,花名魯直,目前在螞蟻金服負責微服務團隊,也是 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 等等。
在 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 為中心的。
我們再來看下 Dubbo 的協議設計,在 Dubbo 的協議設計里面,也基本上分成了三個部分,一個部分是協議版本,一個部分是協議的服務元數據信息,然后是協議體部分,Dubbo 的通信協議設計比早期的 SOFARPC 的通信協議的設計好的地方在於,Dubbo 的通信協議的第二個部分和第三個部分是分開來的,因此,當你需要讀取第二個部分的服務的元數據信息的時候,不需要同時地去讀取第三個部分,這樣,無論是多語言客戶端包還是 Sidecar,都會比較容易處理,並且性能上會比較好;但是 Dubbo 的協議設計一個敗筆在於把 Hessian 作為了超一等公民來對待的,它的整個協議就是構建在 Hessian 的基礎上的,它用 Hessian 把協議頭給序列化了,它用 Hessian 把協議的服務元數據信息也給序列化了,它還用 Hessian 來序列化協議體。這樣,當其他的語言需要去理解這個 Dubbo 協議的時候,必須要先理解 Hessian,而 Hessian 的多語言的支持做地並不是非常好,比如 Golang,就沒有一個比較好的 Hessian 的庫,給其他語言去理解 Dubbo 協議設置了障礙。
在最近 SOFARPC 的版本中,我們重新設計整個通信協議,說是重新設計,不如說是簡化,其實最主要的變化就是在原來的設計中,我們的第二部分的服務元數據信息以及第三部分的協議體是放在一個對象中,然后進行 Hessian 的序列化的;而現在是單獨拿出來了,並且使用類似於 URL 的 Query String 這樣簡單的 KV 結構來序列化,這樣當其他的語言需要讀取服務元數據的信息的時候,非常簡單地就可以將服務元數據的信息給提取出來,當需要增加服務元數據的字段的時候,也只需要在 KV 結構里面增加即可,擴展性上也非常方便。並且,Sidecar 也可以非常容易地將這些元數據信息提取出來,用來完成服務發現,限流,熔斷等等之類的事情。
所以對於一個通信協議來說,要做到比較好地適配不用語言通信的場景,適配 Service Mesh 的場景,在協議的設計上,協議頭的信息必須做到容易提取,容易擴展,否則,無論是在 Sidecar 里面,還是在多語言客戶端里面,處理起來都會非常困難。
前面我們已經說了通信協議的設計的重要性,但是除了通信協議,對於序列化協議的選擇也非常關鍵,為了能夠保證的語言的中立,必須避免序列化協議和特定的語言綁定在一起,另外,在一家公司中,一旦選定了一個序列化協議,想要替換掉,是非常困難的,在螞蟻金服,一直以來序列化協議都是采用了 Hessian,雖然 Hessian 號稱也是多語言的,但是實際上語言的支持有限,並且 Hessian 最近這幾年的發展也比較慢,另外,Hessian 也有一些特性是專門針對 Java 語言做的,比如一些父子類的字段的覆蓋關系的處理等等,這些特性在其他的語言中並不存在,會導致不同語言之間的兼容性問題。因此,我們在做多語言的時候,讓 SOFARPC 支持了 PB 協議,在 Bolt 的通信協議中,協議體里面的數據可以采用 PB 做序列化,PB 相對於 Hessian 來說在多語言的支持上要好上非常多。在這塊,我們的處理的辦法也是比較溫和的,對於用 Java 研發的 SOFA 系統來說,它可以即暴露提供 PB 協議的接口,又提供 Hessian 協議的接口,這樣,一些原來用 Hessian 做序列化調用這個系統的系統就不用做任何修改。
2、服務發現
前面講了在通信協議的設計的重要性,接下來我們就來講一講服務發現上的一些問題,假設一個 RPC 沒有服務發現的能力,基本上它就算是一個玩具,之所以一個 RPC 框架能夠滿足大規模分布式場景下的要求,服務發現的能力是非常基本的。
我們可以先看下左邊的這張圖,左邊的這張圖是 SOFARPC 當前的服務發現的模型,和大家看到的國內的一些開源的 RPC 框架基本上類似,因為最早 SOFARPC 就是為了方便 Java 而設計的,所以也是設計成盡量讓服務調用和本地調用一樣,而服務發現的粒度也是接口的維度,也就是說當一個應用要調用另一個應用發布的服務的時候,它是按照服務的接口信息從服務注冊中心上去尋找服務的提供方的地址的,並且服務的提供方也是按照接口來將服務注冊到服務注冊中心下的。在螞蟻金服里面,大部分的情況下,同一個應用的不同的實例提供了的服務的是一模一樣的,但是也有一些情況下,同一個應用的不同的實例提供了不同的服務。
這樣在社區的服務發現的方案里面就會存在問題,在 Istio 里面,服務的注冊是直接注冊成一個 K8s 的 Service,雖然在 K8s 里面叫做 Service,但是實際上就是一個應用,當服務的調用方需要去調用這個服務的時候,是直接獲取對應的應用的 Service 的里面的地址,本質上,這種服務發現的方式和基於 DNS 的服務發現的方式非常類似,當同一個應用的不同的實例發布的服務是不對等的時候,客戶端就可能尋址到一台沒有對應的服務的機器,從而造成問題。
我們在解決這個問題中,考慮了兩個方案,一種方式是基於應用提供出來的 Actuator 的信息,定時地抓取應用發布的服務的信息,並且根據這些服務生成 DNS 記錄,服務的調用方去訪問這些服務的時候,根據服務的元信息拼裝出對應的 DNS 的地址,然后去調用。
這種方式的問題是依賴於應用提供的 Actuator 的能力的,但是並不是所有的應用都有 Actuator,如果沒有的話,還是需要一定程度的改造,另一個是 DNS 的記錄的時效性的問題,大家知道,在容器時代,容器都是朝生夕滅的,意味着 DNS 的記錄會被頻繁的修改,而如果服務發現的信息更新地不及時地話,調用就非常容易出問題。
另一個方式就是我們通過 Sidecar 來代理將服務注冊到服務注冊中心上面去,當一個 Python 或者 CPP 的系統啟動的時候,它需要主動告訴對應的 Sidecar,它需要發布哪些服務,Sidecar 會將服務注冊到對應的服務注冊中心,當一個系統啟動的時候,他需要主動告訴對應的 Sidecar,它需要訂閱哪些服務,Sidecar 會從 Pilot 中將對應的服務的地址訂閱過來。
這種方式可以避免 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