1. 程式人生 > >Spring Cloud 系列之 Gateway 服務閘道器(四)

Spring Cloud 系列之 Gateway 服務閘道器(四)

本篇文章為系列文章,未讀第一集的同學請猛戳這裡:

  • Spring Cloud 系列之 Gateway 服務閘道器(一)
  • Spring Cloud 系列之 Gateway 服務閘道器(二)
  • Spring Cloud 系列之 Gateway 服務閘道器(三)

本篇文章講解 Gateway 閘道器如何實現限流、整合 Sentinel 實現限流以及高可用閘道器環境搭建。

  

閘道器限流

  

  顧名思義,限流就是限制流量,就像你寬頻包有 1 個 G 的流量,用完了就沒了。通過限流,我們可以很好地控制系統的 QPS,從而達到保護系統的目的。

  

為什麼需要限流

  

  比如 Web 服務、對外 API,這種型別的服務有以下幾種可能導致機器被拖垮:

  • 使用者增長過快(好事)
  • 因為某個熱點事件(微博熱搜)
  • 競爭物件爬蟲
  • 惡意的請求

  這些情況都是無法預知的,不知道什麼時候會有 10 倍甚至 20 倍的流量打進來,如果真碰上這種情況,擴容是根本來不及的。

  

  

  從上圖可以看出,對內而言:上游的 A、B 服務直接依賴了下游的基礎服務 C,對於 A,B 服務都依賴的基礎服務 C 這種場景,服務 A 和 B 其實處於某種競爭關係,如果服務 A 的併發閾值設定過大,當流量高峰期來臨,有可能直接拖垮基礎服務 C 並影響服務 B,即雪崩效應。

  

限流演算法

  

  點選連結觀看:限流演算法視訊(獲取更多請關注公眾號「哈嘍沃德先生」)

  

  常見的限流演算法有:

  • 計數器演算法
  • 漏桶(Leaky Bucket)演算法
  • 令牌桶(Token Bucket)演算法

  

計數器演算法

  

  計數器演算法是限流演算法裡最簡單也是最容易實現的一種演算法。比如我們規定,對於 A 介面來說,我們 1 分鐘的訪問次數不能超過 100 個。那麼我們可以這麼做:在一開始的時候,我們可以設定一個計數器 counter,每當一個請求過來的時候,counter 就加 1,如果 counter 的值大於 100 並且該請求與第一個請求的間隔時間還在 1 分鐘之內,觸發限流;如果該請求與第一個請求的間隔時間大於 1 分鐘,重置 counter 重新計數,具體演算法的示意圖如下:

  

  這個演算法雖然簡單,但是有一個十分致命的問題,那就是臨界問題,我們看下圖:

  從上圖中我們可以看到,假設有一個惡意使用者,他在 0:59 時,瞬間傳送了 100 個請求,並且 1:00 又瞬間傳送了 100 個請求,那麼其實這個使用者在 1 秒裡面,瞬間傳送了 200 個請求。我們剛才規定的是 1 分鐘最多 100 個請求,也就是每秒鐘最多 1.7 個請求,使用者通過在時間視窗的重置節點處突發請求, 可以瞬間超過我們的速率限制。使用者有可能通過演算法的這個漏洞,瞬間壓垮我們的應用。

  

  還有資料浪費的問題存在,我們的預期想法是希望 100 個請求可以均勻分散在這一分鐘內,假設 30s 以內我們就請求上限了,那麼剩餘的半分鐘伺服器就會處於閒置狀態,比如下圖:

  

