分佈式開發-Spring Cloud

目錄

1.服務治理和服務發現——Eureka

1.1 配置服務治理節點

1.2 服務發現

1.3 配置多個服務治理中心節點

2.微服務之間的調用

2.1 Ribbon客戶端負載均衡

2.2 Feign聲明式調用

3 斷路器——Hystrix

3.1 使用降級服務

3.2 啓用Hystrix儀表盤

4.路由網關-Zuul

4.1 構建Zuul網關

4.2 使用過濾器

5.使用@SpringCloudApplication


按照現今互聯網的開發,高併發、大數據、快響應已經是普遍的要求。爲了支持這樣的需求,互聯網系統也開始引入分佈式的開發。爲了實現分佈式的開發,Spring 推出了一套組件,那就是Spring Cloud。當前Spring Cloud已經成爲構建分佈式微服務的熱門技術,它並不是自己獨自造輪子,而是將目前各家公司已經開發的、經過實踐考驗較爲成熟的技術組合起來,並且通過Spring Boot風格再次封裝,從而屏蔽掉了複雜的配置和實現原理,爲開發者提供了一套簡單易懂、易部署和維護的分佈式系統開發包。

Spring Cloud是一套組件,可以細分爲多種組件,如服務發現、配置中心、消息總線、負載均衡、斷路器和數據監控等。我們只討論以下最基礎的技術:

  • 服務治理和服務發現:在Spring Cloud中主要是使用Netflix Eureka作爲服務治理的
  • 客戶端負載均衡:Spring Cloud提供了Ribbon來實現這些功能。
  • 聲明服務調用:對於Rest風格的調用,如果使用RestTemplate會比較麻煩,可讀性不高。爲了簡化多次調用的複雜度,Spring Cloud提供了接口式的聲明服務調用編程,它就是Feign。
  • 斷路器:在分佈式中,因爲存在網絡延遲或者故障,所以一些服務調用無法及時響應。如果此時服務消費者還在大量地調用這些網絡延遲或者故障的服務提供者,那麼很快消費者也會因爲大量的等待,造成積壓,最終導致其自身出現服務癱瘓。爲了克服這個問題,Spring Cloud引入了Netflix的開源組件Hystrix來處理這些問題。當服務消費者長期得不到服務提供者響應時,就可以進行降級、服務熔斷、線程和信號隔離、請求緩存或者合併等處理。
  • API網關:在Spring Cloud中API網關是Zuul。對於網關而言,存在兩個作用:第一個作用是將請求的地址映射爲真實服務器地址,當真實服務器有多臺的時候,可以起到路由分發的作用,從而降低單個節點的負載。第二個作用是過濾服務,在互聯網中,服務器可能面臨各種攻擊,Zuul提供了過濾器,通過它過濾那些惡意或者無效的請求。

爲了更好地討論Spring Cloud組件和內容,假設需要實現一個電商項目,當前團隊需要承擔兩個模塊的開發,分別是用戶模塊和產品模塊。根據微服務的特點,將系統拆分爲用戶服務和產品服務,兩個服務通過Rest風格進行交互。架構如下圖所示:

1.服務治理和服務發現——Eureka

我們首先搭建單個服務治理節點,然後將產品和用戶服務的各自兩個節點註冊到服務治理節點上,再把服務治理節點變爲兩個。

1.1 配置服務治理節點

Spring Cloud的服務治理是使用Netflix的Eureka作爲服務治理器的,它是我們構建Spring Cloud分佈式最爲核心和基礎的模塊,它的作用是註冊和發現各個Spring Boot微服務,並且提供監控和管理功能。首先我們新建一個工程service-config,然後引入對應的jar包:

    <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    <dependencyManagement>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

這樣就引入了Eureka模塊的包了。然後要啓動它只需要在Spring Boot的啓動文件上加入註解:

@EnableEurekaServer

