微服務核心模塊
這是微服務的基本架構圖,不同終端可以通過網關調用我們的核心服務,每個服務可以獨立水平擴展,它們各自管轄自己的數據庫。下面是SpringCloud相關常見技術棧(模塊),我們將通過一個簡化後的真實案例來串聯起它們:
Eureka/Nacos:服務註冊中心,後者由阿里巴巴開源
Ribbon:負載均衡組件
Hystrix:熔斷器組件
Feign:請求客戶端組件
SpringCloud GateWay:網關組件,提供路由、過濾等功能
1. 準備工作
下面我們通過一個案例來整體介紹這些組件。案例背景:B2C商城裡,用戶在購物時會生成訂單,除了支付業務本身的訂單狀態處理之外,系統還會圍繞這些訂單分別給商家、用戶端做些處理。最典型的比如,商家端要做訂單統計、用戶端要做訂單查詢、積分計算等等。為了將不同端的訂單處理分層解耦,通常會劃分多個服務,最簡單的方案是分為商家服務和用戶服務,商家服務管理商家訂單、用戶服務管理用戶訂單。
當用戶下單後,前端通過調用平臺聚合層來分別調用商家、用戶服務。
所以,我們可以新建三個服務項目,PlatformDemo、MerchantDemo、UserDemo。按照微服務的理論,每個服務管控自己的數據庫,所以可以新建兩個單獨的庫,分別是merchantdb、userdb,然後分別新建各自的訂單表merchantorder、userorder。(其實就是垂直分庫)
編譯相關命令
clean compile package -Dmaven.test.skip=true
PlatformDemo怎麼調用MerchantDemo和UserDemo呢?兩種方式:
- platform直接通過http調用merchant和user,優點是:簡單,缺點是:假如merchant和user是多實例的,那麼platform需要手動維護每個實例的地址;
- 將merchant、user註冊到一個服務註冊中心,然後platform僅通過單一的【服務名稱】來路由到不同的merchnt、user服務實例。優點是:服務實例水平擴展很方便,不需要在platform維護實例地址。缺點是:要安裝單獨的服務註冊中心。
在實際場景中,肯定會選2,原因就在於,微服務的意義就是讓服務實例更方便的水平擴展,假如每次還得在調用層手動維護實例地址,會非常麻煩。另外,註冊中心只需要安裝一次,也不存在其他太複雜的操作。
2. Nacos基本介紹
微服務比較常見的註冊中心有Eureka、ZK、Consul、Nacos等。Nacos由阿里巴巴開源,它提供了服務註冊、配置管理等功能。其簡單易用的風格,越來越受到大家的關注,我們的生產級項目都已採用,目前運行良好。
實際上Nacos思路非常簡單,它提供中心服務器(可集群擴展,消除單點)及控制檯,服務提供者(比如Merchant服務)首先主動註冊到中心服務,中心服務輪詢其存活狀態。服務消費者(比如Platform)根據固定的服務名從中心服務器調用目標服務。這種架構的優點是:服務提供者的水平擴展可以對服務消費者完全透明,後者不需要手動維護前者服務列表。
下面我們以Nacos為例,來對註冊中心做個演示。
Nacos服務安裝
安裝過程可以看這裡:https://nacos.io/zh-cn/docs/quick-start.html
我這裡是按照源碼方式安裝,相關nacos命令在 distribution/target/nacos-server-$version/nacos/bin目錄下。
啟動命令:
sh startup.sh -m standalone
關停命令:
sh shutdown.sh
控制檯頁面: http://localhost:8848/nacos/ 默認密碼:nacos/nacos
使用Nacos進行服務註冊
首先引入nacos依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>>0.2.1.RELEASE</version>
</dependency>
這裡我們採用生產驗證過的0.2.1版本
代碼及配置方面的變動:
在主類上加上@EnableDiscoveryClient註解
在配置文件中新增如下內容:
server.port=0
spring.application.name=merchant-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
這裡將port設置為0,意味著每次啟動都會使用隨機端口號,這主要是因為同一類的微服務實例通常會有多個,使用同樣的固定端口會造成端口占用的問題。
Nacos控制檯初探
啟動主類後,即可在控制檯的【服務列表】中看到merchant-service 服務:
這裡我們啟動了3個實例,點擊詳情後,我們可以看到實例的權重及運行情況:
在這裡,我們可以直接編輯實例的權重,也可以直接上下線實例,後面我們會對此進行演示。
藉助Nacos進行微服務調用
如之前所說,我們需要在Platform中調用Merchant服務,完成訂單入庫的操作。由於Merchant已經註冊在了Nacos,所以Platform必須藉助Nacos來完成服務的調用。
Platform項目的配置和前面類似,這裡不再贅述,我們直接看怎麼輪詢調用Merchant服務。為了更清楚的演示輪詢過程,我們直接採用LoadBalancerClient+RestTemplate的方案手動調用服務。LoadBalancerClient用於通過服務名選取服務信息(ip地址、端口號),RestTemplate用於做Http請求。
下面首先配置RestTemplate:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(3000);//單位為ms
factory.setConnectTimeout(3000);//單位為ms
return factory;
}
}
然後新建測試類,核心測試代碼如下:
ServiceInstance serviceInstance = loadBalancerClient.choose("merchant-service");
String url = String.format("http://%s:%s/merchant/saveOrder",serviceInstance.getHost(),serviceInstance.getPort());
System.out.println("request url:"+url);
Object value=restTemplate.postForObject(url,null,String.class);
代碼解釋:首先通過ServiceInstance根據權重獲取服務信息,該信息包括ip+端口,然後拼接服務地址信息,最後通過RestTemplate進行Http請求。
注意:在test之前,先啟動多個merchant服務實例。大家不妨測試一下,假如請求多次,是能看到均衡負載的效果的。
上面這種方式比較手工一點,實際上,我們可以直接讓RestTemplate集成Ribbon,實現LoadBalance的效果,做法很簡單:
- 在構建RestTemplate時加上@LoadBalanced註解:
@LoadBalanced
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}
- 請求服務時,直接使用服務名而非IP+端口:
restTemplate.postForObject("http://merchant-service/merchant/saveOrder",null,String.class);
3. 微服務調用之Feign
Feign是SpringCloud中非常常用的一個HTTP客戶端組件,它提供了接口式的微服務調用API。
首先確保項目中已經導入了Feign依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
然後創建目標服務的接口,比如我們這裡需要調用Merchant服務,那麼可以新建MerchantService接口專門來處理與之相關的服務調用:
@FeignClient(value="merchant-service")
public interface MerchantService {
@PostMapping("/merchant/saveOrder")
public String saveMerchantOrder();
}
這個接口非常容易理解:使用@FeignClient將接口定義為服務接口,使用SpringMVC的@PostMapping、@GetMapping註解將接口方法定義為服務映射方法。就這樣,調用微服務的方式就和普通方法調用的方式沒太大區別(至少感覺上是這樣)。
有時候,我們需要在發起Feign請求時,可以做一些統一的處理,比如:header設置、請求監控等。此時我們可以配置Feign攔截器來實現。
Feign攔截器的實現方式非常簡單,主要分為兩步:
- 實現feign.RequestInterceptor接口,重寫其apply方法,如下:
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("token","123");
}
}
- 將其配置在@FeignClient(configuration)中:
@FeignClient(value="merchant-service",configuration = FeignRequestInterceptor.class)
public interface MerchantService {
@PostMapping("/merchant/saveOrder")
public String saveMerchantOrder();
}
此時我們可以先調整下Merchant服務的接口,使用@RequestHeader("token")來接收token參數。
Feign超時及重試機制
微服務之間調用最大的一個問題就是超時問題(沒有之一)。比如說,當Platform調用Merchant時,由於網絡不通或者Merchant服務響應緩慢,那麼Platform是不能一直等待下去的,這樣資源會一致被佔用,前端也得不到快速響應。此時一般會設置超時時間。
大家可以測試一下,當連接不上服務端時,會報connect timeout,當服務端響應時間過長,會報read timeout。默認情況下,Feign是不會重試的,即重試邏輯為Retryer.NEVER_RETRY。我們可以根據實際情況作如下配置:
@Configuration
public class FeignConfigure {
@Bean
Request.Options feignOptions() {
return new Request.Options(
/**connectTimeoutMillis**/
1 * 1000,
/** readTimeoutMillis **/
1 * 5000);
}
@Bean
public Retryer feignRetryer() {
return new Retryer.Default();
}
}
該配置類裡面,我們設置了連接超時未1秒、讀取超時未5秒,然後默認重試機制會重試5次。測試方式比較簡單,比如我們可以把Merchant服務從Nacos上摘除下來,或者在接口中手動設置sleep,這裡不再給出。
調用方在收到超時異常時,很可能服務方會繼續執行(比如執行過長導致de 超時),所以重試的前提是:【一定要保證服務方的冪等性】,即重複多次不會影響業務邏輯。
4. 微服務間的數據傳輸
在實際開發中,有一個很現實的問題是數據傳輸格式的約定問題。在微服務架構中,實現一個完整的功能需要涉及到多個服務的調用,每個服務都有與自己領域相關的數據封裝,微服務之間的調用需要遵循對方的數據格式要求。以前面的訂單為例,Platform在調用Merchant時,應該傳入商戶訂單對象,然後被返回Merchant服務的響應對象。聽起來很簡單對吧?但是Platform和Merchant是不同項目,後者約定好的對象類在前者是不存在的,前者工程師需要手動新建匹配的類才行。在服務接口非常繁多的情況下,這種手工處理會佔用工程師很多時間。為了讓他們過的爽一點,我們應該讓這些類/對象共享才對。
所以,筆者建議針對每個服務都新建一個DTO項目,專門用於定義數據傳輸對象。比如我們可以新建MerchantDTO,專門定義該服務對應的輸入、輸出對象,每次更新升級時,可以將其打入公司的私有倉庫中。為了自動化這一過程,可以使用CI/CD工具(比如jenkins)自動拉取git代碼並install/deploy到私有倉庫。在需要調用Merchant服務時,在pom中加入依賴就可以了。在項目規模較小時,可以暫時只做一個DTO項目,涵蓋所有服務,以後再拆也是OK的。
在DTO中,我們會約定兩種類型的數據:請求參數值、響應返回值。請求參數與業務域相關。比如保存商戶訂單,那麼請求參數就是商戶訂單數據,比如:
@ApiModel("商戶訂單實體")
@Setter
@Getter
public class MerchantOrderRequest {
@ApiModelProperty(name = "ordername",value = "訂單名稱")
private String ordername;
@ApiModelProperty(name="price",value = "價格")
private double price;
}
通常來說,響應返回值都會有些公共的字段,比如code、message等,一般來說會設計響應對象的基類,這樣便於後面做統一的code處理:
@ApiModel(value = "默認響應實體")
@Setter
@Getter
public class DefaultResponseData {
@ApiModelProperty(name = "code",value = "返回碼,默認1000是成功、5000是失敗")
private String code;
@ApiModelProperty(name = "message",value = "返回信息")
private String message;
/**
* 額外數據
*/
@ApiModelProperty(name = "extra",value = "額外數據")
private String extra;
}
這裡用到了swagger註解,這樣在接口文檔中就會有明確說明,方便調試。@Setter、@Getter主要用於生產Setter/Getter代碼,有助於解放大家的雙手,具體安裝及依賴過程可以看這篇文章:
如何使用Lombok簡化你的代碼?
我們改造一下之前的saveMerchantOrder方法,讓其傳入MerchantOrderRequest、返回DefaultResponseData。
public DefaultResponseData saveMerchantOrder(
@RequestBody MerchantOrderRequest merchantOrderRequest,
@RequestHeader("token") String token){
...
}
重新啟動Merchant服務,打開swagger,可以看到請求和響應參數的描述:
User服務可以完全按照同樣的處理策略,這裡不再贅述。
5. 使用Hystrix進行熔斷保護(降級)
在分佈式/微服務環境中往往會出現各種各樣的問題,比如網絡異常,超時等,而這些問題可能會導致系統的級聯失敗,即使不斷重試,也可能無法解決的,還會耗費更多的資源。比如說我們Platform在調用Merchant時,後者的數據庫突然掛了,然後系統卡頓或者不停報錯,用戶此時可能會不斷刷新,做更多的請求。這最終會讓應用程序由於資源耗盡而導致雪崩。遇到這種情況,更好的做法是在調用階段進行熔斷保護並做降級處理。
熔斷保護類似於電路中的保險絲,當電流異常升高時會自動切斷電流,以保護電路安全。在開發中,熔斷器通常有三個狀態,即Closed、Open、Half-Open,如下圖:
默認情況下,熔斷器是關閉(Closed)的,一旦在某個時間窗口T(默認10秒)內發生異常或者超時的次數在N以上,那麼熔斷器就會開啟(Open),然後在時間窗口S之後,熔斷器會進入半開狀態(Half-Open),此時假如新請求成功執行,那麼會進入關閉狀態(Closed),否則繼續開啟(Open)。為了讓熔斷後能快速降級,我們通常需要指定相應的fallback處理邏輯。
在SpringCloud中,我們主要使用Hystrix組件來完成熔斷降級,下面看看怎麼實現。
首先,我們得引入依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在啟動類上加@EnableHystrix註解,開啟Hystrix。
在API層,我們只需要加@HystrixCommand註解即可。如前面所說,當接口熔斷後,我們需要指定降級邏輯,即指定fallback方法:
@GetMapping("/simpleHystrix")
@HystrixCommand(fallbackMethod = "fallbackHandler"
})
public String simpleHystrix(@RequestParam("count") Integer count){
System.out.println("執行.................");
int i=10/count;
return "success";
}
public String fallbackHandler(Integer count){
System.out.println("count="+count);
return "fail";
}
這裡我們定義了一個簡單的接口,當count=0時,很明顯會發生異常,在某段時間內出現異常的次數達到閾值,新請求就會進入fallbackHandler進行處理,不會繼續調用simpleHystrix的邏輯。我們可以通過commandProperties/@HystrixProperty指定一些基本的參數,比如:
commandProperties = {
@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,value = "3"),
@HystrixProperty(name =HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,value = "20000")
這裡我們指定了10秒內出現3次異常,就會進入Open狀態,然後再20秒之後,會進入Half-Open狀態。
Feign整合Hystrix
在實際場景中,熔斷器解決的大部分是微服務調用的問題,所以這裡我們看看怎樣讓Feign整合Hystrix。
前面提到過的@FeignClient,其實直接支持配置Hystrix。它支持的方式有兩種:fallback、fallbackFactory。前者比較簡單,僅需要配置當前接口實現類作為降級函數,後者功能豐富一點,可以獲取觸發降級的原因。我們這裡先用前者快速實現一下。
首先定義fallback類,該類實現服務接口及其所有方法,以MerchantService為例:
public class MerchantServiceFallBack implements MerchantService {
@Override
public DefaultResponseData saveMerchantOrder(MerchantOrderRequest merchantOrderRequest) {
DefaultResponseData responseData=new DefaultResponseData();
responseData.setCode("1001");
responseData.setMessage("fallback");
return responseData;
}
}
然後在@FeignClient中加上:
fallback = MerchantServiceFallBack.class
最後,別忘記在配置文件中開啟feign-hystrix:
feign.hystrix.enabled=true
當調用MerchantService接口服務時,一旦出現異常情況,會轉入MerchantServiceFallBack的邏輯。
6. API網關之Spring Cloud Gateway
API網關主要解決的問題有:API鑑權、流量控制、請求過濾、聚合服務等。它並非微服務的必需品,具體怎麼用得看實際場景。目前比較流行的網關有Zuul、Spring Cloud GateWay等。前者比較老牌了,網上資料也較多,而後者是新貴,算是SpringCloud的親兒子,個人感覺也更好用,我們以它為例來講解API網關的常見用法。
Spring Cloud Gateway基於Spring5、Reactor以及SpringBoot2構建,提供路由(斷言、過濾器)、熔斷集成、請求限流、URL重寫等功能。
SpringBoot/SpringCloud系列的組件太多,經常會出現版本不對應,以下是經過測試無誤的搭配:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
最重要的一步是定義路由:
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p
.path("/api/order/gateWay")
.uri("http://localhost:8889"))
.build();
}
代碼解釋:當訪問本服務的/api/order/gateWay時,會將請求轉發到http://localhost:8889/api/order/gateWay。然後我們也可以在轉發請求前進行過濾處理,比如新增header參數、請求參數等,大家可以自行測試:
filters(f -> f.addRequestHeader("token", "123"))
在實際項目中,網關所調用的目標服務都註冊在註冊中心裡面,所以一般來說,會讓網關訪問註冊中心地址。假如用的是Nacos,可以將uri中的http換成lb:
lb://platform-service