漏桶演算法

  

  漏桶演算法其實也很簡單,可以粗略的認為就是注水漏水的過程,往桶中以任意速率流入水,以一定速率流出水,當水超過桶流量則丟棄,因為桶容量是不變的,保證了整體的速率。

  漏桶演算法是使用佇列機制實現的。

  

  漏桶演算法主要用途在於保護它人(服務),假設入水量很大,而出水量較慢,則會造成閘道器的資源堆積可能導致閘道器癱瘓。而目標服務可能是可以處理大量請求的,但是漏桶演算法出水量緩慢反而造成服務那邊的資源浪費。

  漏桶演算法無法應對突發呼叫。不管上面流量多大,下面流出的速度始終保持不變。因為處理的速度是固定的,請求進來的速度是未知的,可能突然進來很多請求,沒來得及處理的請求就先放在桶裡,既然是個桶,肯定是有容量上限,如果桶滿了,那麼新進來的請求就會丟棄。

  

令牌桶演算法

  

  令牌桶演算法是對漏桶演算法的一種改進,漏桶演算法能夠限制請求呼叫的速率,而令牌桶演算法能夠在限制呼叫的平均速率的同時還允許一定程度的突發呼叫。在令牌桶演算法中,存在一個桶,用來存放固定數量的令牌。演算法中存在一種機制,以一定的速率往桶中放令牌。每次請求呼叫需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌。

場景大概是這樣的:桶中一直有大量的可用令牌,這時進來的請求可以直接拿到令牌執行,比如設定 QPS 為 100/s,那麼限流器初始化完成一秒後,桶中就已經有 100 個令牌了,等服務啟動完成對外提供服務時,該限流器可以抵擋瞬時的 100 個請求。當桶中沒有令牌時,請求會進行等待,最後相當於以一定的速率執行。

  

  Spring Cloud Gateway 內部使用的就是該演算法,大概描述如下:

  • 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
  • 根據限流大小,設定按照一定的速率往桶裡新增令牌;
  • 桶設定最大的放置令牌限制,當桶滿時、新新增的令牌就被丟棄或者拒絕;
  • 請求到達後首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除;
  • 令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流。

  漏桶演算法主要用途在於保護它人,而令牌桶演算法主要目的在於保護自己,將請求壓力交由目標服務處理。假設突然進來很多請求,只要拿到令牌這些請求會瞬時被處理呼叫目標服務。

  

Gateway 限流

  

  點選連結觀看:Gateway 服務限流視訊(獲取更多請關注公眾號「哈嘍沃德先生」)

  

  Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory 過濾器工廠,使用 RedisLua 指令碼實現了令牌桶的方式。

  官網文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-redis-ratelimiter 具體實現邏輯在 RequestRateLimiterGatewayFilterFactory 類中,Lua 指令碼在如下圖所示的原始碼資料夾中:

  

新增依賴

  

<!-- spring data redis reactive 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- commons-pool2 物件池依賴 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

  

限流規則

  

URI 限流

  

  配置限流過濾器和限流過濾器引用的 bean 物件。

spring:
  application:
    name: gateway-server # 應用名稱
  cloud:
    gateway:
      # 路由規則
      routes:
        - id: product-service           # 路由 ID,唯一
          uri: lb://product-service     # lb:// 根據服務名稱從註冊中心獲取服務請求地址
          predicates:                   # 斷言(判斷條件)
            # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
            - Path=/product/**
          filters:                       # 閘道器過濾器
            # 限流過濾器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
                key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表示式按名稱引用 bean
  # redis 快取
  redis:
    timeout: 10000        # 連線超時時間
    host: 192.168.10.101  # Redis伺服器地址
    port: 6379            # Redis伺服器埠
    password: root        # Redis伺服器密碼
    database: 0           # 選擇哪個庫,預設0庫
    lettuce:
      pool:
        max-active: 1024  # 最大連線數,預設 8
        max-wait: 10000   # 最大連線阻塞等待時間,單位毫秒,預設 -1
        max-idle: 200     # 最大空閒連線,預設 8
        min-idle: 5       # 最小空閒連線,預設 0

  

  編寫限流規則配置類。

package com.example.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
 * 限流規則配置類
 */
@Configuration
public class KeyResolverConfiguration {