有了這個註解,就意味着Spring Boot會啓動Eureka模塊,我們進一步配置Eureka模塊的一些基本內容。

spring.application.name=server
server.port=7001
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=fasle
eureka.client.fetch-registry=fasle
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
  • spring.application.name配置爲server,這是一個標識,它標識某個微服務的共同標識。如果有第二個微服務節點啓動時,也是將這個配置爲server,那麼Spring Cloud也會認爲它是這個微服務的一個節點。
  • eureka.client.register-with-eureka配置爲false,因爲在默認的情況下,項目會自動地查找服務註冊中心去註冊。這裏項目自身就是服務註冊中心,所以取消掉註冊服務中心。
  • eureka.client.fetch-registry配置爲false,這是一個檢索服務的功能,因爲服務治理中心是維護服務實例的,所以也不需要這個功能,即設置爲false。

配置完成之後,我們啓動服務治理應用,在瀏覽器輸入:http://localhost:7001/可以看到如下界面:

這意味着Eureka服務治理中心已經啓動成功,但是還沒有註冊服務。下面開始註冊產品和用戶服務。

1.2 服務發現

註冊服務會用到服務發現,我們新建一個Spring Boot工程,取名爲product-service,並且引入服務發現相關的包:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

我們只需要依賴spring-cloud-starter-netflix-eureka-client,無需添加額外的註解,就可以將當前項目註冊給服務治理中心。這裏需要注意的是不要忘記引入spring-boot-starter-web,否則會導致無法註冊。application文件配置如下:

server.port=9001
spring.application.name=product
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/

這裏端口使用了9001,而應用名稱爲product,這個微服務名稱將會註冊給服務治理中心。

看到上面這個圖,就說明產品微服務已經註冊給了服務治理中心。或許在分佈式服務中需要兩個或者以上的產品微服務節點,我們修改產品微服務的端口爲9002,並再次啓動該Spring Boot應用程序,再次打開服務治理中心頁:

從圖中可以看到,服務治理中心存在兩個產品微服務節點,端口分別爲9001和9002。類比產品服務,我們再新建用戶服務,並配置端口分別爲8001和8002,並啓動該服務。再次查看服務治理中心頁面,可以看到兩個用戶微服務節點都已經註冊成功了。

1.3 配置多個服務治理中心節點

上面只是在服務治理中心將兩個微服務都分別註冊了兩個節點,而服務治理中心卻只有一個節點。我們希望有兩個服務治理中心,一個不可用後,另外一個節點依舊可用,這樣就能保證服務可以繼續正常處理業務,這就體現了高可用的特性。我們停止服務治理中心,並再次新建一個服務治理中心,兩個治理中心相互註冊,即A的註冊域B,B的註冊域爲A,配置信息如下:

A註冊中心:

spring.application.name=server
server.port=7001
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:7002/eureka/

B註冊中心:

spring.application.name=server
server.port=7002
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/

這樣將兩個註冊中心啓動後,就可以看到兩個服務治理中心是通過相互註冊來保持相互監控的,關鍵點是屬性spring.application.name保持一致都爲server,這樣就可以形成兩個甚至是多個服務治理中心。此時再打開服務治理中心頁面,就可以看到;

現在已經有了兩個服務治理中心,接下來,需要將其他的微服務註冊到多個服務治理中心中。我們以單個產品微服務爲例來修改配置文件。

server.port=9001
spring.application.name=product
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

這裏加入了兩個服務治理中心的域,這樣就可以使得應用註冊到兩個服務中心上去。啓動這個產品服務之後,再次修改端口爲9002,然後再次啓動,這樣兩個產品微服務節點都會被註冊到兩個服務治理中心。對於用戶服務,也做類似的操作。做完這些步驟,再次打開服務治理中心首頁,可以看到如下圖:

此時,多個微服務都已經啓動了,並且註冊成功到兩個服務治理中心中進行監控了。那我們如何實現讓各個微服務相互交互起來呢?Spring Cloud提供了另外兩個組件進行支持,它們就是Ribbon和Feign。

2.微服務之間的調用

上面已經把產品和用戶兩個微服務註冊到服務治理中心了。對於業務,則往往需要各個微服務之間相互地協助才能完成,因此這裏涉及到服務之間相互調用的問題。例如,我們需要根據用戶的等級來決定某些商品的折扣,如白金會員是9折,黃金會員8.5折,鑽石會員是8折等。也就是說分佈式系統在執行商品交易邏輯時,需要調用用戶服務獲得用戶信息纔可以決定產品的折扣。除此之外,我們還需要注意服務節點之間的負載均衡,畢竟一個微服務可以由多個節點提供服務。Spring Cloud提供了Ribbon和Feign組件來幫助我們完成這些功能,通過它們,各個微服務之間就能夠相互調用,並且它會默認實現負載均衡。

2.1 Ribbon客戶端負載均衡

我們首先在用戶服務中提供查詢用戶信息的Rest接口。

package com.martin.user.pojo;

import lombok.Data;
import java.io.Serializable;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@Data
public class UserPO implements Serializable {
    private Long id;
    private String userName;
    //1-白銀會員 2-黃金會員 3-鑽石會員
    private int level;
    private String note;
}

然後定義基於REST風格的實現用戶返回的代碼:

package com.martin.user.controller;

