Loading

50-Gateway

1. Gateway

1.1 简单介绍

【网关】单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务。「网关」的核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上。网关还能做统一的熔断、限流、认证、日志监控等。也可以和服务注册中心完美的整合,如:Eureka、Consul、Nacos。

https://spring.io/projects/spring-cloud-gateway

在 SpringCloud 微服务体系中,有个很重要的组件就是网关,在 1.x 版本中都是采用的 Zuul 网关;但在 2.x 版本中,Zuul 的升级一直跳票,SpringCloud 最后自己研发了一个网关替代 Zuul,那就是 SpringCloud Gateway。

网上很多地方都说 Zuul 是阻塞的,Gateway 是非阻塞的,这么说是不严谨的,准确的讲 Zuul1.x 是阻塞的,而在 2.x 的版本中,Zuul 也是基于 Netty,也是非阻塞的,如果一定要说性能,其实这个真没多大差距。

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。而为了提升网关的性能,SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty,所以天生就是异步非阻塞的。

在“一个请求 → 网关根据一定的条件匹配 → 匹配成功之后可以将请求转发到指定的服务地址”这个过程中,我们可以进行一些比较具体的控制(限流、日志、黑白名单等)。

【小结】微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权、安全控制、日志统一处理、易于监控、限流等相关功能。

1.2 功能特征

  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 动态路由:能够匹配任何请求属性;
  • 集成 Spring Cloud 服务发现功能;
  • 集成 Hystrix 的断路器功能;
  • 请求限流功能;
  • 支持路径重写。

上图中是核心的流程图,最主要的就是 Route、Predicates 和 Filters 作用于特定路由。

  • 「Route」是网关最基础的部分,也是网关比较基础的工作单元。路由由一个 ID、一个目标 URL(最终路由到的地址)、一系列的 Predicate 断言(匹配条件判断)和 Filter 过滤器(精细化控制)组成。如果断言为 true,则匹配该路由;
  • 「Predicate」参考了 Java8 中的断言 java.util.function.Predicate,开发人员可以匹配 HTTP 请求中的所有内容(包括请求头、请求参数等)(类似于 Nginx 中的 location 匹配一样),如果断言与请求相匹配则路由;
  • 「Filter」指的是框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

其中,Predicates 断言就是我们的匹配条件,而 Filter 就可以理解为一个无所不能的拦截器,有了这两个元素,结合目标 URL,就可以实现一个具体的路由转发。

1.3 工作原理

我们在学习 Gateway 之前,先弄清楚 Gateway 的工作原理,后面使用它的各个功能时,就知道该如何使用了,工作流程图如下:

  1. Gateway 的客户端会向 Spring Cloud Gateway 发起请求,请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关的上下文,然后网关的上下文会传递到 DispatcherHandler。
  2. DispatcherHandler 是所有请求的分发处理器,DispatcherHandler 主要负责分发请求对应的处理器,比如将请求分发到对应 RoutePredicateHandlerMapping(路由断言处理器映射器)。
  3. 路由断言处理映射器主要用于路由的查找,以及找到路由后返回对应的 FilteringWebHandler。
  4. FilteringWebHandler 主要负责组装 Filter 链表并调用 Filter 执行一系列 Filter 处理,然后把请求转到后端对应的代理服务处理,处理完毕后,将 Response 返回到 Gateway 客户端。

核心逻辑:路由转发 + 执行过滤器链

在 Filter 链中,通过虚线分割 Filter 的原因是,过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的 Pre 类型的 Filter 执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求完毕之后,才会执行 Post 类型的过滤器。

  • 在“Pre”类型的过滤器中可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
  • 在“Post”类型的过滤器中可以做响应头/响应体修改、日志输出、流量监控等;

2. 路由(断言)

Gateway 路由配置分为基于配置的静态路由设置和基于代码动态路由配置,静态路由是指在 application.yml 中把路由信息配置好了,而动态路由则支持在代码中动态加载路由信息,更加灵活,我们接下来把这 2 种路由操作都实现一次。

2.1 举例说明

如上图所示,要求:

  1. 用户所有请求以 /order 开始的请求,都路由到 hailtaxi-order 服务
  2. 用户所有请求以 /driver 开始的请求,都路由到 hailtaxi-driver 服务
  3. 用户所有请求以 /pay 开始的请求,都路由到 hailtaxi-pay 服务