    /**
     * 限流規則
     *
     * @return
     */
    @Bean
    public KeyResolver pathKeyResolver() {
        /*
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return Mono.just(exchange.getRequest().getPath().toString());
            }
        };
         */
        // JDK 1.8
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }

}

  

  多次訪問:http://localhost:9000/product/1 結果如下:

  

  Redis 結果如下:

  

引數限流

  

  配置限流過濾器和限流過濾器引用的 bean 物件。

spring:
  application:
    name: gateway-server # 應用名稱
  cloud:
    gateway:
      # 路由規則
      routes:
        - id: product-service           # 路由 ID,唯一
          uri: lb://product-service     # lb:// 根據服務名稱從註冊中心獲取服務請求地址
          predicates:                   # 斷言(判斷條件)
            # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
            - Path=/product/**
          filters:                       # 閘道器過濾器
            # 限流過濾器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
                key-resolver: "#{@parameterKeyResolver}" # 使用 SpEL 表示式按名稱引用 bean
  # redis 快取
  redis:
    timeout: 10000        # 連線超時時間
    host: 192.168.10.101  # Redis伺服器地址
    port: 6379            # Redis伺服器埠
    password: root        # Redis伺服器密碼
    database: 0           # 選擇哪個庫,預設0庫
    lettuce:
      pool:
        max-active: 1024  # 最大連線數,預設 8
        max-wait: 10000   # 最大連線阻塞等待時間,單位毫秒,預設 -1
        max-idle: 200     # 最大空閒連線,預設 8
        min-idle: 5       # 最小空閒連線,預設 0

  

  編寫限流規則配置類。

package com.example.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
 * 限流規則配置類
 */
@Configuration
public class KeyResolverConfiguration {

    /**
     * 根據引數限流
     *
     * @return
     */
    @Bean
    public KeyResolver parameterKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    }

}

  

  多次訪問:http://localhost:9000/product/1?userId=123 結果如下:

  

  Redis 結果如下:

  

IP 限流

  

  配置限流過濾器和限流過濾器引用的 bean 物件。

spring:
  application:
    name: gateway-server # 應用名稱
  cloud:
    gateway:
      # 路由規則
      routes:
        - id: product-service           # 路由 ID,唯一
          uri: lb://product-service     # lb:// 根據服務名稱從註冊中心獲取服務請求地址
          predicates:                   # 斷言(判斷條件)
            # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
            - Path=/product/**
          filters:                       # 閘道器過濾器
            # 限流過濾器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
                key-resolver: "#{@ipKeyResolver}" # 使用 SpEL 表示式按名稱引用 bean
  # redis 快取
  redis:
    timeout: 10000        # 連線超時時間
    host: 192.168.10.101  # Redis伺服器地址
    port: 6379            # Redis伺服器埠
    password: root        # Redis伺服器密碼
    database: 0           # 選擇哪個庫,預設0庫
    lettuce:
      pool:
        max-active: 1024  # 最大連線數,預設 8
        max-wait: 10000   # 最大連線阻塞等待時間,單位毫秒,預設 -1
        max-idle: 200     # 最大空閒連線,預設 8
        min-idle: 5       # 最小空閒連線,預設 0

  

  編寫限流規則配置類。

package com.example.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
 * 限流規則配置類
 */
@Configuration
public class KeyResolverConfiguration {

    /**
     * 根據 IP 限流
     *
     * @return
     */
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }

}

  

  多次訪問:http://localhost:9000/product/1 結果如下:

  

  Redis 結果如下:

  

Sentinel 限流

  

  點選連結觀看:Sentinel 服務限流視訊(獲取更多請關注公眾號「哈嘍沃德先生」)

  

  Sentinel 支援對 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 進行限流。

  官網文件:

  • https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
  • https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway

  

建立專案

  

  建立 gateway-server-sentinel 專案。

  

