1. 程式人生 > 實用技巧 >微服務閘道器之SpringCloudGateway

微服務閘道器之SpringCloudGateway

一、SpringCloudGateway

1.1 簡介

SpringCloud GatewaySpring Cloud 的一個全新專案,該專案是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的閘道器,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。

SpringCloud Gateway 作為 Spring Cloud 生態系統中的閘道器,目標是替代 Zuul,在 Spring Cloud 2.0 以上版本中,沒有對新版本的 Zuul 2.0 以上最新高效能版本進行整合,仍然還是使用的 Zuul 2.0 之前的 非Reactor 模式

的老版本。而為了提升閘道器的效能,SpringCloud Gateway 是基於 WebFlux 框架實現的,而 WebFlux 框架底層則使用了高效能的 Reactor 模式通訊框架 Netty

Spring Cloud Gateway 不僅提供統一的路由方式,並且基於 Filter 鏈 的方式提供了閘道器基本的功能,例如:安全,監控/指標,和限流等。

官網文件

1.2 名詞解釋

  • Filter(過濾器):

    和 Zuul 的過濾器類似,可以使用它攔截和修改請求,並且對上游的響應,進行二次處理。過濾器為 org.springframework.cloud.gateway.filter.GatewayFilter 類的例項。
    
  • Route(路由):

    閘道器配置的基本組成模組,和 Zuul 的路由配置模組類似。一個 Route 模組 由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配,目標URI會被訪問。
    
  • Predicate(斷言):

    這是一個 Java 8 的 Predicate,可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或引數。斷言的 輸入型別是一個 ServerWebExchange。
    

1.3 Gateway 處理流程

客戶端向 Spring Cloud Gateway 發出請求,在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其傳送到 Gateway Web Handler

Handler 通過指定的過濾器鏈來將請求傳送到我們實際的服務執行業務邏輯,然後返回。過濾器之間用虛線分開是因為過濾器可能會在傳送代理請求之前(“pre”)或之後(“post”)執行業務邏輯。

二、準備工作

2.1 建立 OrderService 模組

  1. 新增依賴
<properties>
    <java.version>1.8</java.version>
    <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.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>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
</dependencies>	
  1. 編寫配置檔案
spring:
  application:
    # 服務名稱
    name: order-service

  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
server:
  # 設定服務埠
  port: 8881            
  1. 建立 OrderController
@RestController
@RequestMapping("/order")
public class OrderController {

    @RequestMapping("/create")
    public String create() {
        return "訂單建立成功";
    }

}

4) 啟動應用,訪問 http://localhost:8881/order/create ,返回:

訂單建立成功

2.2 建立閘道器模組

  1. 新增依賴
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>

<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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
</dependencyManagement>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

三、路由配置方式

3.1 基於配置檔案指定URI路由配置方式

spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://localhost:8881
          predicates:
            - Path=/order/**
server:
  # 配置應用埠
  port: 8080

配置說明:

* routes : 表示路由配置,可以存在多個
* id:自定義的路由 ID,保持唯一
* uri:目標服務地址
* predicates::路由條件,Predicate 接受一個輸入引數,返回一個布林值結果。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。
*- Path:基於路徑路由     

上面的配置用文字表達如下:

配置一個 Id 為 order-service 的路由規則,當訪問地址以 /order 開頭時,將會把請求轉發到 http://localhost:8881 上去

3.2 基於程式碼的路由配置方式

@SpringBootApplication
public class GatewayApplication {

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

    @Bean
    public RouteLocator orderRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route", r -> r.path("/order/**")
                        .uri("http://localhost:8881"))
                .build();
    }
}

3.3 與註冊中心相結合的路由配置方式

  1. 新增依賴
<dependencyManagement>
    <dependencies>
        ......
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    <dependencies>    
<dependencyManagement>   

<dependencies>    
	<dependency>
		<groupId>com.alibaba.cloud</groupId>
		<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	</dependency>    
</dependencies>    
  1. 調整配置
spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          # 與單個URL的區別僅僅在於URI的schema協議不同
          uri: lb://order-service
          predicates:
            - Path=/order/**
server:
  # 配置應用埠
  port: 8080

四、匹配規則

Spring Cloud Gateway 是通過 Spring WebFluxHandlerMapping 做為底層支援來匹配到轉發路由,它內建了很多 Predicates 工廠,這些 Predicates 工廠通過不同的 HTTP 請求引數來匹配,多個 Predicates 工廠可以組合使用。

4.1 Predicate 斷言條件介紹

Predicate 來源於 Java 8,是 Java 8 中引入的一個函式,Predicate 接受一個輸入引數,返回一個布林值結果。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。可以用於介面請求引數校驗、判斷新老資料是否有變化需要進行更新操作。

Spring Cloud GatewaySpring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求引數等不同的條件來進行作為條件匹配到對應的路由。下圖總結了 Spring Cloud 內建的幾種 Predicate 的實現:

4.2 匹配方式

方式名稱 引數名 例項
通過請求引數匹配 Query - Query=name,aaa
通過 Header 屬性匹配 Header - Header=X-Request-Id, \d+
通過Cookie匹配 Cookie - Cookie=sessionId,1001
通過 Host 匹配 Host - Host=..com
通過請求方式匹配 Method - Method=GET
通過請求路徑匹配 Path - Path=/order/**
通過請求 ip 地址進行匹配 RemoteAddr - RemoteAddr=192.168.1.1/24

4.3 通過請求引數匹配例項

Query Route Predicate 支援傳入兩個引數,一個是屬性名一個為屬性值,屬性值可以是正則表示式。

spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Query=name

只有包含屬性名 name 才會轉發,如 http://localhost:8080/order/create?name=aaa

Query 值可以以鍵值對的方式進行配置:

spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Query=name,aaa

只有當請求中包含屬性名 name 並且值為 aaa 才會轉發 ,http://localhost:8080/order/create?name=aaa

4.3 通過 Header 屬性匹配例項

spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
			# \d 表示匹配數字,+ 表示1 次或多次匹配。
            - Header=X-Request-Id, \d+

通過 postman 發起請求,在 header 頭上增加屬性 X-Request-Id ,配置值為任意數值,如 99,即可訪問成功。

五、熔斷降級

5.1 為什麼要實現熔斷降級?

在分散式系統中,閘道器作為流量的入口,因此會有大量的請求進入閘道器,向其他服務發起呼叫,其他服務不可避免的會出現呼叫失敗(超時、異常),失敗時不能讓請求堆積在閘道器上,需要快速失敗並返回給客戶端,想要實現這個要求,就必須在閘道器上做熔斷、降級操作。

5.2 基於 hystrix 熔斷降級

  1. 新增依賴
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  1. 配置
server:
  # 配置應用埠
  port: 8080
  
spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            # 配置 Hystrix
            - name: Hystrix
              args:
                name: fallbackCmdA
                # 降級呼叫 URI
                fallbackUri: forward:/fallbackA
# 設定超時時間,單位:ms
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
  1. 建立降級回撥方法
@RestController
public class FallbackController {

    @GetMapping("/fallbackA")
    public String fallbackA() {
        return "服務暫時不可用";
    }
}

  1. 啟動 OrderServicegateway 服務,並訪問 http://localhost:8080/order/create 返回:
訂單建立成功
  1. 關閉OrderService 訪問 http://localhost:8080/order/create 返回:
服務暫時不可用

證明熔斷降級已生效。

六、限流

6.1 為什麼需要限流?

  • 防止大量的請求使伺服器過載,導致服務不可用
  • 防止網路攻擊

6.2 常見的限流演算法

計數器演算法

在指定時間內對請求數做累計,當數量大於設定的值時,後續的請求都將被拒絕。當指定時間過去後,將計數重置為0,重新開始計數。
    
弊端:如果在單位時間1s內只能允許100個請求訪問,在前10ms已經通過了100個請求,那後面的990ms所有的請求都會被拒絕,這種現象稱為“突刺現象”。    

漏桶演算法

漏桶演算法可以消除"突刺現象",漏桶演算法內部有一個容器,類似生活用到的漏斗,當請求進來時,相當於水倒入漏斗,然後從下端小口慢慢勻速的流出。不管上面流量多大,下面流出的速度始終保持不變。不管服務呼叫方多麼不穩定,通過漏桶演算法進行限流,每10毫秒處理一次請求。因為處理的速度是固定的,請求進來的速度是未知的,可能突然進來很多請求,沒來得及處理的請求就先放在桶裡,既然是個桶,肯定是有容量上限,如果桶滿了,那麼新進來的請求就丟棄
    
弊端:無法應對短時間的突發流量。    

令牌桶演算法

在令牌桶演算法中,存在一個桶,用來存放固定數量的令牌。演算法中存在一種機制,以一定的速率往桶中放令牌。每次請求呼叫需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設定qps為100,那麼限流器初始化完成一秒後,桶中就已經有100個令牌了,這時服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最後相當於以一定的速率執行。

6.3 Gateway 限流支援

Spring Cloud Gateway 中,有 Filter 過濾器,因此可以在 pre 型別的 Filter 中自行實現上述三種過濾器。但是限流作為閘道器最基本的功能,Spring Cloud Gateway 官方就提供了 RequestRateLimiterGatewayFilterFactory 這個類,適用在 Redis 內的通過執行 Lua 指令碼實現了令牌桶的方式。具體實現邏輯在 RequestRateLimiterGatewayFilterFactory 類中,lua 指令碼在如下圖所示的資料夾中:

6.4 例項

  1. 新增 redis 依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  1. 在配置檔案中配置
server:
  # 配置應用埠
  port: 8080


spring:
  application:
    # 配置應用名稱
    name: gateway
  cloud:
    nacos:
      discovery:
        # 註冊服務中心地址
        server-addr: 192.168.205.10:8848
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - name: RequestRateLimiter
              args:
                # 用於限流的鍵的解析器的 Bean 物件的名字,通過 SpEL 表示式從 Spring 容器中獲取 
                key-resolver: '#{@hostAddrKeyResolver}'
                # 令牌桶每秒填充平均速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的上限
                redis-rate-limiter.burstCapacity: 3
  redis:
    host: localhost
    port: 6379
    database: 0
  1. 自定義限流策略,通過實現 KeyResolver 介面

基於 hostAddress 限流:

public class HostAddrKeyResolver implements KeyResolver {

    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
    
}

基於 URI 限流:

public class UriKeyResolver implements KeyResolver {

    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getURI().getPath());
    }

}

基於使用者 限流:

public class UserKeyResolver implements KeyResolver {

    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

}

只允許一個策略生效,這裡我們採用 HostAddrKeyResolver :

@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
	return new HostAddrKeyResolver();
}
  1. 啟動 OrderServicegateway 服務,通過 jmeter 併發訪問

可以看到請求部分成功,部分失敗。