a. 基于配置路由设置

配置参数说明:

spring:
  cloud:
    gateway:
      routes:                       # 路由配置
        - id:                       # 唯一标识符
          uri:                      # 路由地址,可以是 lb://IP:PORT,也可以是 lb://${spring.application.name}
          predicates:               # 断言(路由条件)
          - Path=/driver/**         # Predicate 接收一个输入参数,返回一个布尔值结果。这里表示匹配所有以 driver 开始的请求
          filters:                  # 过滤器
          - StripPrefix=1           # 真实路由的时候,去掉第 1 个路径,路径个数以 / 分割区分

补充配置(表明 Gateway 开启服务注册和发现的功能,并且 Gateway 自动根据服务发现为每一个服务创建了一个 router,这个 router 将以服务名开头的请求路径转发到对应的服务):

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true

b. 基于代码路由配置

我们同样实现上面的功能,但这里基于代码方式实现。所有路由规则我们可以从数据库中读取并加载到程序中。基于代码的路由配置我们只需要创建 RouteLocator 并添加路由配置即可,代码如下:

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("hailtaxi-driver", r -> r.path("/driver/**").uri("lb://hailtaxi-driver"))
        .route("hailtaxi-order", r -> r.path("/order/**").uri("lb://hailtaxi-order"))
        .route("hailtaxi-pay", r -> r.path("/pay/**").uri("lb://hailtaxi-pay"))
        .build();
}

在真实场景中,基于配置文件的方式更直观、简介,但代码的路由配置是更强大,可以实现很丰富的功能,可以把路由规则存在数据库中,每次直接从数据库中加载规则,这样的好处是可以动态刷新路由规则,通常应用于权限系统动态配置。

2.2 Predicate 断言

a. 内置断言实现

上面路由匹配规则中我们都用了- Path方式,其实就是路径匹配方式,除了路径匹配方式,Gateway 还支持很多丰富的 Predicates 匹配方式,实现了各种路由匹配规则(通过 Header、请求参数等作为条件)匹配到对应的路由。

b. 断言源码分析

以 Cookie 断言为例:

public class CookieRoutePredicateFactory extends AbstractRoutePredicateFactory<CookieRoutePredicateFactory.Config> {

    /**
     * Name key.
     */
    public static final String NAME_KEY = "name";

    /**
     * Regexp key.
     */
    public static final String REGEXP_KEY = "regexp";

    public CookieRoutePredicateFactory() {
        super(Config.class);
    }
    
    /*
      通过shortcutFieldOrder方法设置Config配置类中的属性,需要根据具体的规则来设置
      通过shortcutType方法获取具体规则,具体参看:org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType
      规则包括以下几种:
        DEFAULT : 按照shortcutFieldOrder顺序依次赋值
    */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY, REGEXP_KEY);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                List<HttpCookie> cookies = exchange.getRequest().getCookies().get(config.name);
                if (cookies == null) {
                    return false;
                }
                for (HttpCookie cookie : cookies) {
                    if (cookie.getValue().matches(config.regexp)) {
                        return true;
                    }
                }
                return false;
            }

            @Override
            public String toString() {
                return String.format("Cookie: name=%s regexp=%s", config.name, config.regexp);
            }
        };
    }
    
    /*
      内部配置类是用来接收在配置文件中配置的参数的
      routes:
        - id: hailtaxi-driver
          uri: lb://hailtaxi-driver
          predicates:
            - Cookie=username,liujiaqi
    */
    @Validated
    public static class Config {

        @NotEmpty
        private String name;

        @NotEmpty
        private String regexp;

        public String getName() {
            return name;
        }

        public Config setName(String name) {
            this.name = name;
            return this;
        }

        public String getRegexp() {
            return regexp;
        }

        public Config setRegexp(String regexp) {
            this.regexp = regexp;
            return this;
        }

    }

}

尽管 Spring Cloud Gateway 已经包含了很多路由匹配规则,有时候我们需要开发自定义路由匹配规则来满足需求,下面简单的介绍一下如何自定义路由匹配规则。

【需求】转发带 token 的请求到 hailtaxi-drvier 服务中,这里定义请求带 token 是指包含某个请求头的请求,至于是什么请求头可以由配置指定。