import com.martin.user.pojo.UserPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@Slf4j
@RestController
public class UserController {
    /**
     * 服務發現客戶端
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/user/{id}")
    public UserPO getUserPO(@PathVariable("id") Long id) {
        ServiceInstance service = discoveryClient.getInstances("USER").get(0);
        log.info("[" + service.getServiceId() + "]:" + service.getHost() + ":" + service.getPort());

        UserPO userPO = new UserPO();
        userPO.setId(id);
        int level = (int) (id % 3 + 1);
        userPO.setLevel(level);
        userPO.setUserName("user_name_" + id);
        userPO.setNote("note_" + id);
        return userPO;
    }
}

這裏的DiscoveryClient對象是Spring Boot自動創建的,然後在方法中會打印出第一個用戶微服務ID、服務主機和端口,這樣有利於後續的監控和對負載均衡的研究。

然後,我們在產品微服務上通過Maven加入對Ribbon的依賴,如代碼清單:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-ribbon</artifactId>
        </dependency>

然後對RestTemplate進行初始化:

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication(scanBasePackages = "com.martin.product.*")
public class ProductServiceApplication {

    @LoadBalanced
    @Bean
    public RestTemplate initRestTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }

}

這段代碼中在RestTemplate中加入了註解@LoadBalanced,它的作用是讓RestTemplate實現負載均衡,也就是說通過這個RestTemplate對象調用用戶微服務請求的時候,Ribbon會自動給用戶微服務節點實現負載均衡,這樣請求就會被分攤到微服務的各個節點上,從而降低單點的壓力。默認的情況下,Ribbon會使用輪詢的負載均衡算法。接着我們使用RestTemplate調用用戶服務,代碼如下:

package com.martin.product.controller;

import com.martin.common.dto.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/ribbon")
    public UserDTO testRibbon() {
        UserDTO userDTO = null;
        for (int i = 0; i < 10; i++) {
            userDTO = restTemplate.getForObject("http://USER/user/" + (i + 1), UserDTO.class);
        }

        return userDTO;
    }
}

方法中使用了“USER”這個字符串代替了服務器及其端口,這是一個服務ID。我們啓動USER服務和PRODUCT服務,然後在瀏覽器中輸入地址:http://localhost:9002/product/ribbon,可以看到如下的返回結果:

{
    "id": 10,
    "userName": "user_name_10",
    "level": 2,
    "note": "note_10"
}

與此同時,兩個用戶微服務的後臺日誌各有5條日誌打出:

2020-02-13 21:28:27.821  INFO 16324 --- [nio-8001-exec-6] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.831  INFO 16324 --- [nio-8001-exec-7] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.842  INFO 16324 --- [nio-8001-exec-8] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.853  INFO 16324 --- [nio-8001-exec-9] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.860  INFO 16324 --- [io-8001-exec-10] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001

這說明在產品中心通過Ribbon調用時已經負載均衡成功。

2.2 Feign聲明式調用

如果我們需要多次調用服務,使用RestTemplate並非那麼友好,因爲除了要編寫URL,還需要注意這些參數的組裝和結果的返回等操作。爲了克服這些不友好,除了Ribbon外,Spring Cloud還提供了聲明式調用組件——Feign。

Feign是一個基於接口的編程方式,開發者只需要聲明接口和配置註解,在調度接口方法時,Spring Cloud就根據配置來調度對應的Rest風格請求,從其他微服務系統中獲取數據。首先,我們引入Feign的Maven依賴包:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

這樣就把Feign所需要的依賴包加載進來了,爲了啓用Feign,首先需要在Spring Boot的啓動文件中加入註解@EnableFeignClients,這個註解代表該項目會啓動Feign客戶端。

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.martin.product.*")
@EnableFeignClients(basePackages = "com.martin.product.*")
public class ProductServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

然後在產品微服務中加入接口聲明,注意這裏僅僅是一個接口聲明,並不需要實現類,代碼實現如下:

package com.martin.product.service;

import com.martin.common.dto.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@FeignClient("user")
public interface UserService {
    @GetMapping("/user/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

這裏@FeignClient("user")代表這是一個Feign客戶端,而配置的“user”是一個服務的ID,它指向了用戶微服務,這樣Feign就會知道向用戶微服務請求,並會實現負載均衡。這裏的註解@GetMapping代表啓用HTTP的GET請求用戶微服務,@PathVariable代表從URL中獲取參數。下面我們在ProductController中加入UserService接口對象的注入,並且使用它來調度用戶微服務的REST端點。實現代碼如下:

package com.martin.product.controller;

import com.martin.common.dto.UserDTO;
import com.martin.product.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private UserService userService;
    @GetMapping("/fegin")
    public UserDTO testRibbon() {
        UserDTO userDTO = null;
        for (int i = 0; i < 10; i++) {
            userDTO = userService.getUser((long) i);
        }

        return userDTO;
    }
}

與Ribbon相比,Feign屏蔽掉了RestTemplate的使用,提供了接口聲明式的調用,使得程序可讀性更高,同時在多次調用中更加方便。

3 斷路器——Hystrix

在微服務中,如果一個服務不可用,而其他微服務還大量地調用這個不可用的微服務,也會導致其自身不可用,其自身不可用之後又可能繼續蔓延到其他與之相關的微服務上,這樣就會使更多的微服務不可用,最終導致分佈式服務癱瘓。

爲了防止這樣的蔓延,微服務提出了斷路器的概念。在微服務系統之間大量調用可能導致服務消費者自身出現癱瘓的情況下,斷路器就會將這些積壓的大量請求“熔斷”,來保證其自身服務可用,而不會蔓延到其他微服務系統上。通過這樣的熔斷機制可以保持各個微服務持續可用。

3.1 使用降級服務

應該說,處理限制請求的方式的策略有很多,如限流、緩存等。這裏主要介紹最爲常用的降級服務。所謂降級服務,就是當請求其他微服務出現超時或者發生故障時,就會使用自身服務其他的方法進行響應。在Spring Cloud中,斷路器是由NetFlix的Hystrix實現的,它默認監控微服務之間的調用超時時間爲2s,如果超過這個時間,它就會根據你的配置使用其他方法進行響應。

首先我們在用戶微服務中增加一個模擬超時的方法:

    @GetMapping("/user/timeout")
    public String timeout() throws InterruptedException {
        long ms = (long) (3000L * Math.random());
        Thread.sleep(ms);
        return "熔斷測試";
    }

該方法沒有任何業務含義,只是會使用sleep方法讓當前線程休眠隨機的毫秒數。這個毫秒數可能超過Hystrix所默認的2000ms,這樣就可以出現短路,進入降級方法。

然後,我們在產品微服務中啓用斷路器,要啓用斷路器首先需要引入Hystrix的包:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

其次在Spring Boot的啓動文件中加入註解@EnableCircuitBreaker,就可以啓動斷路機制:

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.martin.product.*")
@EnableFeignClients(basePackages = "com.martin.product.*")
@EnableCircuitBreaker

public class ProductServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

Spring Cloud啓用斷路機制以後,在後續的代碼中加入註解@HystrixCommand就能指定哪個方法啓用斷路機制。詳細設計代碼如下:

package com.martin.product.controller;

import com.martin.product.service.UserService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private UserService userService;

    @GetMapping("/circuitBreaker")
    @HystrixCommand(fallbackMethod = "error")
    public String circuitBreaker() {
        return userService.getUserTimeOut();
    }

    public String error() {
        return "超時出錯";
    }
}

代碼中@HystrixCommand註解表示將在方法上啓用斷路機制,而其屬性fallbackMethod則可以指定降級方法,指定爲error,那麼降級方法就是error。這樣,在請求circuitBreaker方法的時候,只要超時超過了2000ms,服務就會啓用error方法作爲響應請求,從而避免請求的積壓,保證微服務的高可用性。其流程圖如下:

所以當我們請求circuitBreaker的時候,有時候會出現“熔斷測試”有時候會返回“超時出錯”。Hystrix默認的是2000ms會超時,但是希望能把這個超時時間進行自定義,我們可以使用如下代碼:

    @GetMapping("/circuitBreaker")
    @HystrixCommand(fallbackMethod = "error",
            commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
    public String circuitBreaker() {
        return userService.getUserTimeOut();
    }

@HystrixCommand除了配置超時時間,還有很多可配置的內容,這裏不再贅述了。

3.2 啓用Hystrix儀表盤

對於Hystrix,Spring Cloud還提供了一個儀表盤進行監控斷路的情況,從而讓開發者監控可能出現的問題。我們新建一個hystrix-dashboard的項目,並引入相關的包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId>
        </dependency>

在Spring Boot的啓動類中加入註解:

package com.martin.hystrix.dashboard;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardApplication.class, args);
    }
}

配置文件application.properties添加如下的配置信息:

server.port=6001
spring.application.name=hystrix_dashboard

啓動該應用,並輸入http://localhost:6001/hystrix,就可以看到如下圖所示結果:

從Hystrix儀表盤首頁可以看出,它支持3種監控,前兩種是基於Turbine的,一種是默認集羣,另一種是指定集羣,第三種是單點監控。兩個輸入框分別是輪詢時間和標題。輪詢時間表示隔多久時間輪詢一次,標題表示儀表盤的頁面標題。從單點監控的說明可以看出,只需要給出 https://hystrix-app:port/actuator/hystrix.stream格式的URL給儀表盤即可。

在產品微服務中,我們已經使用了Hystrix,還需要引入Spring Boot的監控Actuator,同時將端點暴露。首先引入spring-boot-starter-actuator依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

在application.properties中增加如下端點暴露:

management.endpoints.web.exposure.include=health,info,hystrix.stream

management.endpoints.web.exposure.include代表Actuator監控對外暴露的端點,在默認情況下,知會暴露health和info端點,這裏增加了hystrix.stream端點,這樣儀表盤才能讀到HTTP協議下的Hystrix信息流。我們重啓一下產品服務,並輸入如下圖所示的鏈接:

輸入完對應的信息後,點擊“Monitor Stream”按鈕,它就會跳到監控頁面。此時我們在瀏覽器中執行幾次http://localhost:9002/product/circuitBreaker,讓它出現斷路情況,此時再觀察儀表盤,就可以看到如下界面了:

至此,已經完成了讓儀表盤監控斷路機制的任務。當發生斷路的時候,監控就會給予具體的統計和分析。

4.路由網關-Zuul

通過上面的內容,我們已經搭建了一個基於Spring Cloud的分佈式應用。在實際的應用中,我們通常還會引入一個網關,如Nginx、F5等。網關的功能對於分佈式網站是十分重要的,它通常具有路由、負載均衡和攔截過濾等功能。下面我們看一下如何構建一個Zuul網關。

4.1 構建Zuul網關

在Spring Cloud的組件中,Zuul是支持API網關開發的組件。Zuul來自NetFlix的開源網關,它的使用十分簡單。首先新建zuul-gateway應用,並引入zuul的包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

這裏引入了服務發現包,啓用Zuul十分簡單,只需要在啓動類上添加一個註解@EnableZuulProxy就可以:

package com.martin.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication(scanBasePackages = "com.martin.zuul")
@EnableZuulProxy
public class ZuulGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }

}

這樣就啓用了Zuul網關代理功能了,註解@EnableZuulProxy的源碼如下:

package org.springframework.cloud.netflix.zuul;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.context.annotation.Import;

@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}

這裏也使用了@EnableCircuitBreaker,從這裏可以看出Zuul已經引入了斷路機制,在請求不到的時候,就會進行斷路,以避免網關發生請求無法釋放的場景,導致微服務癱瘓。

我們先簡單地配置application.properties文件,如代碼清單:

server.port=80
spring.application.name=zuul
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

上述的代碼中,使用了80端口啓動Zuul,在瀏覽器中這個端口是默認端口,因此在地址欄中不需要顯式輸入,而Spring 應用名稱則是zuul。我們啓動微服務系統,並在瀏覽器中輸入http://localhost/user/user/1,可以看到如下界面:

這裏的localhost代表的是請求zuul服務,因爲採用的是默認的80端口,user代表用戶的微服務ID,而/user/1表示的是請求路徑,這樣Zuul就會將請求轉發到用戶微服務。同理,我們也可以請求產品服務,例如:http://localhost/product/product/fegin

除此之外,Zuul也允許我們配置請求映射,在application.properties中增加如下代碼:

server.port=80
spring.application.name=zuul
# 用戶微服務映射規則
zuul.routes.user-service.path=/u/**
#指定映射的服務用戶地址,這樣Zuul就會將請求轉發到用戶微服務上了
zuul.routes.user-service.url=http://localhost:8001/
# 產品微服務映射規則
zuul.routes.product-service.path=/p/**
#映射產品服務中心服務ID,Zuul會自動使用服務端負載均衡,分攤請求
zuul.routes.product-service.service-id=product
ribbon.eureka.enabled=true
# 註冊給服務治理中心
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

先看最後一個配置,這個配置是將Zuul網關注冊給服務治理中心,這樣它就能夠獲取各個微服務的服務ID了。當請求的地址滿足path通配時,請求會轉發到對應配置的URL上。這裏需要注意的是使用ServiceId配置,Zuul會自動實現負載均衡。

4.2 使用過濾器

上面只是將請求轉發到具體的服務器或者具體的微服務上,但是有時候還希望網關功能更加強大一些。例如,監測用戶登錄、黑名單用戶、購物驗證碼、惡意請求攻擊等場景。如果這些在過濾器內判斷失敗,那麼久不要再把請求轉發到其他微服務上,以保護微服務的穩定。

在Zuul中存在一個抽象類,它便是ZuulFilter,它的定義如下圖所示:

這裏需要注意的是,只是畫出了抽象類ZuulFilter自定義的抽象方法和接口IZuulFilter定義的抽象類,也就是說,當需要定義一個非抽象的Zuul過濾器的時候,需要實現這4個抽象方法:

  • shouldFilter:返回boolean值,如果爲true,則執行這個過濾器的run方法。
  • run:返回過濾邏輯,這是過濾器的核心方法。
  • filterType:過濾器類型,它是一個字符串,可以配置爲四種,pre(請求執行之前filter)、route(處理請求,進行路由)、post(請求處理完成之後執行的filter)、error(出現錯誤時執行的filter)
  • filterOrder:指定過濾器順序,值越小優先級越高。

下面模擬這樣一個場景,假設用戶輸入了用戶名和密碼,路由網關過濾器判斷用戶輸入的用戶名和密碼是否正確,當不正確時,則不再轉發請求到微服務。實現代碼如下:

package com.martin.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.awt.PageAttributes;

/**
 * @author: martin
 * @date: 2020/2/16
 */
@Component
public class MyZuulFilter extends ZuulFilter {

