2018年上半年,螞蟻金服決定基於 Istio 訂製本身的 ServiceMesh 解決方案,並在6月底正式對外公佈了 SOFAMesh。
git在 SOFAMesh 的開發過程當中,針對遇到的實際問題,咱們給出了一套名爲 x-protocol 的解決方案,本文將會對這個解決方案進行詳細的講解,後面會有更多內容,歡迎持續關注本系列文章。github
在 Istio 和 Envoy 中,對通信協議的支持,主要體如今 HTTP/1.1和 HTTP/2上,而咱們 SOFAMesh,則須要支持如下幾個 RPC 協議:網絡
SOFARPC:這是螞蟻金服大量使用的RPC協議(已開源)架構
HSF RPC:這是阿里集團內部大量使用的RPC協議(未開源)併發
Dubbo RPC: 這是社區普遍使用的RPC協議(已開源)負載均衡
對於服務間通信解決方案,性能永遠是一個值得關注的點。而 SOFAMesh 在項目啓動時就明確要求在性能上要有更高的追求,爲此,咱們不得不在 Istio 標準實現以外尋求能夠獲取更高性能的方式,好比支持各類 RPC 協議。框架
期間有兩個發現:分佈式
Istio 在處理全部的請求轉發如 REST/gRPC 時,會解碼整個請求的 header 信息,拿到各類數據,提取爲 Attribute,而後以此爲基礎,提供各類豐富的功能,典型如 Content Based Routing。ide
而在測試中,咱們發現:解碼請求協議的 header 部分,對 CPU 消耗較大,直接影響性能。
所以,咱們有了一個很簡單的想法:是否是能夠在轉發時,不開啓部分功能,以此換取轉發過程當中的更少更快的解碼消耗?畢竟,不是每一個服務都須要用到 Content Based Routing 這樣的高級特性,大部分服務只使用 Version Based Routing,尤爲是使用 RPC 通信協議的服務,沒有HTTP那麼表現力豐富的 header,對 Content Based Routing 的需求要低不少。
此外,對於部分對性能有極高追求的服務,不開啓高級特性而換取更高的性能,也是一種知足性能要求的折中方案。考慮到系統中總存在個別服務對性能很是敏感,咱們以爲 Service Mesh 提供一種性能能夠接近直連的方案會是一個有益的補充。爲了知足這些特例而不至於所以總體否決 Service Mesh 方案,咱們須要在 Service Mesh 的大框架下提供一個折中方案。
在咱們進一步深刻前,咱們先來探討一下實現請求轉發的技術細節。
有一個關鍵問題:當 Envoy/SOFA MOSN 這樣的代理程序,接收到來自客戶端的TCP 請求時,須要得到哪些信息,才能夠正確的轉發請求到上游的服務器端?
首先,毫無疑問的,必須拿到 destination 目的地,也就是客戶端請求必須經過某種方式明確的告之代理該請求的 destination,這樣代理程序才能根據這個 destionation去找到正確的目標服務器,而後纔有後續的鏈接目標服務器和轉發請求等操做。
Destination 信息的表述形式可能有:
1. IP地址
多是服務器端實例實際工做的 IP 地址和端口,也多是某種轉發機制,如Nginx/HAProxy 等反向代理的地址或者 Kubernetes 中的 ClusterIP。
舉例:「192.168.1.1:8080」是實際IP地址和端口,「10.2.0.100:80」是 ngxin 反向代理地址,「172.168.1.105:80」是Kubernetes的ClusterIP。
2. 目標服務的標識符
可用於名字查找,如服務名,可能帶有各類前綴後綴。而後經過名字查找/服務發現等方式,獲得地址列表(一般是IP地址+端口形式)。
舉例:「userservice」是標準服務名, 「com.alipay/userservice」是加了域名前綴的服務名, 「service.default.svc.cluster.local」是k8s下完整的全限定名。
Destination信息在請求報文中的攜帶方式有:
1. 經過通信協議傳遞
這是最多見的形式,標準作法是經過header頭,典型如HTTP/1.1下通常使用 host header,舉例如「Host: userservice」。HTTP/2下,相似的使用「:authority」 header。
對於非HTTP協議,一般也會有相似的設計,經過協議中某些字段來承載目標地址信息,只是不一樣協議中這個字段的名字各有不一樣。如SOFARPC,HSF等。
有些通信協議,可能會將這個信息存放在payload中,好比後面咱們會介紹到的dubbo協議,致使須要反序列化payload以後才能拿到這個重要信息。
2. 經過TCP協議傳遞
這是一種很是特殊的方式,經過在TCP option傳遞,上一節中咱們介紹Istio DNS尋址時已經詳細介紹過了。
如何從請求的通信協議中獲取destination?這涉及到具體通信協議的解碼,其中第一個要解決的問題就是如何在連續的TCP報文中將每一個請求內容拆分開,這裏就涉及到經典的TCP沾包、拆包問題。
轉發請求時,因爲涉及到負載均衡,咱們須要將請求發送給多個服務器端實例。所以,有一個很是明確的要求:就是必須以單個請求爲單位進行轉發。即單個請求必須完整的轉發給某臺服務器端實例,負載均衡須要以請求爲單位,不能將一個請求的多個報文包分別轉發到不一樣的服務器端實例。因此,拆包是請求轉發的必備基礎。
因爲篇幅和主題限制,咱們不在這裏展開TCP沾包、拆包的原理。後面針對每一個具體的通信協議進行分析時再具體看各個協議的解決方案。
RequestId用來關聯request和對應的response,請求報文中攜帶一個惟一的id值,應答報文中原值返回,以便在處理response時能夠找到對應的request。固然在不一樣協議中,這個參數的名字可能不一樣(如streamid等)。
嚴格說,RequestId對於請求轉發是可選的,也有不少通信協議不提供支持,好比經典的HTTP1.1就沒有支持。可是若是有這個參數,則能夠實現多路複用,從而能夠大幅度提升TCP鏈接的使用效率,避免出現大量鏈接。稍微新一點的通信協議,基本都會原生支持這個特性,好比SOFARPC、Dubbo、HSF,還有HTTP/2就直接內建了多路複用的支持。
HTTP/1.1不支持多路複用(http1.1有提過支持冪等方法的pipeline機制可是未能普及),用的是經典的ping-pong模式:在請求發送以後,必須獨佔當前鏈接,等待服務器端給出這個請求的應答,而後才能釋放鏈接。所以HTTP/1.1下,併發多個請求就必須採用多鏈接,爲了提高性能一般會使用長鏈接+鏈接池的設計。而若是有了requestid和多路複用的支持,客戶端和Mesh之間理論上就能夠只用一條鏈接(實踐中可能會選擇創建多條)來支持併發請求:
而Mesh與服務器(也多是對端的Mesh)之間,也一樣能夠受益於多路複用技術,來自不一樣客戶端而去往同一個目的地的請求能夠混雜在同一條鏈接上發送。經過RequestId的關聯,Mesh能夠正確將reponse發送到請求來自的客戶端。
因爲篇幅和主題限制,咱們不在這裏展開多路複用的原理。後面針對每一個具體的通信協議進行分析時再具體看各個協議的支持狀況。
請求轉發參數總結
上面的分析中,咱們能夠總結到,對於Sidecar,要正確轉發請求:
必須獲取到destination信息,獲得轉發的目的地,才能進行服務發現類的尋址
必需要可以正確的拆包,而後以請求爲單位進行轉發,這是負載均衡的基礎
可選的RequestId,這是開啓多路複用的基礎
所以,這裏咱們的第一個優化思路就出來了:儘可能只解碼獲取這三個信息,知足轉發的基本要求。其餘信息若是有性能開銷則跳過解碼,所謂「快速解碼轉發」。基本原理就是犧牲信息完整性追求性能最大化。
而結合上一節中咱們引入的DNS通用尋址方案,咱們是能夠從請求的TCP options中獲得ClusterIP,從而實現尋址。這個方式能夠實現不解碼請求報文,尤爲是header部分解碼destination信息開銷大時。這是咱們的第二個優化思路:跳過解碼destination信息,直接經過ClusterIP進行尋址。
具體的實現則須要結合特定通信協議的實際狀況進行。
如今咱們開始,以Proxy、Sidecar、Service Mesh的角度來看看目前主流的通信協議和咱們前面列舉的須要在SOFAMesh中支持的幾個協議。
SOFARPC 是一款基於 Java 實現的 RPC 服務框架,詳細資料能夠查閱 官方文檔。SOFARPC 支持 bolt,rest,dubbo 協議進行通訊。REST、dubbo後面單獨展開,這裏咱們關注bolt協議。
bolt 是螞蟻金服集團開放的基於 Netty 開發的網絡通訊框架,其協議格式是變長,即協議頭+payload。具體格式定義以下,以request爲例(response相似):
咱們只關注和請求轉發直接相關的字段:
TCP拆包
bolt協議是定長+變長的複合結構,前面22個字節長度固定,每一個字節和協議字段的對應如圖所示。其中classLen、headerLen和contentLen三個字段指出後面三個變長字段className、header、content的實際長度。和一般的變長方案相比只是變長字段有三個。拆包時思路簡單明瞭:
先讀取前22個字節,解出各個協議字段的實際值,包括classLen,headerLen和contentLen
按照classLen、headerLen和contentLen的大小,繼續讀取className、header、content
Destination
Bolt協議中的header字段是一個map,其中有一個key爲「service」的字段,傳遞的是接口名/服務名。讀取稍微麻煩一點點,須要先解碼整個header字段,這裏對性能有影響。
RequestId
Blot協議固定字段中的requestID
字段,能夠直接讀取。
SOFARPC中的bolt協議,設計的比較符合請求轉發的須要,TCP拆包,讀取RequestID,都沒有性能問題。只是Destination的獲取須要解碼整個header,性能開銷稍大。
總結:適合配合DNS通用解碼方案,跳過對整個header部分的解碼,從而提高性能。固然因爲這個header自己也不算大,優化的空間有限,具體提高須要等對比測試的結果出來。
HSF協議是通過精心設計工做在4層的私有協議,因爲該協議沒有開源,所以不便直接暴露具體格式和字段詳細定義。
不過基本的設計和bolt很是相似:
採用變長格式,即協議頭+payload
在協議頭中能夠直接拿到服務接口名和服務方法名做爲Destination
有RequestID字段
基本和bolt一致,考慮到Destination能夠直接讀取,比bolt還要方便一些,HSF協議能夠說是對請求轉發最完美的協議。
總結:目前的實現方案也只解碼了這三個關鍵字段,速度足夠快,不須要繼續優化。
Dubbo協議也是相似的協議頭+payload的變長結構,其協議格式以下:
其中long類型的id
字段用來把請求request和返回的response對應上,即咱們所說的RequestId
。
這樣TCP拆包和多路複用都輕鬆實現,稍微麻煩一點的是:Destination在哪裏?Dubbo在這裏的設計有點不夠理想,在協議頭中沒有字段能夠直接讀取到Destination,須要去讀取data字段,也就是payload,裏面的path字段一般用來保存服務名或者接口名。method字段用來表示方法名。
從設計上看,path字段和method字段被存放在payload中有些美中不足。慶幸的是,讀取這兩個字段的時候不須要完整的解開整個payload,好險,否則,那性能會無法接受的。
以hession2爲例,data字段的組合是:dubbo version + path + interface version + method + ParameterTypes + Arguments + Attachments。每一個字段都是一個byte的長度+字段值的UTF bytes。所以讀取時並不複雜,速度也足夠快。
基本和HSF一致,就是Destination的讀取稍微麻煩一點,放在payload中的設計讓人嚇了一跳,好在有驚無險。總體說仍是很適合轉發的。
總結:同HSF,不須要繼續優化。
HTTP/1.1的格式應該你們都熟悉,而在這裏,不得不指出,HTTP/1.1協議對請求轉發是很是不友好的(甚至能夠說是惡劣!):
HTTP請求在拆包時,須要先按照HTTP header的格式,一行一行讀取,直到出現空行表示header結束
而後必須將整個header的內容所有解析出來,才能取出Content-Length header
經過Content-Length
值,才能完成對body內容的讀取,實現正確拆包
若是是chunked方式,則更復雜一些
Destination一般從Host
header中獲取
沒有RequestId,徹底沒法實現多路複用
這意味着,爲了完成最基本的TCP拆包,必須完整的解析所有的HTTP header信息,沒有任何能夠優化的空間。對比上面幾個RPC協議,輕鬆自如的快速獲取幾個關鍵信息,HTTP無疑要重不少。這也形成了在ServiceMesh下,HTTP/1.1和REST協議的性能老是和其餘RPC方案存在巨大差別。
對於註定要解碼整個header部分,徹底沒有優化空間可言的HTTP/1.1協議來講,Content Based Routing 的解碼開銷是必須付出的,不管是否使用 Content Based Routing 。所以,快速解碼的構想,對HTTP/1.1無效。
總結:受HTTP/1.1協議格式限制,上述兩個優化思路都沒法操做。
做爲HTTP/1.1的接班人,HTTP/2則表現的要好不少。
備註:固然HTTP/2的協議格式複雜多了,因爲篇幅和主題的限制,這裏不詳細介紹HTTP/2的格式。
首先HTTP/2是以幀的方式組織報文的,全部的幀都是變長,固定的9個字節+可變的payload,Length字段指定payload的大小:
HTTP2的請求和應答,也被稱爲Message,是由多個幀構成,在去除控制幀以外,Message一般由Header幀開始,後面接CONTINUATION幀和Data幀(也可能沒有,如GET請求)。每一個幀均可以經過頭部的Flags字段來設置END_STREAM標誌,表示請求或者應答的結束。即TCP拆包的問題在HTTP/2下是有很是標準而統一的方式完成,徹底和HTTP/2上承載的協議無關。
HTTP/2經過Stream內建多路複用,這裏的Stream Identifier
扮演了相似前面的RequestId
的角色。
而Destination信息則經過Header幀中的僞header :authority
來傳遞,相似HTTP/1.1中的Host
header。不過HTTP/2下header會進行壓縮,讀取時稍微複雜一點,也存在須要解壓縮整個header幀的性能開銷。考慮到拆包和獲取RequestId都不須要解包(只需讀取協議頭,即HTTP/2幀的固定字段),速度足夠快,所以存在很大的優化空間:不解碼header幀,直接經過DNS通用尋址方案,這樣性能開銷大爲減小,有望得到極高的轉發速度。
總結:HTTP/2的幀設計,在請求轉發時表現的很是友好。惟獨Destination信息放在header中,會形成必須解碼header幀。好在DNS通用尋址方案能夠彌補,實現快速解碼和轉發。
在文章的最後,咱們總結並探討一下,對於Service Mesh而言,什麼樣的RPC方案是最理想的?
必須能夠方便作TCP拆包,最好在協議頭中就簡單搞定,標準方式如固定協議頭+length字段+可變payload。HSF協議、 bolt協議和dubbo協議表現完美,HTTP/2採用幀的方式,配合END_STREAM標誌,方式獨特但有效。HTTP/1.1則是反面典型。
必須能夠方便的獲取destination字段,一樣最好在協議頭中就簡單搞定。HSF協議表現完美,dubbo協議藏在payload中但終究仍是能夠快速解碼有驚無險的過關,bolt協議和HTTP/2協議就很遺憾必須解碼header才能拿到,好在DNS通用尋址方案能夠彌補,但終究丟失了服務名和方法名信息。HTTP/1.1依然是反面典型。
最好有RequestId字段,一樣最好在協議頭中就簡單搞定。這方面HSF協議、dubbo協議、bolt協議表現完美,HTTP/2協議更是直接內建支持。HTTP/1.1繼續反面典型。
所以,僅以方便用最佳性能進行轉發,對 Service Mesh、sidecar 友好而言,最理想的 RPC 方案是:
傳統的變長協議
固定協議頭+length 字段+可變 payload,而後在固定協議頭中直接提供 RequestId 和destination。
基於幀的協議
以 HTTP/2 爲基礎,除了請求結束的標誌位和 RequestId 外,還須要經過幀的固定字段來提供 destination 信息。
或許,在將來,在 Service Mesh 普及以後,對 Service Mesh 友好成爲 RPC 協議的特別優化方向,咱們會看到表現完美更適合Service Mesh時代的新型 RPC 方案。
相關連接:
SOFA 文檔: http://www.sofastack.tech/
SOFA: https://github.com/alipay
SOFAMosn:https://github.com/alipay/sofa-mosn
SOFAMesh:https://github.com/alipay/sofa-mesh
長按關注,獲取分佈式架構乾貨
歡迎你們共同打造 SOFAStack https://github.com/alipay