(1)修改 application.yml

    gateway:
      routes:
        - id: hailtaxi-driver
          uri: lb://hailtaxi-driver
          predicates:
            # 自定义一个 Token 断言,如果请求包含 Authorization 的 token 信息则通过
            - Token=Authorization

(2)创建 RoutePredicateFactory

断言工厂默认命名规则必须按照「名称 + RoutePredicateFactory」,如上 TokenRoutePredicateFactory 的断言名称为 Token。

@Slf4j
@Component // 要交给容器管理
public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory<TokenRoutePredicateFactory.Config> {

    public static final String NAME_KEY = "headerName";
  
    public TokenRoutePredicateFactory() {
        super(Config.class);
    }

    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String headerName = config.getHeaderName();
            HttpHeaders headers = exchange.getRequest().getHeaders();
            List<String> header = headers.get(headerName);
            return header!=null && header.size()>0;
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY);
    }

    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.DEFAULT;
    }

    @Data
    public static class Config {
        private String headerName;
    }
}

3. 过滤器

3.1 简单说明

过滤器作为 Gateway 的重要功能。常用于请求鉴权、服务调用时长统计、修改请求或响应 header、限流、去除路径等。

如 Gateway Filter 可以去掉 url 中的占位后转发路由,比如:

predicates:
  - Path=/resume/**
filters:
  - StripPrefix=1  # 可以去掉 resume 之后转发

从过滤器生命周期(影响时机点)的角度来说,主要有 pre 和 post:

生命周期时机点 作用
pre 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
post 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

从过滤器类型的角度,Spring Cloud GateWay 的过滤器分为 GateWayFilter 和 GlobalFilter 两种:

过滤器类型 影响范围
GateWayFilter 应用到单个路由路由上
GlobalFilter 应用到所有的路由上

GatewayFilter】需要通过 spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过 spring.cloud.default-filters 配置在全局,作用在所有路由上。Gateway 内置了多种过滤器工厂,配套的过滤器可以直接使用,如下图所示。

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

3.2 内置过滤器

举例说明内置过滤器的使用。

(1)添加响应头

AddResponseHeaderGatewayFilterFactory 属于 GatewayFilter。

对输出响应头设置属性,比如对输出的响应设置其头部属性名称为:X-Response-Default-MyName,值为 tree6x7。

spring:
  cloud:
    gateway:
      # 配置全局默认过滤器,作用在所有路由上,也可单独为某个路由配置
      default-filters:
        # 往响应过滤器中加入信息
        - AddResponseHeader=X-Response-Default-MyName,tree6x7

修改后,再进行接口调用,会发现响应头里增加了该 header。

(2)前缀处理

在项目中做开发对接接口的时候,我们很多时候需要统一 API 路径,比如统一以 /api 开始的请求调用 hailtaxi-driver 服务,但真实服务接口地址又没有 /api 路径,我们可以使用 Gateway 的过滤器处理请求路径。

在 Gateway 中可以通过配置路由的过滤器 stripPrefix 实现映射路径中的前缀处理,我们来使用一下该过滤器,再进一步做说明。

    gateway:
      routes:
      - id: hailtaxi-driver
        uri: lb://hailtaxi-driver
        predicates:
        - Path=/api/driver/**
        filters:
          - StripPrefix=1   # 去除请求uri第1个路径,即/api

上面配置最终执行如下表:

配置 路由地址 访问地址
StripPrefix=1 http://localhost:8001/api/driver/info/2 http://localhost:18081/driver/info/2
StripPrefix=2 http://localhost:8001/api/suri/driver/info/2 http://localhost:18081/driver/info/2

有时候为了简化用户请求地址,比如用户请求 http://localhost:8001/info/1 我们想统一路由到 http://localhost:18081/driver/info/1,可以使用 PrefixPath 过滤器增加前缀。

    gateway:
      routes:
      - id: hailtaxi-driver
        uri: lb://hailtaxi-driver
        predicates:
        - Path=/**
        filters:
          - PrefixPath=/driver

上面配置最终执行如下表:

配置 路由地址 访问地址
PrefixPath=/driver http://localhost:8001/info/2 http://localhost:18081/driver/info/2

3.3 自定义过滤器

a. GatewayFilter

(1)实现 GatewayFilter 接口

GatewayFilter 一般作用在某一个路由上,需要实例化创建才能使用,局部过滤器需要实现接口 GatewayFilterOrdered

public class PayFilter implements GatewayFilter,Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("[PayFilter] >>> pre");
        return chain.filter(exchange).then(Mono.fromRunnable(()->{
            System.out.println("[PayFilter] >>> post");
        }));
    }

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

使用局部过滤器:

/***
 * 路由配置
 * @param builder
 * @return
 */
 @Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
  return builder.routes()
    .route("hailtaxi-driver", r -> r.path("/api/driver/**")
           .and().cookie("username", "itheima")
           .and().header("token", "123456")
           .filters(f -> f.filter(new PayFilter())
                    .addResponseHeader("X-Response-Default-MyName", "tree6x7")
                    .addRequestHeader("myheader", "1234567")
                    .stripPrefix(1)
                   )
           .uri("lb://hailtaxi-driver")
          )
    .route("hailtaxi-order", r -> r.path("/order/**").uri("lb://hailtaxi-order"))
    .route("hailtaxi-pay", r -> r.path("/pay/**").uri("lb://hailtaxi-pay"))
    .build();
}