    /**
     * 過濾器的類型爲請求前執行
     *
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 過濾器排序
     *
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否過濾
     *
     * @return
     */
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String userName = request.getParameter("userName");
        String pwd = request.getParameter("password");
        //如果用戶名和密碼都不爲空則返回true,啓用過濾器
        return !StringUtils.isEmpty(userName) && !StringUtils.isEmpty(pwd);
    }

    /**
     * 過濾器的邏輯
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String userName = request.getParameter("userName");
        String pwd = request.getParameter("password");
        if (!("123".equals(userName) && "123".equals(pwd))) {
            //不再轉發請求
            ctx.setSendZuulResponse(false);
            //設置HTTP的響應碼爲401(未授權)
            ctx.setResponseStatusCode(401);
            //設置響應類型爲JSON數據集
            ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8.getType());
            //設置響應體
            ctx.setResponseBody("{'message':'Verification Code Error'}");
        }
        //賬號密碼一致,驗證通過
        return null;
    }
}

上述代碼中在類上標註了@Component,這樣Spring就會掃描它,將其裝配到IOC容器中。因爲繼承了抽象類ZuulFilter,所以Zuul會自動將它識別爲過濾器。filterType方法返回了pre,則過濾器會在路由之前執行。filterOrder返回爲0,這個方法在指定多個過濾器順序纔有意義,數字越小,則越優先。shouldFilter中判斷是否存在賬號和密碼,如果存在則返回true,這意味着將啓用這個過濾器,否則不再啓用這個過濾器。run方法是過濾器的核心方法,它對輸入的用戶名和密碼進行判斷,如果匹配不一致,則設置不再轉發請求到微服務系統,並且將響應碼設置401,響應類型爲JSON數據集,最後還會設置響應體的內容;如果一致,則返回null,放行服務。

在實際的工作中,過濾器可以使用在安全驗證、過濾黑名單請求等場景,有效地保護分佈式的微服務系統。

5.使用@SpringCloudApplication

在上面的內容中,對於啓動文件採用了很多的註解,如@SpringBootApplication、@EnableDiscoveryClient和@EnableCircuitBreaker等。這些註解有時候會讓人覺得冗餘,爲了簡化開發,Spring Cloud還提供了自己的註解@SpringCloudApplication來簡化使用Spring Cloud的開發。註解代碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.cloud.client;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

上述代碼可以看出@SpringCloudApplication會啓用Spring Boot應用,以及開發服務發現和斷路器的功能。但是它還缺乏配置掃描包的配置項,所以往往需要配合使用註解@ComponentScan來定義掃描的包。另外,@SpringCloudApplication並不會自動啓動Feign,所以如果使用Feign的時候,註解@EnableFeignClients也是必不可少的。

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@EnableFeignClients(basePackages = "com.martin.product.*")
@ComponentScan(basePackages = "com.martin.product.*")
@SpringCloudApplication
public class ProductServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章