前言
看到文章的題目了嗎?就是這麼抽象和籠統的一個問題,確實是我面試中真實被問到的,某共享貨車平臺的真實面試問題。
SpringCloud確實是用過,但是那是三四年前了,那個時候SpringCloud剛開始流行沒多久,我們技術總監讓我們調研一下,然後算上我在內的三個同事就一人買了一本SpringCloud的書籍,開始看,開始研究,正好那個時候DDD也比較火,然後我們就一邊研究的SpringCloud一邊按照DDD的模型搭建自己的專案。
但是這個專案最後做了三個月,才完成了一期。後面二期還沒開始,我就撤了。所以SpringCloud總共的使用時間就兩三個月,所以對這部分知識掌握的並不紮實,而且入職了新公司之後,都是使用公司自己封裝的框架,也已經三年沒有用過SpringCloud了,這次是要面試換工作了,所以決定將這方面的知識,總結一下。
服務治理 Spring Cloud Eureka
我們之前在使用服務之間相互呼叫的時候,一般是靠一些靜態配置來完成的。比如服務A,要呼叫服務B來完成一個業務操作時,為了實現服務B的高可用,一般是通過手動配置來完成服務B的服務例項清單的維護。
隨著業務的發展,系統功能越來越複雜,相應的服務不斷增加,而且服務的IP還一直在變化,靜態配置來維護各服務,就會變得越來越困難。
這個時候就出現了服務治理框架,Spring Cloud Eureka。
Spring Cloud Eureka 主要是圍繞著服務註冊與服務發現機制來完成對微服務的自動化管理的。
服務註冊
Eureka提供了服務端元件,我們也稱為註冊中心。每個服務都向Eureka的服務註冊中心,登記自己提供服務的後設資料,包括服務的ip地址、埠號、版本號、通訊協議等。這樣註冊中心,就將各個服務維護在了一個服務清單中(雙層Map,第一層key是服務名,第二層key是例項名,value是服務地址加埠)。
服務註冊中心,還會以心跳的方式去監聽清單中的服務是否可用(預設30秒),若不可用(服務續約時間預設90秒),需從清單中剔除,達到排除故障服務的效果。
Eureka註冊中心提供了高可用方案,可以支援叢集部署註冊中心,然後多個註冊中心例項之間又相互註冊,這樣每個例項中都有一樣的服務清單了。
服務發現
Eureka不但提供服務端,還提供了客戶端,客戶端是在各個服務中執行的。
Eureka客戶端主要有兩個作用:
- 向註冊中心註冊自身提供的服務,並週期性的傳送心跳來更新它非服務租約。
- 同時,也能從服務端查詢當前註冊的服務資訊,並把他們快取到本地,並週期性的重新整理服務狀態。
在Eureka Server中註冊的服務,相互之間呼叫,不再是通過指定的具體例項地址,而是通過向服務名發請求實現呼叫,因為每個服務服務都是多例項,並且例項地址還有可能經常變。
但是通過服務名稱呼叫,並不知道具體的服務例項位置,因此需要向註冊中心諮詢,並獲取所有服務例項清單,然後實現服務的請求訪問。
舉例
服務A的一個業務操作,需要呼叫服務B和服務C來完成。
那麼服務A和服務B和服務C都將自己註冊到Eureka的註冊中心,然後服務A通過諮詢註冊中心,將註冊中心的服務列表清單快取到自己本地。
通過服務名稱獲取到服務B和服務C的服務例項地址,最後通過一種輪詢策略取出一個具體的服務例項地址來進行呼叫。
總結一下
Eureka Client : 主要是將服務本身註冊到Eureka Server中,同時查詢Eureka Server的註冊服務列表快取到本地。
Eureka Server:註冊中心,儲存了所有註冊服務的後設資料,包括ip地址,埠等資訊。
客戶端負載均衡 Spring Cloud Ribbon
服務的呼叫方,在通過Eureka Client快取到本地的登錄檔之後,通過服務名稱,找到具體的服務對應的例項地址,但是被呼叫方的服務地址是有多個的,那麼該用那個地址去進行呼叫呢?
服務A:
192.168.12.10:9001
192.168.12.11:9001
192.168.12.12:9001
這個時候Spring Cloud Ribbon就出現了,它是專門解決這個問題的,它的作用就是做負載均衡,會均勻的把請求分發到每臺機器上。
Ribbon預設使用Round Ribbon的策略進行負載均衡,具體就是採用輪詢的方式進行請求。
Ribbon除了有Round Ribbon這種輪詢策略,還有其他策略以及自定義策略。
主要有:
- RandomRole: 從服務例項清單中隨機選擇一個服務例項。
- RoundRobinRule: 按照線性輪詢的方式依次選擇每個服務例項。
- RetryRule:根據輪詢方式進行,且具備重試機制進行選擇例項。
- WeightedResponseTimeRule:對RoundRobinRule的擴充套件,增加了根據例項的執行情況來計算權重,並根據權重來挑選例項。
- ZoneAvoidanceRule:根據服務方的zone區域和可用性來輪詢選擇。
Spring Cloud Ribbon具體的執行示例如下:
例項程式碼
下面的程式碼就是通過Ribbon呼叫服務的程式碼例項。
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/ribbon-consumer")
public String helloConsumer(){
return restTemplate.getForEntity("http://example-service/index",String.class).getBody();
}
}
可以看到Ribbon也是通過發起http請求,來進行的呼叫,只不過是通過呼叫服務名的地址來實現的。雖然說Ribbon不用去具體請求服務例項的ip地址或域名了,但是每呼叫一個介面都還要手動去發起Http請求,也是比較繁瑣的,而且返回型別也比較抽象,所以Spring Cloud對呼叫方式進行了升級封裝。
宣告式服務呼叫 Spring Cloud Feign
Spring Cloud 為了簡化服務間的呼叫,在Ribbon的基礎上進行了進一步的封裝。單獨抽出了一個元件,就是Spring Cloud Feign。在引入Spring Cloud Feign後,我們只需要建立一個介面並用註解的方式來配置它,即可完成對服務提供方的介面繫結。
Spring Cloud Feign具備可插拔的註解支援,並擴充套件了Spring MVC的註解支援。
下面我們來看一個具體的例子:
服務方具體的介面定義與實現程式碼如下:
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 介面定義
*/
@FeignClient(value="service-hi",fallback = TestFeignServiceImpl.class)
public interface TestFeignService {
@RequestMapping(value="/hi",method = RequestMethod.GET)
String sayHi(@RequestParam("name") String name);
}
/**
* 具體的服務實現
*/
@Component
public class TestFeignServiceImpl implements TestFeignService {
@Override
public String sayHi(String name) {
return "你好,"+name;
}
}
呼叫方的使用程式碼如下:
@RestController
public class TestController
{
@Resource
private TestFeignService testFeignService;
@RequestMapping(value="/hi",method = RequestMethod.GET)
public String sayHi(@RequestParam String name)
{
// 呼叫遠端服務
return testFeignService.sayHi(name);
}
}
通過上面的程式碼,我們可以看到,呼叫方通過Feign程式遠端服務呼叫的時候,非常簡單,就向是在呼叫本地服務一樣。
像之前的建立連線,構造請求,發起請求,獲取響應,解析響應等等操作,對使用者來說都是透明化的,使用者不用關心服務是怎麼實現呼叫的,直接使用即可。
那麼Feign是如何實現這套封裝邏輯的呢?
其實Feign底層主要是靠動態代理來實現這整個服務的呼叫過程的。
主要邏輯如下:
- 如果一個介面上定義了@FeignClient註解,Feign就會根據這個介面生成一個動態代理類。
- 如果呼叫方,在呼叫這個定義了@FeignClient註解的介面時,本質上是會呼叫Feign生成的代理類。
- Feign生成的動態代理類,會根據具體介面方法中的@RequestMapping等註解,來動態構造出需要請求的服務地址。
- 最後針對這個地址,再通過Ribbon發起服務呼叫,解析響應等操作。
因為Spring Cloud Feign的使用方式比Spring Cloud Ribbon更方便,所以一般專案中都是使用Feign,而且Feign還有繼承特性,可以將遠端服務介面繼承過來然後再進行自己的個性化擴充套件。因此Feign的使用範圍以及普及率更高一些。
服務容錯保護 Spring Cloud Hystrix
在微服務架構中,我們將系統拆分成多個服務單元,各個服務之間通過服務註冊與訂閱的方式互相依賴。
我們以一個電商網站下單的過程來舉例,在下單的業務操作過程中需要呼叫庫存服務,支付服務,積分、物流等服務。假設訂單服務最多同一時間只能處理50個請求,這個時候如果積分服務掛了,那麼每次訂單服務去呼叫積分服務的時候,都會卡這麼一段時間,然後才返回超時異常。
在這種場景下會有什麼問題呢?
如果目前電商網站正在搞活動,進行搶購活動,下單的人非常多,這種高併發的場景下,訂單服務的已經同時在處理50個下單請求了,並且都卡在了請求積分服務的過程中。訂單服務已經沒有能力去處理其他請求了。
那麼其他服務再來呼叫訂單服務時,發訂單服務無響應,這樣就導致訂單服務也不可用了。然後其他依賴訂單服務的服務,也最終會導致不可用。這就是微服務架構中的服務雪崩。
就上圖所示,如果多個服務之間相互呼叫,而不做任何保護措施的話,那麼一個服務掛了,就會產生連鎖反應,導致其他服務也掛了。
其實就算是積分服務掛了,也並不應該導致訂單服務也掛了,積分服務掛了,我們可以跳過積分服務,或者是放一個預設值,然後繼續往下走,等著積分服務恢復了,可以手動恢復一下資料。
那麼Spring Cloud Hystrix就是解決這個問題的元件,他主要是起到熔斷,隔離,降級的作用。
Spring Cloud Hystrix其實是會為每一個服務開闢一個執行緒池,然後每個執行緒池中的執行緒用於對服務的呼叫請求。這樣就算是積分服務掛了,那也只是呼叫積分服務的執行緒池出現問題了,而其他服務的執行緒池還正常工作。這就是服務的隔離。
這樣訂單服務在的呼叫積分服務的時候,如果發現有問題了,積分服務可以通過Hystrix返回一個預設值(預設是5秒內20次呼叫失敗就熔斷)。這樣訂單服務就不用在這裡卡住了,可以繼續往下呼叫其他服務進行業務操作了。這就是服務的熔斷。
雖然說是積分服務掛了,並且也返回了預設值了,但是後續如果積分服務恢復了,想恢復資料怎麼辦呢?這個時候積分服務可以將姐收到的請求記錄下來,或者是打到日誌中,能為後面恢復資料提供依據就行。這就是服務的降級。
整個過程大致如下圖所示:
API閘道器服務Spring Cloud Zuul
通過上面幾個元件的結合使用,已經能夠完成一個基本的微服務架構了。但是當一個系統中微服務的數量逐漸增多時,一些通用的邏輯,例如:許可權校驗機制,請求過濾,請求路由,限流等等,這些每個服務對外提供能力的時候都要考慮到的邏輯,就會變得冗餘。
這個時候API閘道器的概念應運而生,它類似於物件導向設計模式中的Facade模式(門面模式/外觀模式),所有的外部客戶端訪問都需要經過它來進行排程和過濾。主要實現請求路由、負載均衡、校驗過濾、服務限流等功能。
Spring Cloud Zuul就是Spring Cloud提供的API閘道器元件,它通過與Eureka進行整合,將自身註冊為Eureka下的應用,從Eureka下獲取所有服務的例項,來進行服務的路由。
Zuul還提供了一套過濾器機制,開發者可以自己指定哪些規則的請求需要執行校驗邏輯,只有通過校驗邏輯的請求才會被路由到具體服務例項上,否則返回錯誤提示。
Spring Cloud Zuul的依賴包spring-cloud-starter-zuul
本身就包含了對spring-cloud-starter-hystrix
和spring-cloud-starter-ribbon
模組的依賴,所以Zuul天生就擁有執行緒隔離和斷路器的自我保護功能,以及對服務呼叫的客戶端負載功能。
Zuul的路由實現是通過Path和serviceId還實現的,path是一個http請求去除ip和埠號後的方法路徑,例如:http://192.168.20.12:9001/api-zuul/123
,那麼path就是/api-zuul/123
,Zuul在配置時支援模糊匹配,若123是動態引數,可以將path配置成/pai-zuul/**
,serviceId就是服務在Eureka中註冊的服務名稱。
zuul.routes.api-zuul.path= /api-zuul/**
zuul.routes.api-zuul.serviceId= service-jimoer
有了統一的閘道器後,再做統一的鑑權、限流、認證授權、安全等方面的工作就會變的更加方便了。
總結
上面總結了Spring Cloud的幾個核心元件,其實Spring Cloud 除了這幾個元件還有一些其他的元件,例如:
- 分散式配置中心:
Spring Cloud Config
; - 訊息匯流排:
Spring Cloud Bus
; - 訊息驅動:
Spring Cloud Stream
; - 分散式服務跟蹤:
Spring Cloud Sleuth
。
主要是後面這些元件我們平時用的不多,而且對於微服務來說有些是有替代品的,所以我暫時就沒有總結。還有一點畢竟我這次總結是為了解決面試的問題,所以後面如果在實際的工作中用到了剩下的這些元件,我會繼續總結的。
好了,總結一下這次的幾個元件的內容吧。
- Spring Cloud Eureka 各個微服務在啟動時將自己註冊到Eureka Server中,並且各個服務中的Eureka Client又能從註冊中心獲取各個服務的例項地址清單。
- Spring Cloud Ribbon 各個服務相互呼叫的時候,通過Ribbon來進行客戶端的負載均衡,從多個例項中根據一定的策略選擇一臺進行請求。
- Spring Cloud Feign 基於動態代理機制,根據註解和引數拼接URL,選擇具體的服務例項發起請求,簡化了服務間相互呼叫的開發工作。
- Spring Cloud Hystrix 呼叫每個服務的時候都是通過執行緒池中的執行緒來發起的,不同的服務走不同的執行緒池,實現了服務的隔離,而且服務不可用時還提供了熔斷機制以及支援降低措施。
- Spring Cloud Zuul 外部請求統一通過Zuul閘道器來進入,支援自定義路由規則,自定義過濾規則,可以實現同一的鑑權、限流、認證等功能。
最後來一個整體的架構圖,將各個元件串起來。