(2)继承 GatewayFilterFactory

如果定义局部过滤器,想在配置文件中进行配置来使用,可以继承 AbstractGatewayFilterFactory<T> 抽象类或 AbstractNameValueGatewayFilterFactory

这两个抽象类的区别就是前者接收一个参数(像 StripPrefix 和我们创建的这种),后者接收两个参数(像AddResponseHeader)。代码的编写可以参考:StripPrefixGatewayFilterFactoryAddRequestHeaderGatewayFilterFactory

过滤器工厂默认命名规则必须按照「名称 + GatewayFilterFactory」,如上 StripPrefixGatewayFilterFactory 的过滤器名称为 StripPrefix。

b. GlobalFilter

定义全局过滤器需要实现 GlobalFilter、Ordered 接口。

/**
 * 自定义全局过滤器实现 IP 访问限制
 * 定义全局过滤器,会对所有路由生效
 */
@Slf4j
@Component  // 让容器扫描到,等同于注册了
public class BlackListFilter implements GlobalFilter, Ordered {

    // 模拟黑名单(实际可以去数据库或者redis中查询)
    private static List<String> blackList = new ArrayList<>();

    static {
        blackList.add("0:0:0:0:0:0:0:1");  // 模拟本机地址
    }

    /**
     * 过滤器核心方法:
     * 获取客户端IP,判断是否在黑名单中,在的话就拒绝访问,不在的话就放行。
     * @param exchange 封装了request和response对象的上下文
     * @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从上下文中取出 request 和 response 对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 从 request 对象中获取客户端 IP
        String clientIp = request.getRemoteAddress().getHostString();
        // 拿着 clientIP 去黑名单中查询,存在的话就拒绝访问
        if(blackList.contains(clientIp)) {
            // 拒绝访问,返回!
            response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码
            log.debug("=> IP:" + clientIp + " 在黑名单中,将被拒绝访问!");
            String data = "Request be denied!";
            DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
            return response.writeWith(Mono.just(wrap));
        }

        // 合法请求,放行,执行后续的过滤器
        return chain.filter(exchange);
    }


    /**
     * 返回值表示当前过滤器的顺序(优先级),数值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

4. 网关搭建

(1)新建模块:cloud-gateway-gateway-9527

(2)新增依赖(注意!不要引入 starter-web 模块,引入 web-flux!)

<artifactId>cloud-gateway-gateway-9527</artifactId>

<!--spring boot 父启动器依赖-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-commons</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--GateWay 网关-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--引入webflux-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <!--日志依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!--测试依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--lombok工具-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.4</version>
        <scope>provided</scope>
    </dependency>

    <!--引入Jaxb,开始-->
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-core</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>2.2.10-b140310.1920</version>
    </dependency>
    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!--引入Jaxb,结束-->

    <!-- Actuator可以帮助你监控和管理Spring Boot应用-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--热部署-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>

    <!--链路追踪-->
    <!--<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>-->
</dependencies>

<dependencyManagement>
    <!--spring cloud依赖版本管理-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <!--编译插件-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>11</source>
                <target>11</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
        <!--打包插件-->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

(3)新建 application.yml

server:
  port: 9527
eureka:
  client:
    serviceUrl: # eureka server 的路径
      defaultZone: http://eurekaservera:8761/eureka/,http://eurekaserverb:8762/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
  name: lagou-cloud-gateway
  cloud:
    gateway:
      routes: # 路由可以有多个
        - id: service-autodeliver-router # 我们自定义的路由 ID,保持唯一
          # [动态路由] uri 配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址。
          # 应由网关从服务注册中心获取实例信息后,先负载再路由。
          #uri: http://127.0.0.1:8096
          uri: lb://lagou-service-autodeliver
          # [断言] 路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。
          # 该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(与或非)。
          predicates:
            - Path=/autodeliver/**
        - id: service-resume-router
          uri: lb://lagou-service-resume
          predicates:
            - Path=/resume/**
          filters:
            # http://localhost:9002/resume/openstate/1545132
            # http://127.0.0.1:8081/openstate/1545132 [404!]
            - StripPrefix=1        # 可以去掉 resume 之后转发

上面这段配置的意思是,配置了一个 id 为 service-autodeliver-router 的路由规则,当向网关发起请求 http://localhost:9527/autodeliver/checkAndBegin/1101,请求会被分发路由到对应的微服务上。

(4)结合注册中心实现动态路由

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      # 默认情况下 Gateway 会根据注册中心的服务列表,以注册中心上
      # 微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
      discovery:
        locator:
          enabled: true   # 开启从注册中心动态创建路由的功能
          lower-case-service-id: true

5. 扩展配置

5.1 跨域配置

出于浏览器的同源策略限制。

同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。

在 Spring Cloud Gateway 中配置跨域是非常简单的,如下面所示:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
              - PUT

另外一种写法就需要创建 CorsWebFilter 过滤器,代码如下:

@Bean
public CorsWebFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    // cookie跨域
    config.setAllowCredentials(Boolean.TRUE);
    config.addAllowedMethod("*");
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    // 配置前端js允许访问的自定义响应头
    config.addExposedHeader("Authorization");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
    source.registerCorsConfiguration("/**", config);
    return new CorsWebFilter(source);
}

5.2 高可用

网关作为非常核心的一个部件,如果挂掉,那么所有请求都可能无法路由处理,因此我们需要做 GateWay 的高可用。

GateWay 的高可用很简单:可以启动多个 GateWay 实例来实现高可用,在 GateWay 的上游使用 Nginx 等负载均衡设备进行负载转发以达到高可用的目的。

启动多个 GateWay 实例(假如说两个,端口分别为 9002 和 9003),剩下的就是使用 Nginx 完成负载代理即可。示例如下:

# 配置多个 GateWay 实例
upstream gateway {
  server 127.0.0.1:9002;
  server 127.0.0.1:9003;
}
location / {
  proxy_pass http://gateway;
}

6. 源码分析

[源码专栏] https://zhuanlan.zhihu.com/p/359118792

spring-cloud-gateway 核心配置类,里面初始化了很多核心 bean,列出部分:

  • GatewayProperties
  • NettyConfiguration
  • RouteDefinitionLocator
  • PropertiesRouteDefinitionLocator
  • RouteLocator
  • RouteRefreshListener
  • RoutePredicateFactory
  • RoutePredicateHandlerMapping
  • FilteringWebHandler
  • AdaptCachedBodyGlobalFilter
  • RetryGatewayFilterFactory
  • PrefixPathGatewayFilterFactory
  • GatewayControllerEndpoint

大致协作流程为:

  1. route 层:RouteDefinitionLocator 加载 Route 配置,通过 RouteLocator 获取 Route;
  2. handler 层:Route 先后被 RoutePredicateHandlerMapping、FilteringWebHandler 处理;
  3. filter 层:Route 被各种 XxxFilter 处理。

/**
 * Central dispatcher for HTTP request handlers/controllers. Dispatches to
 * registered handlers for processing a request, providing convenient mapping
 * facilities.
 *
 * 'DispatcherHandler' discovers the delegate components it needs from
 * Spring configuration. It detects the following in the application context:
 * 
 * > HandlerMapping       -- map requests to handler objects
 * > HandlerAdapter       -- for using any handler interface
 * > HandlerResultHandler -- process handler return values
 *
 * 'DispatcherHandler' is also designed to be a Spring bean itself and
 * implements 'ApplicationContextAware' for access to the context it runs
 * in. If 'DispatcherHandler' is declared with the bean name "webHandler"
 * it is discovered by 'WebHttpHandlerBuilder#applicationContext' which creates
 * a processing chain together with 'WebFilter', 'WebExceptionHandler' and others.
 *
 * A 'DispatcherHandler' bean declaration is included in
 * 'org.springframework.web.reactive.config.EnableWebFlux' @EnableWebFlux configuration.
 *
 */
public class DispatcherHandler implements WebHandler, ApplicationContextAware {

