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 進行限流。

官網文檔:

創建項目

創建 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 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~


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