1. 程式人生 > >唯品會的Service Mesh三年進化史

唯品會的Service Mesh三年進化史

每種架構風格,都會因各公司面臨的情況不同而有不同的實現路線,Service Mesh也不例外,比如江南白衣描述的唯品會的服務化體系開放服務平臺OSP(Open Service Platform)走的ServiceMesh之路就與流行的Istio不同。但它們要解決的核心問題是相似的,比如服務的註冊發現、路由、熔斷等如何實現,如何高效的傳輸與序列化、代理,甚至包括如何更方便的對原有的系統升級改造等。


 

一、標準服務化體系階段

如果用兩根手指將Local Proxy 和 Remote Proxy的框框按住,就是個標準的服務化框架。

傳輸與序列化層

當時還沒有GRPC,自己拿Netty擼了一個TCP長連線,多路複用,非同步IO的傳輸層,基於Thrift協議的序列化。

當然,這個組合從效能上現在看也不過時,自定義TCP不比HTTP2弱, Thrift 也比ProtocolBuffer略快,我們還支援基於Java類來定義Thrift介面,而不是原生Thrift,PB的有額外學習成本的中立語言,完美。

缺點嘛,就是跨語言時會略尷尬,雖然原生的Thrift也在某種程度上跨語言,但我們除了保留Thrift序列化協議外,其他從頭到腳都重新實現了,也就不再能從它那借力。

服務註冊中心

當時的大環境下,ZooKeeper也是比較流行的註冊中心選擇,不過最近在做去ZK化,改為自研的Http Api Server,資料用Redis或 Etcd儲存。

API閘道器

當然要有入口閘道器,將外網的HTTP請求,經過認證,安全防刷, 再轉換為OSP呼叫後端服務。

服務路由

服務的註冊發現,負載均衡,路由定製,機房策略等路由策略,超時,重試,熔斷等可用性策略,我們統稱為路由邏輯。

我司的主力語言,前端是PHP 和 Java,後端是Java,在呼叫端有著硬性的跨語言需求。

SM第一步

上述的路由邏輯,如果在每種語言實現一次客戶端,顯然是不合算的。為了PHP,將這些邏輯抽取成獨立的Proxy。

SM第二步

這個獨立的Proxy,是部署成傳統的集中式的Proxy叢集,還是當時還不很著名的本地SideCar模式呢?

在公司的體量下,有著強烈的去中心化的願望,所以我們選了SideCar模式。

SM第三步

PHP使用了Proxy,那Java呢,繼續像dubbo一樣在客戶端裡可以嗎?

這個當時有比較大的爭議,因為畢竟多經過了一層節點,雖然本地連線不經過網路,也很難說效能毫無影響。 為此我們還專門做了個基於Unix Domain Socket的版本,但後來覺得效能足夠,一直沒上線。

最後,老闆力排眾議,一是為了架構統一性,二是因為我們的Proxy功能還不成熟,而使用它的應用又特別多,如果不獨立出來,很難推動快速升級。所以統一使用了Proxy模式。

SM第四步

SideCar模式雖好,但是存在單點的問題,如果SideCar在升級,或者掛掉了怎麼辦?

SideCar升級,可以聯動同一臺機上的應用先摘除流量。
SideCar掛掉,可以搞個指令碼把它自動重新拉起。但重新拉起的間隔裡還是會丟失的請求,如果重新拉起還是失敗呢?

作為一個架構師,高可用的觀念是深入到骨子裡的,所以我們又在每個機房搭建了一個Remote Proxy叢集,並在客戶端裡的SDK加入瞭如下的邏輯:

“如果本地Proxy不可用或宣稱自己準備關閉,就將請求無損轉發到Remote Proxy,再啟動一個監視器觀察本地Proxy什麼時候重新可用。”

至此,一個封閉的服務化框架完備了,我們有了標準的服務化體系。能力上,也比當時久不更新的Dubbo,還有後來的Netflix,SpringCloud要強不少。

 

二,按不住的多語言的客戶端接入

時間匆匆過, 一個公司裡,總是按不住會有更多的語言出現,比如Node.js, C++, Go,像前面說的,再擼一次TCP/Thrift 感覺有點累了,這些也不是主流語言,花太多時間在上面不值得。 所以,我們在Proxy上額外支援了HTTP/JSON的傳輸層,再根據註冊中心裡的元資料,重新序列化成Thrift來呼叫後端服務就好了。

我們還發現,在PHP裡用Thrift,還不如它內建的C寫的HTTP/JSON庫快。

不過,我們也不想在這些客戶端再實現一次本地SideCar與Remote Proxy的切換邏輯了,反正這些非主流語言的客戶端的呼叫量不會很大,就通通去呼叫Remote Proxy叢集好了。哪天撐不住時,可能會改用前面說的不那麼完美的方案。

至此,多語言的客戶端完全咩有接入限制了。

 