	@Nullable
	private List<HandlerMapping> handlerMappings;

	@Nullable
	private List<HandlerAdapter> handlerAdapters;

	@Nullable
	private List<HandlerResultHandler> resultHandlers;

	// ...

	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		if (this.handlerMappings == null) {
			return createNotFoundError();
		}
		return Flux.fromIterable(this.handlerMappings)
        // --- 1.为请求找对应的Handler
				.concatMap(mapping -> mapping.getHandler(exchange))
				.next()
				.switchIfEmpty(createNotFoundError())
        // --- 2.Handler处理本次请求
				.flatMap(handler -> invokeHandler(exchange, handler))
				.flatMap(result -> handleResult(exchange, result));
	}

	private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
		if (this.handlerAdapters != null) {
			for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
				if (handlerAdapter.supports(handler)) {
					return handlerAdapter.handle(exchange, handler);
				}
			}
		}
		return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
	}

	private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {
		if (this.resultHandlers != null) {
			for (HandlerResultHandler resultHandler : this.resultHandlers) {
				if (resultHandler.supports(handlerResult)) {
					return resultHandler;
				}
			}
		}
		throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue());
	}

	private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
		return getResultHandler(result).handleResult(exchange, result)
				.checkpoint("Handler " + result.getHandler() + " [DispatcherHandler]")
				.onErrorResume(ex ->
						result.applyExceptionHandler(ex).flatMap(exResult -> {
							String text = "Exception handler " + exResult.getHandler() +
									", error=\"" + ex.getMessage() + "\" [DispatcherHandler]";
							return getResultHandler(exResult).handleResult(exchange, exResult).checkpoint(text);
						}));
	}
}