新增依賴

  

  單獨使用新增 sentinel gateway adapter 依賴即可。

  若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依賴來讓 spring-cloud-alibaba-sentinel-gateway 模組裡的 Spring Cloud Gateway 自動化配置類生效。

  同時請將 spring.cloud.sentinel.filter.enabled 配置項置為 false(若在閘道器流控控制檯上看到了 URL 資源,就是此配置項沒有置為 false)。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>gateway-server-sentinel</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>gateway-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 專案依賴 -->
    <dependencies>
        <!-- spring cloud gateway 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 單獨使用 -->
        <!-- sentinel gateway adapter 依賴 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
        </dependency>
        <!-- 和 Sentinel Starter 配合使用 -->
        <!--
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        -->
    </dependencies>

</project>

  

配置檔案

  

server:
  port: 9001 # 埠

spring:
  application:
    name: gateway-server-sentinel # 應用名稱
  cloud:
    sentinel:
      filter:
        enabled: false
    gateway:
      discovery:
        locator:
          # 是否與服務發現元件進行結合,通過 serviceId 轉發到具體服務例項。
          enabled: true                  # 是否開啟基於服務發現的路由規則
          lower-case-service-id: true    # 是否將服務名稱轉小寫
      # 路由規則
      routes:
        - id: order-service           # 路由 ID,唯一
          uri: lb://order-service     # 目標 URI,lb:// 根據服務名稱從註冊中心獲取服務請求地址
          predicates:                 # 斷言(判斷條件)
            # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
            - Path=/order/**

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設定服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

  

限流規則配置類

  

  使用時只需注入對應的 SentinelGatewayFilter 例項以及 SentinelGatewayBlockExceptionHandler 例項即可。

  GatewayConfiguration.java

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 載入閘道器限流規則
        initGatewayRules();
    }

    /**
     * 閘道器限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是閘道器中的 route 名稱或者使用者自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間視窗,單位是秒,預設是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間視窗,單位是秒,預設是 1 秒
        // 載入閘道器限流規則
        GatewayRuleManager.loadRules(rules);
    }

}

  

啟動類

  

  GatewayServerSentinelApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 開啟 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,預設會開啟該註解
//@EnableEurekaClient
@SpringBootApplication
public class GatewayServerSentinelApplication {

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

}

  

訪問

  

  多次訪問:http://localhost:9001/order/1 結果如下:

  介面 BlockRequestHandler 的預設實現為 DefaultBlockRequestHandler,當觸發限流時會返回預設的錯誤資訊:Blocked by Sentinel: FlowException。我們可以通過 GatewayCallbackManager 定製異常提示資訊。

  

自定義異常提示

  

  GatewayCallbackManagersetBlockHandler 註冊函式用於實現自定義的邏輯,處理被限流的請求。

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 載入閘道器限流規則
        initGatewayRules();
        // 載入自定義限流異常處理器
        initBlockHandler();
    }

    /**
     * 閘道器限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是閘道器中的 route 名稱或者使用者自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間視窗,單位是秒,預設是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間視窗,單位是秒,預設是 1 秒
        // 載入閘道器限流規則
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 自定義限流異常處理器
     */
    private void initBlockHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map<String, String> result = new HashMap<>();
                result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
                result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                result.put("route", "order-service");
                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(result));
            }
        };

        // 載入自定義限流異常處理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

}

  

訪問

  

  多次訪問:http://localhost:9001/order/1 結果如下:

  

