SpringCloud Gateway 学习总结

 

学习视频:https://www.bilibili.com/video/BV1io4y1m72G?p=1

 

1. 什么是Spring Cloud Gateway

  Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代 Netfix Zuul,Zuul并不仅提供统一的路由方式,并且还基于Filter 链的方式提供了网关的基本功能。目前最新版 Spring Cloud 中引用的还是Zuul 1.x版本,而这个版本是基于过滤器的,是阻塞IO,并不支持长连接。

  Spring Cloud Gateway 是基于Spring 生态系统之上狗叫的API网关,包括Spring5,Spring Boot 2 和 Project Reactor。Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到API,并为它们提供跨领域的关注点,例如:安全性,监视/指标,限流等。 由于Spring 5.0 支持Netty,Http2,而Spring Boot 2.0 支持 Spring 5.0,因此Spring Cloud Gateway 支持Netty 和 Http2 顺利成章。

 

2. 什么是服务网关

  API Gateway(APIGW / API网关),顾名思义,是出现在系统边界上的一个面向API的、串行集中式的强管控服务,这里的边界是企业IT系统的边界,可以理解为 企业级应用防火墙,主要起到 隔离外部访问与内部系统的作用。在微服务概念的流行之前,API网关就已经诞生了,例如银行、证券等领悦常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。

  API 网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一个Web应用,扩展到多种使用场景,且每种引用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API 网关成了微服务架构的一个标配组件。

  API 网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供定制的API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。

  API网关并不是微服务场景中必须的组件。但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入API网关也有一系列的好处:

  (1)聚合接口,使服务对调用者透明,客户端和后端的耦合度降低;

  (2)聚合后台服务,节省流量,提高性能,提升用户体验;

  (3)提供安全、流控、过滤、缓存、计费、监控等API管理功能;

 

3. 为什么要使用网关

  单体应用:浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对于单体应用来说不需要网关的。

  微服务:微服务的应用可能部署在不同机房、不同地区、不同域名下。此时客户端(浏览器/手机/软件工具)想要请求对应的服务,都需要知道机器的具体IP或者域名URL,当微服务实例众多时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把骑牛转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。

  总结:

  如果让客户端直接与各个微服务交互:

  (1)客户端会多次请求不通的微服务,增加了客户端的复杂性

  (2)存在跨域请求,在一定场景下处理相对复杂;

  (3)身份认证问题,每个微服务需要独立身份认证;

  (4)难以重构,随着项目的迭代,可能需要重新划分微服务;

  (5)某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难;

  因此,我们需要网关介于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只需要与网关教育,只需要知道网关地址即可。这样便简化了开发且有一些优点:

  (1)易于监控,可在微服务网关手机监控数据并将其推送到外部系统进行分析;

  (2)易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,从而无需在每个微服务中进行认证;

  (3)减少了客户端与各个微服务之间的交互次数;

  网关具有身份认证与安全、审查与监控、动态路由、缓存、请求分片与管理、静态响应处理等功能。当然最主要的职责还是与“外界联系”。总结一下,网关应当具备以下功能:

  性能:API高可用,负载均衡,容错机制

  安全:权限身份认证、脱敏、流量清洗、后端签名(保证全链路可信调用),黑名单(非法调用的限制)

  日志:日志记录,一旦涉及分布式,全链路跟踪必不可少

  缓存:数据缓存。

  监控:记录请求响应数据,API耗时分析,性能监控。

  限流:流向控制,错峰流控,可以自定义多种限流规则。

  灰度:线上灰度部署,可以减小风险。

  路由:动态路由规则。

 

4. 环境准备

  在开发之前我们要准备以下几个项目

  注册中心:eureka-server 、eureka-server02 

  商品服务:product-service  提供了服务主键查询商品接口 http://localhost:7070/product/{id}

  订单服务:order-service    提供了根据主键查询订单接口  http://localhost:9000/order/{id} 且订单服务调用商品服务。

  具体代码就不提供了,大家可以按照自己的业务逻辑去模拟。

 

5. Gateway 实现API网关

  Gateway官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/

 

