微服務核心子產品
這是微服務的基本架構圖,不同終端可以通過網關調用我們的核心服務,每個服務可以獨立水準擴充,它們各自管轄自己的資料庫。下面是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時,會将請求轉發到
:8889/api/order/gateWay。然後我們也可以在轉發請求前進行過濾處理,比如新增header參數、請求參數等,大家可以自行測試:
filters(f -> f.addRequestHeader("token", "123"))
在實際項目中,網關所調用的目标服務都注冊在注冊中心裡面,是以一般來說,會讓網關通路注冊中心位址。假如用的是Nacos,可以将uri中的http換成lb:
lb://platform-service