(1)为请求找合适的 Handler

(2)Handler 处理本次请求

除了上面两张图列的,还有别的内置 Filter,比如通过 Netty 发送请求到目标服务的、路由转发的 ...

7. 扩展:限流

7.1 Nginx限流

(1)控制速率(突发流量)

语法:limit_req_zone key zone rate

  • key:定义限流对象,binary_remote_addr就是一种key,基于客户端 IP 限流;
  • Zone:定义共享存储区来存储访问信息,10m 可以存储 16w IP 地址访问信息;
  • Rate:最大访问速率,rate=10r/s 表示每秒最多请求 10 个请求;
  • burst=20:相当于桶的大小;
  • Nodelay:快速处理

(2)控制并发连接数

  • limit_conn perip 20:对应的 key 是 $binary_remote_addr,表示限制单个 IP 同时最多能持有 20 个连接。
  • limit_conn perserver 100:对应的 key 是 $server_name,表示虚拟主机(server)同时能处理并发连接的总数。

7.2 网关限流

配置文件中,微服务路由设置添加局部过滤器 RequestRateLimiter。

  • key-resolver:定义限流对象(ip、路径、参数),需代码实现,使用 spel 表达式获取;
  • replenishRate:令牌桶每秒填充平均速率;
  • urstCapacity:令牌桶总容量。

7.3 小结

Q:你们项目中有没有做过限流 ? 怎么做的 ?

(1)先来介绍业务,什么情况下去做限流,需要说明QPS具体多少

我们当时有一个活动,到了假期就会抢购优惠券,QPS 最高可以达到 2000,平时 10~50 之间,为了应对突发流量,需要做限流。

常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是多少(压测结果)。

(2)Nginx 限流

控制速率(突发流量),使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量。

控制并发数,限制单个 IP 的链接数和并发链接的总数。

(3)网关限流

在 Gateway 中支持局部过滤器 RequestRateLimiter 来做限流,使用的是令牌桶算法。

可以根据 IP 或路径进行限流,可以设置每秒填充平均速率和令牌桶总容量。

posted @ 2022-04-10 16:48  tree6x7  阅读(75)  评论(0编辑  收藏  举报