5.1 核心概念

  路由(Route):路由是网关最基础的部分,路由信息由ID、目标URI、一组断言和一个过滤器组成。如果断言路由为真,则说明请求的URI和配置匹配。

  断言(Predicate):Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去自定义匹配来自于 Http Request 中的任何信息,比如请求头和参数等。

  过滤器(Filter):一个标准的Spring Web Filter。Spring Cloud Gateway 中的Filter 分为两种类型,分别是Gateway Filter 和Global Filter。过滤器将会对请求和影响进行处理。

 

5.2 工作原理

 

 

   这是Spring Cloud Gateway上提供的工作原理图。如上图所示,客户端向Spring Cloud Gateway 发出请求。再由网关处理程序 Gateway Handler Mapping 映射确定与请求向匹配的路由,将其发送到网关Web处理程序 Gateway Web Handler。该处理器程序通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器由虚线分隔的原因是,过滤器可以在发送带来请求之前和之后运行逻辑。所有pre 过滤器逻辑均被执行,然后发出代理请求。发出代理请求后,将运行post 过滤器逻辑。

 

5.3 搭建Gateway服务

5.3.1 创建项目

  我们在之前的项目包里创建一个 gateway-server 的项目

 

5.3.2 添加依赖

  在pom.xml文件中添加Spring Cloud Gateway 的依赖。

        <!-- spring cloud gateway 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

 

5.3.3 配置文件

  在application.yml 文件中添加基本的配置信息

server:
  port: 9000

spring:
  application:
    name: gateway-server  # 应用名称

 

5.3.4 启动类

@SpringBootApplication
public class GatewayServerApplication {

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

}

 

5.4 配置路由规则

  下面我们在yml文件中配置一段最基本的路由规则

