開發與維運

Java微服務應用開發(簡版)實戰之SpringCloud

微服務核心模塊

cloud.png

這是微服務的基本架構圖,不同終端可以通過網關調用我們的核心服務,每個服務可以獨立水平擴展,它們各自管轄自己的數據庫。下面是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呢?兩種方式:

  1. platform直接通過http調用merchant和user,優點是:簡單,缺點是:假如merchant和user是多實例的,那麼platform需要手動維護每個實例的地址;
  2. 將merchant、user註冊到一個服務註冊中心,然後platform僅通過單一的【服務名稱】來路由到不同的merchnt、user服務實例。優點是:服務實例水平擴展很方便,不需要在platform維護實例地址。缺點是:要安裝單獨的服務註冊中心。

在實際場景中,肯定會選2,原因就在於,微服務的意義就是讓服務實例更方便的水平擴展,假如每次還得在調用層手動維護實例地址,會非常麻煩。另外,註冊中心只需要安裝一次,也不存在其他太複雜的操作。

2. Nacos基本介紹

微服務比較常見的註冊中心有Eureka、ZK、Consul、Nacos等。Nacos由阿里巴巴開源,它提供了服務註冊、配置管理等功能。其簡單易用的風格,越來越受到大家的關注,我們的生產級項目都已採用,目前運行良好。
Nacos註冊中心.png

實際上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 服務:

nacos01.jpg

這裡我們啟動了3個實例,點擊詳情後,我們可以看到實例的權重及運行情況:

nacos2.jpg

在這裡,我們可以直接編輯實例的權重,也可以直接上下線實例,後面我們會對此進行演示。

藉助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的效果,做法很簡單:

  1. 在構建RestTemplate時加上@LoadBalanced註解:
@LoadBalanced
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
    return new RestTemplate(factory);
}
  1. 請求服務時,直接使用服務名而非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攔截器的實現方式非常簡單,主要分為兩步:

  1. 實現feign.RequestInterceptor接口,重寫其apply方法,如下:
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("token","123");
    }
}
  1. 將其配置在@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,可以看到請求和響應參數的描述:
m1.jpg

User服務可以完全按照同樣的處理策略,這裡不再贅述。

5. 使用Hystrix進行熔斷保護(降級)

在分佈式/微服務環境中往往會出現各種各樣的問題,比如網絡異常,超時等,而這些問題可能會導致系統的級聯失敗,即使不斷重試,也可能無法解決的,還會耗費更多的資源。比如說我們Platform在調用Merchant時,後者的數據庫突然掛了,然後系統卡頓或者不停報錯,用戶此時可能會不斷刷新,做更多的請求。這最終會讓應用程序由於資源耗盡而導致雪崩。遇到這種情況,更好的做法是在調用階段進行熔斷保護並做降級處理。

熔斷保護類似於電路中的保險絲,當電流異常升高時會自動切斷電流,以保護電路安全。在開發中,熔斷器通常有三個狀態,即Closed、Open、Half-Open,如下圖:

hystrix.png

默認情況下,熔斷器是關閉(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

Leave a Reply

Your email address will not be published. Required fields are marked *