分組限流

  

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 載入閘道器限流規則
        initGatewayRules();
        // 載入自定義限流異常處理器
        initBlockHandler();
    }

    /**
     * 閘道器限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是閘道器中的 route 名稱或者使用者自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間視窗,單位是秒,預設是 1 秒
         */
        // rules.add(new GatewayFlowRule("order-service")
        //         .setCount(3) // 限流閾值
        //         .setIntervalSec(60)); // 統計時間視窗,單位是秒,預設是 1 秒
        // --------------------限流分組----------start----------
        rules.add(new GatewayFlowRule("product-api")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間視窗,單位是秒,預設是 1 秒
        rules.add(new GatewayFlowRule("order-api")
                .setCount(5) // 限流閾值
                .setIntervalSec(60)); // 統計時間視窗,單位是秒,預設是 1 秒
        // --------------------限流分組-----------end-----------
        // 載入閘道器限流規則
        GatewayRuleManager.loadRules(rules);
        // 載入限流分組
        initCustomizedApis();
    }

    /**
     * 自定義限流異常處理器
     */
    private void initBlockHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map<String, String> result = new HashMap<>();
                result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
                result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                result.put("route", "order-service");
                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(result));
            }
        };

        // 載入自定義限流異常處理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

    /**
     * 限流分組
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        // product-api 組
        ApiDefinition api1 = new ApiDefinition("product-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 匹配 /product-service/product 以及其子路徑的所有請求
                    add(new ApiPathPredicateItem().setPattern("/product-service/product/**")
                            .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});

        // order-api 組
        ApiDefinition api2 = new ApiDefinition("order-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 只匹配 /order-service/order/index
                    add(new ApiPathPredicateItem().setPattern("/order-service/order/index"));
                }});
        definitions.add(api1);
        definitions.add(api2);
        // 載入限流分組
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

}

  

訪問

  

  訪問:http://localhost:9001/product-service/product/1 觸發限流

  訪問:http://localhost:9001/order-service/order/index 觸發限流

  訪問:http://localhost:9001/order-service/order/1 不會觸發限流

  

高可用閘道器

  

  業內通常用多少 9 來衡量網站的可用性,例如 QQ 的可用性是 4 個 9,就是說 QQ 能夠保證在一年裡,服務在 99.99% 的時間是可用的,只有 0.01% 的時間不可用,大約最多 53 分鐘。

  對於大多數網站,2 個 9 是基本可用;3 個 9 是叫高可用;4 個 9 是擁有自動恢復能力的高可用。

  實現高可用的主要手段是資料的冗餘備份和服務的失效轉移,這兩種手段具體可以怎麼做呢,在閘道器裡如何體現?主要有以下幾個方向:

  • 叢集部署
  • 負載均衡
  • 健康檢查
  • 節點自動重啟
  • 熔斷
  • 服務降級
  • 介面重試

  

Nginx + 閘道器叢集實現高可用閘道器

  

  

下載

  

  官網:http://nginx.org/en/download.html 下載穩定版。為了方便學習,請下載 Windows 版本。

  

安裝

  

  解壓檔案後直接執行根路徑下的 nginx.exe 檔案即可。

  Nginx 預設埠為 80,訪問:http://localhost:80/ 看到下圖說明安裝成功。

  

配置閘道器叢集

  

  進入 Nginx 的 conf 目錄,開啟 nginx.conf 檔案,配置閘道器叢集:

http {

	...

    # 閘道器叢集
	upstream gateway {
		server 127.0.0.1:9000;
		server 127.0.0.1:9001;
	}
	
    server {
        listen       80;
        server_name  localhost;

        ...

        # 代理閘道器叢集,負載均衡呼叫
		location / {
            proxy_pass http://gateway;
        }

        ...
    }
    
    ...
    
}

  

訪問

  

  啟動兩臺閘道器伺服器 http://localhost:9000/http://localhost:9001/ 和相關服務。

  訪問:http://localhost/product-service/product/1 實現高可用閘道器。

  

總結

  

  一個請求過來,首先經過 Nginx 的一層負載,到達閘道器,然後由閘道器負載到真實後端,若後端有問題,閘道器會進行重試訪問,多次訪問後仍返回失敗,可以通過熔斷或服務降級立即返回結果。而且,由於是負載均衡,閘道器重試時不一定會訪問到出錯的後端。

  

  至此 Gateway 服務閘道器所有的知識點就講解結束了。

  本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

  大家可以通過 分類 檢視更多關於 Spring Cloud 的文章。