spring:
  application:
    name: gateway-server  # 应用名称
  cloud:
    gateway:
      # 路由规则
      routes: 
        - id: product-service           # 路由ID,唯一
          uri: http://localhost:7070/   # 目标URI,路由到微服务的地址
          predicates:                   # 断言(判断条件)
            - Path=/product/**          # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后

  请求接口: http://localhost:9000/product/1  将会路由到 http://localhost:7070/product/1

 

6 路由规则

  Spring Cloud Gateway 创建 Route 对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate对象可以赋值给 Route。

  (1)Spring Cloud Gateway 包含需要内置的 Route Predicate Factories。

  (2)所有这些断言都匹配 HTTP 请求的不同属性。

  (3)多个 Route Predicate Factories 可以通过逻辑与(and)结合起来一起使用。

  路由断言工厂 RoutePredicateFactory 包含的主要实现类如图所示,包含 Datetime、请求的远端地址、路由权重、请求头、Host地址、请求方法、请求路径和请求参数等类型的路由断言。

 

 

 

6.1 Path 路由

spring:
  application:
    name: gateway-server  # 应用名称
  cloud:
    gateway:
      # 路由规则
      routes:
        - id: product-service           # 路由ID,唯一
          uri: http://localhost:7070/   # 目标URI,路由到微服务的地址
          predicates:                   # 断言(判断条件)
            - Path=/product/**          # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后

  请求接口: http://localhost:9000/product/1  将会路由到 http://localhost:7070/product/1

 

10.2 Query 路由

spring:
  application:
    name: gateway-server  # 应用名称
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: http://localhost:7070/  # 目标URI,路由到微服务的地址
          predicates:                  # 断言(判断条件)
            # - Query=token            # 匹配请求参数中包含 token 的请求
            - Query=token, abc.        # 匹配请求参数中包含 token 并且参数值满足正则表达式是 abc. 的请求

  Query=token   匹配请求,比如:http://localhost:9000/product/1?token=123

  Query=token, abc.    匹配请求,比如:http://localhost:9000/product/1?token=abc1

 

5.3 Method 路由

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: http://localhost:7070/  # 目标URI,路由到微服务的地址
          predicates:                  # 断言(判断条件)
            - Method=GET               # 匹配任何GET请求

 

5.4 Datetime 路由

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: http://localhost:7070/  # 目标URI,路由到微服务的地址
          predicates:                  # 断言(判断条件)
            - After=2020-02-02T20:20:20.000+08:00[Asia/Shanghai]  # 匹配中国上海时间 2020-02-02 20:20:20 之后的请求

 

5.5 RemoteAddr 路由

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: http://localhost:7070/  # 目标URI,路由到微服务的地址
          predicates:                  # 断言(判断条件)
            - RemoteAddr=192.168.10.1/0 # 匹配远程地址请求的是 RemoteAddr的请求,O表示子网掩码

  RemoteAddr=192.168.10.1/0     匹配情况,比如:http://192.168.10.1:9000/product/1

 

5.6 Header 路由

spring:
  application:
    name: gateway-server  # 应用名称
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: http://localhost:7070/  # 目标URI,路由到微服务的地址
          predicates:                  # 断言(判断条件)
            - Header=X-Request-Id, \d+ # 匹配请求头包含X-Request-Id 并且其值匹配正则表达式 \d+ 的请求

 

6 动态路由

  动态路由其实就是面向服务的路由,Spring Cloud Gateway 支持与 Eureka / Nacos 整合开发,根据 serviceId 自动从注册中心获取服务地址并且转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改 Gateway 的路由配置。

 

6.1 添加依赖

  我们需要添加注册中心的jar包,按照自己要求添加eureka 或者 nacos 的client 的jar。

  修改gateway-server 的yml文件,添加注册中心的相关配置。

 

6.2 动态获取URI

  我们主要修改uri 的相关配置,把原来的 IP 换成服务名称

      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            - Path=/product/**         # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后

 

6.3 服务名称转发

  如果我们要请求Order服务,我们就需要在配置文件上配置一个order-servcie的路由。

  如果我们也很多个微服务,按照上面的原则,每个微服务都需要在这里配置他的微服务名。

  但是,SpringCloud 提供了一个“约定大于配置”,根据服务名称转发的功能,配置后自动读取注册中心的服务名,用于自动转发。

spring:
  cloud:
    gateway:
# 去掉之前的路由配置 discovery: locator: # 是否与服务发生组件进行结合,通过 serviceId 转发到具体的服务实例。 enabled
: true # 是否开启基于服务发现的路由规则 lower-case-service-id: true # 是否将服务名称转小写

  这是请求的URL就是:http://localhost:9000/order-service/order/1

  URL在IP和端口后面,紧跟这的是服务名称,后面才是接口地址。

 

7. 过滤器

  Spring Cloud Gateway 根据作用范围划分为 GatewayFilterGlobalFilter,二者区别如下:

  GatewayFilter:网关过滤器,需要通过 spring.cloud.routes.filters 配置在具体路由上,只作用在当前路由上或通过 spring.cloud.default.filters 配置在全局,作用在所有路由上。

  GlobalFilter:全局过滤器,不需要在配置文件上配置,作用在所有的路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器,它为请求业务以及路由的 URI 转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。

 

7.1 网关过滤器 GatewayFilter

  网关过滤器用于拦截并链式处理Web请求,可以实现横切与应用无关的需求,比如:安全、访问超时的设置等。修改传入的HTTP请求或传出HTTP响应。Spring Cloud Gateway 包含许多内置的网关过滤器工厂,一共22个。包括头部过滤器、路径过滤器、Hystrix过滤器和重写请求URL的过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工程的用途来划分,可以分为以下几种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter 和 Hystrix。

 

7.1.1 Path 路径过滤器

  Path 路径过滤器可以实现URL 重写,通过重写URL可以实现隐藏实际路径提高安全性,易于用户记忆和键入,易于被搜索引擎收录等优点。实现方式如下:

7.1.1.1 RewritePathGatewayFilterFactory

  RewritePath 网关过滤器工厂采集路径正则表达式参数和替换参数,使用Java正则表达式来灵活地重写请求地址。

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      # 路由规则
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/product/**, /api-gateway/**     
          filters:
            # 将 /api-gateway/product/1 重写为 /product/1
            - RewritePath=/api-gateway(?<segment>/?.*), $\{segment}

  访问:http://localhost:9000/api-gateway/product/1 结果与  http://localhost:9000/product/1   相同

 

7.1.1.2 PrefixPathGatewayFilterFactory

  PrefixPath 网关过滤器工厂为匹配的URI 添加指定前缀。

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/**
          filters:
            # 将 /1 重写为 /product/1
            - PrefixPath=/product

  访问地址:http://localhost:9000/1 结果与  http://localhost:9000/product/1 相同

 

7.1.1.3 StripPrefixGatewayFilterGateway

  StripPrefix 网关过滤器工厂采用一个参数 StripPrefix,该参数表示在将请求发送到下游之前,从请求中剥离的路径个数。

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/**
          filters:
            # 将 /api/123/product/1 重写为 /product/1
            - StripPrefix=2

  访问地址:http://localhost:9000/api/123/product/1 结果与 http://localhost:9000/product/1 相同

 

7.1.1.4 SetPathGatewayFilterGateway

  Setpath网关过滤器工厂采用路径模板参数。它提供了一种通过允许模板路径段来操作请求路径的简单方法,使用Spring Framework 中的 uri 模板,允许多个匹配段。
 
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/api/product/{segment}
          filters:
            # 将 /api//product/1 重写为 /product/1
            - SetPath=/product/{segment}

  访问地址:http://localhost:9000/api/product/1

 

7.1.2 Parameter 参数过滤器

  AddRequestParameter 网关过滤器工厂会将制定参数添加至匹配到下游请求中。

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/api-gateway/**
          filters:
            # 将 /api-gateway/product/1 重写为 /product/1
            - RewritePath=/api-gateway/(?<segment>/?.*),$\{segment}
            # 在下游请求中添加 flag=1
            - AddRequestParameter=flag,1

  注意:filter 是可以组合使用的,这里我们同时使用了 RewritePath 和 AddReequestParameter ,访问地址:http://localhost:9000/api-gateway/product/1

  我们可以修改这个接口:/product/1  多一个Integer flag 参数,打印日志后看是否可以收到此参数。

  

7.1.3 Status 状态过滤器

  SetStatus 网关过滤器工厂采用单个状态参数,它必须是有效的 Spring HttpStatus。它可以是整数 404 或枚举 NOT_FOUND 的字符串表示。
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/api-gateway/**
          filters:
            # 将 /api-gateway/product/1 重写为 /product/1
            - RewritePath=/api-gateway/(?<segment>/?.*),$\{segment}
            # 任何情况下,响应的 HTTP 状态都将设置为 404
            - SetStatus=404

  访问地址:http://localhost:9000/api-gateway/product/1  发现虽然后返回结果,但Http的状态码是404

 

7.2 全局过滤器

  全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器,它是请求业务以及路由的 URI 转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
  下面这些是Gateway内置的全局过滤器,已经在所有路由生效。

 

 

7.3 自定义过滤器

  即使 Spring Cloud Gateway 自带许多实用的 GatewayFilter FactoryGateway FilterGlobalFilter,但是在很多情景下我们仍然系统可以自定义自己的过滤器,实现一些自定义操作。

7.3.1 自定义网关过滤器

  自定义网关过滤器需要实现以下两个接口:GatewayFilterOrdered

 7.3.1.1 创建过滤器

/**
 * 自定义网关过滤器
 */
