單體應用
我們剛開始的服務,其實並沒有那麼複雜。我只有一臺配置非常低的機器,我的應用,我的代碼,我的聰明才智,全部在這一個小小的工程裏面。
由於我是搞 IT 的,所以我的項目名字就叫 jisuanji。有人說我用中文拼音做項目名,太那個。
我不聽,我就是這麼命名。我還把公共模塊叫 gg,密碼字段叫 mm,誰管得着呢。
對,看下面的圖,就是這麼簡單。項目能活到用 Nginx 來做負載均衡這一步,就算是小成功了。
這個時候,所有的代碼就是一個整體,用戶訪問什麼,我直接給就是。
兩個服務
可能是我和我一樣二的人有點多,我的項目訪問量越來越大,這也許就叫臭味相投吧。
我自己的開發速度,已經追不上頭腦裏的 Idea,是時候招個人對服務進行拆分了。
不能拆的太過火,所以剛開始,我把 jisuanji 拆成了兩個服務。其中的服務 B,僅僅部署了一個節點,因爲它的壓力還不是太大。
即使這樣,我不得不買上 3 臺服務器來部署服務節點,真是肉痛。我這麼摳門的人,數據庫當然也是共用的。雖然有時候機器壓力有點大,但暫時還死不了人。
這個時候我就面臨了一個選擇問題:服務 A 要怎麼訪問服務 B 呢?
由於我搞過一段時間的 Web Service,首先就想到了它。但這玩意太重了,我還不如通過 HTTP 訪問來的舒爽。
通過 HTTP Client,或者 Ok Http,我的服務 A,現在可以直接模擬 HTTP 請求訪問服務 B 了。
當團隊裏有第二個人,就開始吐槽我的項目了。以下是他羅列的,我的項目的罪狀:
-
複雜度太高,代碼嚴重耦合。
-
技術債務多,拍腦袋需求一籮筐。
-
代碼不規範,一坨屎。
-
技術創新難,一個類幾千行…
至於麼?從一個服務拆成兩個,就這麼吐槽我。不過爲了以後能拆出成百上千個服務,這口氣我暫時忍了,畢竟我這人還是比較虛心的。
亂成一鍋粥了
等過去半年一看,好傢伙,服務給我拆了了幾十個。當我的同伴把系統結構圖拿給我看,我直接懵逼了。
我挑了 9 個能看的服務,畫了張圖:
首先進行了業務拆分。比如支付業務,訂單業務,用戶中心,商品中心等,都組建了獨立的團隊。每個業務又進行了細分,拆分成不同的服務。
在這之間,進行了下面的改動:
-
有小夥伴寫了個通用的 HTTP Client 調用組件,自己的負載均衡策略。
-
有另外一個小夥伴,習慣 Protobuf,所以選了 gRPC。
-
事實證明 SOA 還是有市場的,這不,就有幾個服務的交互引入了 Web Service。
-
有人想要用 RMI,被我及時發現、否決,腹死胎中了。
-
每次建個新服務,都需要更新一下 Excel,然後將這個 Excel 周知出去。
現在的整個系統,簡直是個四不像。什麼通信方式都有,什麼交互格式都不缺。
拿最要命的 D 服務來說,光通訊模塊,就引入了 20 幾個 Jar 包。如果應用擴展到上千個…My God…
更要命的是,這麼多服務,每次上線一個模塊都膽戰心驚,因爲他不知道到底會有什麼連鎖反應。
是時候叫出超級飛俠了。哦不,叫出微服務了。
微服務來襲
目前,最火的微服務框架,就是 Spring Cloud 了。雖然 Netflix 公司對某些組件的維護經常爽約,但有些核心組件還是非常經典的。
註冊中心:Eureka
服務 A,怎麼找到服務 B,有很多種方式。比如你生活在一個小鎮上,你問 xjjdog 是誰,老王可能認識他,但小李可能並不知曉;但小李認識老王,所以通過他最終也能找到 xjjdog,只不過麻煩一些。
你可以隨便拉小鎮上的一個人,來問 xjjdog 是誰。你還會變戲法一樣拿出一個小本本,把你認識的人,都告訴他們。當你腦殘式的問了一個遍,到最後所有人都知道 xjjdog 了。
上面說的就是 Gossip 協議。最終,你們都能夠知道彼此,因爲都是大嘴巴。
比如小鄭生了個孩子,過不了多少時間,全鎮子的人都把這個孩子記錄在本子上了。用這種方式,服務都能夠知道彼此,完成通信。
可惜這並不美好,從小鎮的東頭跑到西頭,需要很長時間。在這個時間裏,小鄭剛生的孩子可能因爲先天疾病夭折了。我們需要一種信息集中度和實效性更高的方式。
這就需要一箇中心,那裏的信息就是權威。 在 Spring Cloud 體系中,最常用的註冊中心就是 Eureka。
任何服務啓動以後,都會把自己註冊到 Eureka 的註冊表中;當服務死亡的時候,也會通知 Eureka。
這樣,當服務 A 想要找服務 B 的時候,只需要問一下 Eureka Server 就可以了,它什麼都知道。
爲了達到這個目的,還是要有一部分工作量的。且看下圖。這個註冊動作,是由一個叫做 Eureka Client 的組件來完成的。
服務啓動和關閉的時候,會通過這個組件註銷自己;而當服務 A 想要調用服務 B 的時候,直接問 Eureka Server 就可以了。服務 A 拿到結果後,會把結果緩存在本地的註冊表裏。
你可以認爲是一個拷貝。所以 Eureka Server 死掉後,並不影響服務 A 找到服務 B。
負載均衡組件:Ribbon
現在問題來了。服務 A 拿到服務 B 的實例列表以後,發現有兩臺。
10.0.0.12
10.0.0.16
接下來麻煩了,該調哪臺機器呢?這就是 Spring Cloud 中組件 Ribbon 的作用。
其實 Round Robin 是一個通用的計算機術語。它是最常用的負載均衡策略,請求會均勻的分配給後面的每臺服務器。
Ribbon 工作時,會做下面四件事:
-
優先選擇在一個 Zone 且負載較少的 Eureka Server,進行連接。
-
定期從 Eureka 更新、過濾服務和實例列表。
-
根據負載均衡策略,從註冊表中選擇一個真正的實例地址。
-
通過 Rest Client 對服務發起調用。
可以看到,Ribbon 背後,還是採用的 HTTP 協議進行交互。看以下代碼,就可以直接實現對遠端服務的調用:
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
...
@Autowired
RestTemplate restTemplate;
public String test() {
return restTemplate.getForObject("http://test-service/test", String.class);
}
Ribbon 的 Filter 會查找 Test-Service,並替換成相應的實例地址。
Ribbon 不僅僅提供了輪詢的策略,還有其他的,比如:
-
隨機 Random
-
根據響應時間加權
-
自定義
拿輪詢來說,最終的選擇邏輯就在 RoundRobinRule 類中:
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
爲簡化代碼而生:Feign
可以看到,Ribbon 需要自己構建 HTTP 請求,模擬 HTTP 請求然後使用 RestTemplate 發送給其他服務,步驟相當繁瑣。而且返回類型不安全,也表達不出什麼語義。
其實,通過 Ribbon 方式,已經能夠完成微服務之間的調用了。但 Spring Cloud 的開發語言是 Java,肯定要進行更加高級的封裝,才能體現它的逼格。
Feign 得益於 Java 的動態代理機制,最終封裝出一套簡潔的接口調用方式,將需要調用的其他服務的方法定義成抽象方法即可,不需要自己構建 HTTP 請求。
首先,Feign 會根據 @FerignClient 註解,通過動態代理,創建一個動態代理類。
接下來,你只要通過調用接口的方式,就可以構造上面提到的 Ribbon 調用參數,這個過程會自動填充。最後,通過構造的 Ribbon 請求,發起真正的調用,並通過反射組裝返回值。
所以,Feign 只是一層皮,最終還是要通過 Ribbon 進行調用。在我看來,把 Ribbon 和 Feign 合成一個組件,也是合理的。
它們有一個比較通用的名詞,就叫做 RPC(遠程調用)。
異常的保護傘:斷路器 Hystrix
下面以一個支付請求爲例,說一下不是風平浪靜的情況下,服務會有什麼反應。
每一個真正的支付請求,都會調用其他四個服務。首先,使用鑑權服務,獲取用戶的支付權限;然後,風控服務會做一些規則驗證。
爲了更好的推銷產品,會調用營銷業務,獲取一些推薦信息;最後,調用聚合支付服務,進行真正的支付。
其中,營銷業務其實是可有可無的。讓用戶首先把錢花出去,是我們的首要任務。
考慮下面一種場景,營銷業務由於系統故障或者負載問題,發生了大面積的不可用或者超時。然後,所有的請求都卡在了獲取營銷信息的代碼上。
如圖所示,鑑權和風控都已經通過了。因爲一個旁路功能:營銷業務,導致真正的支付無法進行。這個時候,如果有人調用支付請求,會發現支付請求也出錯了。
因爲它們最終都卡在了營銷這一段小代碼上:
所以,對於營銷業務這種不是鏈路上必備的服務提供者,要有一個手段,讓它在發生問題的時候,隔離它一段時間。
負責這個功能的組件,就叫做 Hystrix。以我們編程的思維來說,這就是個 if 條件:
if(服務發生問題){
return "暫時不要處理";
}
但我們不能這麼編碼在業務代碼裏。所以 Hystrix 對每個服務開了一個線程池,並有比較複雜的規則,來控制這些出問題的服務的行爲。
比如,在2分鐘內,直接返回營銷業務的默認結果,而不是一直卡在那裏。
這個過程,就叫熔斷。就像電源一樣,出了問題,先切斷保險絲,別把電器給燒了。
此網關非彼網關:Zuul
API 網關是一個反向的路由,它屏蔽了內部的細節,爲調用者提供了統一的入口。
網關,其實是一堆過濾器的幾何,可以實現一系列和業務無關的橫切面功能。
熟悉 Spring 的都知道 AOP,路由的一個功能,就是針對於分佈式服務的一個 AOP。
還是先說下網關的職責吧。簡單羅列幾個:
-
安全認證。提供統一的認證方式和鑑權功能,避免重複開發。
-
熔斷,限流。針對問題服務,進行熔斷操作;對流量進行預估,限制訪問。
-
日誌監控。統一流量入口,進行流量分析和監控。
-
屏蔽內部細節,對外提供一致的接口。
-
實現灰度。使用自定義策略實現分流,達到測試的目的。
網關的位置,大體就如下圖:
可以看到,我們平常用的 Nginx,就可以當作網關。但對於微服務來說,Nginx 的配置實在是太麻煩了。
不是說 Nginx 功能不夠強大,而是因爲它們不是一個體系的,就存在整合成本(比如 Kong)。
Zuul 就不一樣了,它和 Spring Cloud 的其他組件,是一家子的。一家子的,當然會特殊照顧。
Zuul 本身就是一個 Servlet,外部請求經過一系列 Filter 後,會達到真正的服務。上面說的熔斷器,就是高度集成的。
一張聚合圖
有了上面關鍵組件,事情就明瞭的多了。我們把它放在一張圖中,就是下面的樣子:
我們將其簡化一下,就可以得到一張更簡潔的圖。可以看到,只需要 3 個關鍵點:
-
服務註冊中心,統一管理所有服務的信息,默認組件是 Eureka。
-
RPC,網絡通信組件,服務 A 怎麼調用服務 B。在 Spring Cloud 中,就是 Ribbon+Feign。
-
網關,拆分的服務怎麼暴露接口,最終見人的樣子。默認組件是 Zuul。