三, 普通Web應用也想零成本變身服務化

時間又匆匆過,又有大量原來的Web應用,不想改造成OSP應用,又想作為一個服務,或者接入API閘道器,或者加入服務化大家庭,享受各種服務治理的能力。

為此,我們開發了一個很輕量的註冊器,也是以sidecar的形式執行,不斷的對Web應用的做健康檢查,與註冊中心進行註冊,心跳,和反註冊。

當然,免費的午餐肯定沒那麼好吃,比如沒有了基於IDL生成SDK的與客戶端的契約化程式設計,沒有了高效的傳輸層序列化層,沒有了限流,自動隔離執行緒池,ClassLoader預熱,閒時主動GC等等細微的Runtime加強。

至此,多語言的服務端也完全咩有問題了。

 

四、容器化了,容器化了

為了容器化,sidecar跑在哪裡,又成了問題。 如果跑在每一個Pod裡,一來呢,身為Java應用,堆內堆外記憶體吃得有點多;二來呢,升級Proxy時,又要重新發布每一個應用;

所以,我們選擇了DaemonSet的形式,每臺宿主機上只執行一個Proxy,Proxy啟動時把自己的IP寫在一個共享檔案裡,這個檔案也Mount進各個容器裡面,各個客戶端會監聽這個檔案。

為了隔離性,Proxy加了個來源IP的限流,效果就是單個容器的呼叫高於2萬QPS時,第二萬零一個請求開始就把它臨時重定向到Remote Proxy叢集。 十秒鐘後再重試本地Proxy,如果還是高,又繼續打發過去。

另一個改動,我們之前基於IP來定義路由規則( 比如把一些消耗較大的介面,都發送到隔離的三臺機器上)。容器裡IP不固定了怎麼辦?

我們引入了一個部署池的概念,比如一個應用有兩個部署池,一個3臺機器,另一個20臺機器,這個部署池的名字,會註冊為服務例項的元資訊。在服務路由裡就用這個池的名字來定義規則。

Istio 的區別

1. Server端無入口Agent

Server端前面也擺一個Agent,全部流量都流經它,有點重啊。當初在Client前擺一個Agent都爭半天了,真的要再來一個?

如果已經是OSP應用,當然沒必要擺這個Agent了。如果是一個希望零成本變身的Web應用,那我們回顧一下服務端要做的事情:

一是服務註冊和心跳。 我們在應用旁邊擺了個輕量級的註冊與心跳器來實現。

二是分散式呼叫鏈記錄。分散式呼叫鏈監控原本就支援Web應用呀,不需要額外的Agent。

三是服務端授權,服務端限流之類必須在服務端進行的服務治理,相對沒那麼重要與常用,真的需要要時同樣以Servlet WebFilter 或多語言SDK包方式提供。

所以,這個偏重的Agent暫時沒有太大必要。

 

2. Client端不基於IPTable劫持

為了客戶端零改造成本,Istio裡基於IPTable,將Client發出的所有請求劫持到Proxy上。但IPTable的效能一般,尤其是一個環境裡有很多應用時,效能更加跌得很厲害。所以我們就不節約客戶端的一點點改造成本了。

基於SDK的訪問,支援Local Proxy與Remote Proxy的完美切換,比IPTable更加的高可用。而如果某種語不想寫這種複雜SDK,那要麼全部打到本地Proxy(等於IPTable),要麼全部直接訪問 Remote Proxy叢集。

 

3. 沒有畫大餅用的Mixer

為了Proxy實現的可替換性,Istio裡將與基礎設施相關的部分都提取到中央的Mixer裡,連分散式呼叫鏈跟蹤都分離出來太那啥了,每個請求發生前發生後都可能要呼叫一下這個Mixer,一個是效能,一個是中央化的容量瓶頸。

雖然理解Google架構師們為了畫大餅的無奈,但自用體系,不需要考慮Proxy的可替換性時,還是把該下沉到Proxy的基礎設施埋點給下沉下去。
 

4. SideCar以DaemonSet形式執行

因為是Java,堆內堆外要3G左右,所以每臺宿主機只執行一個Proxy。 當然也為了升級Proxy方便,不用每次升級Proxy將全部應用的容器重新發布一遍。

 

5. 不依賴K8S和容器技術

這麼一路走來,當然不會依賴於K8S。目前Istio們為了節約開發精力而完全依賴K8S,其實也大大侷限了它們的適用範圍,起碼一個公司裡如果混合物理機與容器雙向呼叫的就難搞了。

 

6. 路由和路由是不一樣的,熔斷和熔斷是不一樣的

一個詞能涵蓋完全不同級別的實現。大家都說路由,但我覺得比如Dubbo,比如OSP,才算是真正的規則路由。

其他負載均衡,熔斷,重試等等,都一個名字也可以做得差別極大。

http://calvin1978.blogcn.com/?p=1779