public class CustomGatewayFilter implements GatewayFilter, Ordered {

    /**
     * 过滤器,业务逻辑
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("自定义网关过滤器被执行");
        // 继续往下执行
        return chain.filter(exchange);
    }

    /**
     * 过滤器执行顺序,数值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

 

 7.3.1.2 注册过滤器

  自定义的网关过滤器是需要显示配置之后,才能生效的。基本配置内容如下:

/**
 * 自定义网关过滤器,配置类
 */
@Configuration
public class GatewayRoutesConfiguration {

    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
        return builder.routes().route(r -> r
                // 断言(判断条件)
                .path("/product/**")
                // 目标URI,路由到微服务的地址
                .uri("lb://product-service")
                // 注册自定义网关过滤器
                .filters(new CustomGatewayFilter())
                // 路由ID,唯一
                .id("product-service"))
                .build();
    }

}

 

7.3.2 自定义全局过滤器

  自定义全局过滤器需要实现以下两个即可欧:GlobalFilterOrdered,通过全局过滤器可以实现权限校验,安全性验证等功能。

7.3.2.1 创建过滤器

  实现指定接口,添加 @Component 注解即可。

/**
 * 自定义全局过滤器
 */
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    /**
     * 过滤器逻辑
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("自定义全局过滤器被执行");
        // 继续往下执行
        return chain.filter(exchange);
    }

    /**
     * 过滤器执行顺序,数值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

 

7.3.3 统一鉴权

  接下来我们在网关过滤器中通过 token 判断用户是否登录,完成一个统一鉴权案例。

7.3.3.1 创建过滤器

/**
 * 鉴权过滤器
 */
