美文网首页Spring Cloud
Spring Cloud 系列之 Gateway 服务网关(四)

Spring Cloud 系列之 Gateway 服务网关(四)

作者: 哈喽沃德先生 | 来源:发表于2020-04-23 10:52 被阅读0次

    本篇文章为系列文章,未读第一集的同学请猛戳这里:

    本篇文章讲解 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 的文章。

    🤗 您的点赞转发是对我最大的支持。

    📢 关注公众号 哈喽沃德先生「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~

    相关文章

      网友评论

        本文标题:Spring Cloud 系列之 Gateway 服务网关(四)

        本文链接:https://www.haomeiwen.com/subject/cjjoihtx.html