@Component
public class AccessFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = "";
        // 从Header中获取Token
        List<String> headerTokenList = exchange.getRequest().getHeaders().get("token");
        if(headerTokenList != null && headerTokenList.size() > 0){
            token = headerTokenList.get(0);
        } else {
            // 从请求参数,获取Token
            token = exchange.getRequest().getQueryParams().getFirst("token");
        }
        if(null == token || token.equals("")){
            System.out.println("warn token is null ...");
            ServerHttpResponse response = exchange.getResponse();
            // 响应类型
            response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
            // 响应状态码,HTTP 401 错误代表用户没有访问权限
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 响应内容
            String message = "{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}";
            DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
            // 请求结束,不在继续往下请求
            return response.writeWith(Mono.just(buffer));
        }
        // TODO 接下来应该是验证Token是否有效的逻辑
        System.out.println("token is OK!");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

 

8 网关限流

  顾名思义,限流就是限制流量,就像你带宽包有1个G的流量,用完了就没了。通过限流,我们可以很好地控制系统的QPS,从而达到保护系统的目的。

 

8.1 为什么需要限流

  比如Web服务、对外API,这种类型的服务有以下几种可能导致机器被拖垮:

  (1)用户增长过快(好事)

  (2)因为某个热点事件(微博热搜,秒杀)

  (3)竞争对手爬虫

  (4)恶意的请求

  这些情况是无法预知的,不知道什么时候回有10倍甚至20倍的流量打引来,如果真碰上这种情况,扩容是根本来不及的。

 

8.2 限流算法

  常见的限流算法有:计数器算法、漏桶(Leaky Bucket)算法、令牌桶(Token Bucket) 算法

 

8.2.1 计数器算法

  计数器算法是限流算法里最简单也是最容器实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter 就加1, 如果 counter 的值大于100,并且该请求与第一个请求的间隔时间还在1分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于1分钟,重置counter重新计数,具体算法的示意图如下:

  这个算法虽然简单,但有一个十分致命的问题,那就是临界问题。

  假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒里面,瞬间发送了200个请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。

  还有资源浪费的问题存在,我们的预期想法是系统100个请求可以均匀分散到这1分钟内,假设30s以内我们就请求上限了,name剩余的半分钟服务器就会处于闲置状态。

 

8.2.2 漏桶算法

  漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率注入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。

  漏桶算法的示意图在网上有很多,大家可以自己找来看看。

 

  漏桶算法是使用队列机制实现的。

 

8.2.3 令牌桶算法

   令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可以用令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数量达到上线,就丢弃令牌。

  场景大概是这样的:桶中一直有大量的可用令牌,这是进来的请求可以直接拿到令牌执行,比如设置QPS为100/s,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。

  Spring Cloud Gateway 内部使用的就是该算法,大概描述如下:

  (1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;

  (2)根据限流大小,设置按照一定的速率往桶里添加令牌;

  (3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;

  (4)请求到达后首先要获取令牌桶中的令牌,拿到令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;

  (5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后就不会删除令牌,以此保证足够的限流。

  漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到令牌这些请求会瞬时被处理调用目标服务。

 

8.3 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 脚本在如下图所以所示的源码文件夹中:

 

 

 

8.3.1 添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

 

8.3.2 限流规则

8.3.2.1 URI 限流

  配置限流过滤器和限流过滤器引用的Bean对象。

@Configuration
public class KeyResolverConfiguration {

    /**
     * 限流规则
     */
    @Bean
    public KeyResolver pathKeyResolver(){
        // 匿名内部类的写法
//        return new KeyResolver() {
//            @Override
//            public Mono<String> resolve(ServerWebExchange exchange) {
//                return Mono.just(exchange.getRequest().getPath().toString());
//            }
//        };

        // JDK1.8 lambda写法
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }
}

  配置文件内容:

server:
  port: 9000

spring:
  application:
    name: gateway-server
  cloud:
    # 注册中心
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id : product-service         # 路由ID,唯一
          uri: lb://product-service    # lb:// 根据服务名称从注册中心获取服务请求地址
          predicates:                  # 断言(判断条件)
            # 匹配对应URL的请求,将匹配到的请求追加在目标URI之后
            - Path=/product/**
          filters:
            # 限流过滤器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter:
                  replenishRate: 1 # 令牌桶每秒填充速率
                  burstCapacity: 2 # 令牌桶总容量
                key-resolver: "#{@pathKeyResolver}" # 使用SpEL 表达式按名称引用bean
  # redis 配置
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    timeout: 10000  # 连接超时时间
    lettuce:
      pool:
        max-active: 1024 # 最大连接数,默认 8
        max-wait: 10000  # 最大连接阻塞等待时间,默认毫秒,默认 -1
        max-idle: 200    # 最大空闲连接,默认 8
        min-idle: 5      # 最小空闲连接,默认 0

  注意:为了测试限流,我们需要把之前的自定义网关配置,注释掉。GatewayRoutesConfiguration

  请求地址:http://localhost:9000/product/1?token=123

  当我们每秒请求1次的时候,都可以正常访问。如果在1秒内多次访问地址,就会出现限流提示:

  此时我们打开Redis的可视化工具,就可以看到Gateway限流功能,自动生成了以下2个key

 

8.3.2.2 参数限流

  注意:KeyResolverConfiguration配置类里面,同时只能出现一个@Bean 对象。所以要把前一个删掉。否则Gateway启动会报错。

@Configuration
public class KeyResolverConfiguration {

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

}

  配置文件只需要修改红色部分

          filters:
            # 限流过滤器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter:
                  replenishRate: 1 # 令牌桶每秒填充速率
                  burstCapacity: 2 # 令牌桶总容量
                key-resolver: "#{@parameterKeyResolver}" # 使用SpEL 表达式按名称引用bean

  请求地址:http://localhost:9000/product/1?token=123&userId=1

  因为我们配置的参数限流的参数是userId,所有在请求地址里必须传入userId,否则在 parameterKeyResolver 方法里会报空指针异常。所以正常的逻辑在 parameterKeyResolver 还需要判空的。

  当多次请求后也会出现上面的限流提示。

  Redis中的Key如下:

 

 

8.3.2.3 IP限流

  加入IP限流的Bean对象,并且注销之前的Bean

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

  修改配置类的 红色部分

          filters:
            # 限流过滤器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter:
                  replenishRate: 1 # 令牌桶每秒填充速率
                  burstCapacity: 2 # 令牌桶总容量
                key-resolver: "#{@ipKeyResolver}" # 使用SpEL 表达式按名称引用bean

  请求地址:http://localhost:9000/product/1?token=123&userId=1

  这时的请求就与参数无关了,Gateway会抓取请求的IP地址,Redis的Key如下:

 

 

8.4 Sentinel 限流

  Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。

 

 

  Sentinel 网关限流公共模块(API Gateway Adapter Common)支持的功能包括:请求属性解析、网关规则管理、网关规则检查、调用参数组装、自定义API分组管理、API路径匹配

  官网文档: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

 

8.4.1 创建项目

  创建一个 gateway-server-sentinel 的项目。

 

8.4.2 添加依赖

  单独使用添加 sentinel gateway adapter 依赖即可。

  若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖,同时需要添加 spring-cloud-starter-gateway 依赖来让 spring-cloud-alibaba-sentinel-gateway 模块里的 Spring Cloud Gateway 自动化配置类生效。

  同时请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。Sentinel 网关流控默认的粒度是 route 维度以及自定义 API 分组维度,默认不支持 URL 粒度。如需细化到 URL 粒度,请参考 网关流控文档 自定义 API 分组。

  注意:网关流控规则数据源类型是 gw-flow,若将网关流控规则数据源指定为 flow 则不生效。

    <dependencies>
        <!-- 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
        <!-- spring cloud gateway 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
        <!-- Gateway 和 Sentinel Starter 配合使用 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
    </dependencies>

 

8.4.3 yml配置文件

server:
  port: 9001

spring:
  application:
    name: gateway-server-sentinel
  cloud:
    # 注册中心
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # 禁止网关流控控制台上看到 URL 资源
    sentinel:
      filter:
        enabled: false
    gateway:
      discovery:
        locator:
          # 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务实例
          enabled: true                # 是否开启基于服务发现的路由规则
          lower-case-service-id: true  # 是否将服务名转为小写
      # 路由规则
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            # 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后
            - Path=/order/**

 

8.4.4 限流规则配置类

  使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。

/**
 * 限流规则配置类
 * @author he.zhang
 * @date 2022/4/21 14:36
 */
@Configuration
public class GatewayConfiguration {

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

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

    /**
     * 限流异常处理器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(){
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

//    /**
//     * 限流过滤器,使用Sentinel Starter 方式集成,会自动继承,不需要配置
//     */
//    @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);
    }
}

 

8.4.5 启动类

@SpringBootApplication
public class GatewayServerSentinelApplication {

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

 

8.4.6 访问

  多次访问:http://localhost:9001/order/1  结果如下:Blocked by Sentinel: ParamFlowException

  接口 BlockReequestHandler 的默认实现了 DefaultBlockRequestHandler,当触发限流时会返回默认的错误信息:Blocked by Sentinel: FlowException。我们可以通过 GatewayCallbackManager 定制异常提示信息。

 

8.4.7 自定义异常提示

  GatewayCallbackManagersetBlockHandler 注册函数用于实现自定义的逻辑,处理被限流的请求。

  接下来我们在 GatewayConfiguration 这个配置里改造,新建一个 initBlockHandler 类,并在doInit 初始化方法里调用。

    /**
     * Spring 容器初始化的时候执行该方法
     */
    @PostConstruct
    public void doInit(){
        // 加载网关限流规则
        initGatewayRules();
        initBlockHandler();
    }/**
     * 自定义限流异常处理器
     */
    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);
    }

  我重启 gateway-server-sentinel 网关之后,再次频繁请求 http://localhost:9001/order/1 接口,返回信息如下:{"code":"429","route":"order-service","message":"Too Many Requests"}

 

8.4.8 分组限流

  改造 GatewayConfiguration 类的 initGatewayRules 方法,并新写一个 initCustomizedApis 方法。

    /**
     * 网关限流规则
     */
    private void initGatewayRules(){
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
         * resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
         * count:限流阈值
         * intervalSec:统计时间窗口,单位是秒,默认是 1 秒
         */
        // ------------  限流分组 ---------------
        rules.add(new GatewayFlowRule("product-api")
                .setCount(3)           // 限流阈值
                .setIntervalSec(60));  // 统计时间窗口,单位是秒,默认是1秒
        rules.add(new GatewayFlowRule("order-api")
                .setCount(3)           // 限流阈值
                .setIntervalSec(60));  // 统计时间窗口,单位是秒,默认是1秒
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
        // 加载限流分组
        initCustomizedApis();
    }

    /**
     * 限流分组
     */
    public 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/order-service/order/1    多次访问不会被限制

  http://localhost:9001/order-service/order/index  开始可以访问通,多次访问后被限制

  http://localhost:9001/product-service/product/**  下面的所有接口,访问多次都会被限制

 

9 高可用网关

  业内通常用多少 9 来衡量网站的可用性,例如QQ的可用性是4个9,就是说 QQ 能够保证在一年里,服务在 99.99% 的时间是可用的,只有 0.01% 的时间不可用,大约最多53 分钟。

  对于大多数的网站,2 个 9 是基本可用; 3 个 9 是叫高可用; 4 个 9 是拥有自动回复能力的高可用。

  实现高可用的主要手段是 数据的冗余备份 和 服务的失效转移,这两种手段具体可以怎么做呢?在网关里如何体现?主要有以下几个方向: 集群部署、负载均衡、健康检查、节点自动重启、熔断、服务降级、接口重试

 

9.1 Nginx + 网关集群实现高可用网关

  通过Nginx拦截请求,再把请求轮询转到网关集群上每一个实例。

 

 

 

 

 

 

posted @ 2022-04-21 17:00  闲人鹤  阅读(844)  评论(0编辑  收藏  举报