微服务网关实战Spring-Cloud-Gateway

9次阅读

共计 11534 个字符,预计需要花费 29 分钟才能阅读完成。

作为 Netflix Zuul 的替代者,Spring Cloud Gateway 是一款非常实用的微服务网关,在 Spring Cloud 微服务架构体系中发挥非常大的作用。本文对 Spring Cloud Gateway 常见使用场景进行了梳理,希望对微服务开发人员提供一些帮助。

微服务网关 SpringCloudGateway

1. 概述

Spring cloud gateway 是 spring 官方基于 Spring 5.0、Spring Boot2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供简单、有效和统一的 API 路由管理方式,Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且还基于 Filer 链的方式提供了网关基本的功能,例如:安全、监控 / 埋点、限流等。

2. 核心概念

网关提供 API 全托管服务,丰富的 API 管理功能,辅助企业管理大规模的 API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等贡呢。一般来说网关对外暴露的 URL 或者接口信息,我们统称为路由信息。如果研发过网关中间件或者使用过 Zuul 的人,会知道网关的核心是 Filter 以及 Filter Chain(Filter 责任链)。Sprig Cloud Gateway 也具有路由和 Filter 的概念。下面介绍一下 Spring Cloud Gateway 中几个重要的概念。

  • 路由。路由是网关最基础的部分,路由信息有一个 ID、一个目的 URL、一组断言和一组 Filter 组成。如果断言路由为真,则说明请求的 URL 和配置匹配
  • 断言。Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring5.0 框架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 http request 中的任何信息,比如请求头和参数等。
  • 过滤器。一个标准的 Spring webFilter。Spring cloud gateway 中的 filter 分为两种类型的 Filter,分别是 Gateway Filter 和 Global Filter。过滤器 Filter 将会对请求和响应进行修改处理

如上图所示,Spring cloudGateway 发出请求。然后再由 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway web handler。Handler 再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。

快速入门

以 Spring Boot 框架开发为例,启动一个 Gateway 服务模块(以 Consul 作为注册中心),一个后端服务模块。client 端请求经 gateway 服务把请求路由到后端服务。

前提条件:

  • Consul:版本 1.5.0。
  • Spring bot:版本 2.1.5。
  • Spring cloud:版本 Greenwich.SR1。
  • Redis:版本 5.0.5。

1. 微服务开发

这里以使用 Spring Boot 框架开发微服务为例,启动一个服务并注册到 Consul。

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>


注册服务到 Consul,配置文件配置如下:

spring:
  application:
    name: service-consumer
  cloud:
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        service-name: service-consumer


如下定义 RestController,发布 HTTP 接口。

@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;
    @GetMapping(value = "/info")
    public User info() {return userService.info();
    }
}

注:此为服务端配置,经 Gateway 把请求路由转发到该服务上。

2. 网关配置
创建一个 Gateway 服务,引入以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>


启动类配置如下:

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);
    }
}


Spring Cloud Gateway 对 client 端请求起到路由功能,主要配置如下:

server:
  port: 8098
spring:
  application:
    name: service-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true   
          lower-case-service-id: true  
    consul:
      host: 127.0.0.1 #注册 gateway 网关到 consul
      port: 8500
      discovery:
        service-name: service-gateway


此时使用 http://localhost:8089/service-consumer/user/info 访问服务,网关即可对服务进行路由转发,把请求转发到具体后端服务上。此时,url 中使用的 url 前缀 service-consumer,是后端服务在 Consul 注册的服务名称转为小写字母以后的字符串。

最佳实践

01

Gateway 网关配置

本文第二部分开发规范中定义了网关进行路由转发的配置,除了上述配置方式还可以使用下面的方式进行配置:

gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
      - id: service_consumer
        uri: lb://service-consumer
        predicates:
        - Path= /consumer/**
        filters:
        - StripPrefix=1


在上面的配置中,配置了一个 Path 的 predicat, 将以 /consumer/** 开头的请求都会转发到 uri 为 lb://service-consumer 的地址上,lb://service-consumer(注册中心中服务的名称)即 service-consumer 服务的负载均衡地址,并用 StripPrefix 的 filter 在转发之前将 /consumer 去掉。同时将 spring.cloud.gateway.discovery.locator.enabled 改为 false,如果不改的话,之前的 http://localhost:8081/service-consumer/user/info 这样的请求地址也能正常访问,因为这时为每个服务创建了 2 个 router。

本文第二部分和本节一共讲述了两种配置方式,两种配置都可以实现请求路由转发的功能。参数 spring.cloud.gateway.discovery.locator.enabled 为 true,表明 Gateway 开启服务注册和发现的功能,并且 Spring Cloud Gateway 自动根据服务发现为每一个服务创建了一个 router,这个 router 将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId 是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了)。

gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true


02

Gateway 跨域访问

Spring Cloud Gateway 还针对跨域访问做了设计,可以使用以下配置解决跨域访问问题:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "https://docs.spring.io"
            allowedMethods:
            - GET
            allowHeaders:
            - Content-Type


在上面的示例中,允许来自 https://docs.spring.io 的 get 请 …。

03

Gateway 过滤器

Spring Cloud Gateway 的 filter 生命周期不像 Zuul 那么丰富,它只有两个:“pre”和“post”:

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

Spring Cloud gateway 的 filter 分为两种:GatewayFilter 和 Globalfilter。GlobalFilter 会应用到所有的路由上,而 Gatewayfilter 将应用到单个路由或者一个分组的路由上。

利用 Gatewayfilter 可以修改请求的 http 的请求或者是响应,或者根据请求或者响应做一些特殊的限制。更多时候可以利用 Gatewayfilter 做一些具体的路由配置。

下面的配置是 AddRequestParameter Gatewayfilter 的相关配置。

spring:
  application:
    name: service-gateway
  cloud:
    gateway:
     discovery:
        locator:
         enabled: true
     routes:
     - id: parameter_route
      uri: http://localhost:8504/user/info
      filters:
      - AddRequestParameter=foo, bar
      predicates:
      - Method=GET


上述配置中指定了转发的地址,设置所有的 GET 方法都会自动添加 foo=bar, 当请求符合上述路由条件时,即可在后端服务上接收到 Gateway 网关添加的参数。

另外再介绍一种比较常用的 filter,即 StripPrefix gateway filter。

配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: stripprefixfilter
        uri: lb://service-consumer
        predicates:
        - Path=/consumer/**
        filters:
        - StripPrefix=1


当 client 端使用 http://localhost:8098/consumer/user/info 路径进行请求时,如果根据上述进行配置 Gateway 会将请求转换为 http://localhost:8098/service-consumer/user/info。以此作为前端请求的最终目的地。

04

Gateway 请求匹配

Gateway 网关可以根据不同的方式进行匹配进而把请求分发到不同的后端服务上。

通过 header 进行匹配,把请求分发到不同的服务上,配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: http://baidu.com
        predicates:
        - Header=X-Request-Id, \d+


通过 curl 测试:curl http://localhost:8080 -H “X-Request-Id:666666”, 返回页面代码证明匹配成功。

如果是以 Host 进行匹配,配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://baidu.com
        predicates:
        - Host=**.baidu.com


通过 curl http://localhost:8098 -H “Host: www.baidu.com” 进行测试,返回页面代码即转发成功。

可以通过 POST、GET、PUT、DELTE 等不同的方式进行路由:

spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: http://baidu.com
        predicates:
        - Method=GET


通过 curl http://localhost:8098 进行测试,返回页面代码即表示成功。

上述是单个匹配进行路由,如果把多个匹配合在一起进行路由,必须满足所有的路有条件才会进行路由转发。

05

Gateway 熔断

Spring Cloud Gateway 也可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级,同时项目中必须加上 Hystrix 的依赖。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>    


配置后,Gateway 将使用 fallbackcmd 作为名称生成 HystrixCommand 对象进行熔断处理。如果想添加熔断后的回调内容,需要添加以下配置:

spring:
  cloud:
    gateway:
      routes:
      - id: hystrix_route
        uri: lb://consumer-service
        predicates:
        - Path=/consumer/**
        filters:
        - name: Hystrix
          args:
            name: fallbackcmd
            fallbackUri: forward:/fallback
        - StripPrefix=1
hystrix:  
  command:
    fallbackcmd:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000 #超时时间,若不设置超时时间则有可能无法触发熔断


上述配置中给出了熔断之后返回路径,因此,在 Gateway 服务模块添加 /fallback 路径,以作为服务熔断时的返回路径。

@RestController
public class GatewayController {@RequestMapping(value = "/fallback")
    public String fallback(){return "fallback nothing";}
}


fallbackUri: forward:/fallback 配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到 /fallback 这个 URI,并以此路径的返回值作为返回结果。

06

Gateway 重试路由器

通过简单的配置,Spring Cloud Gateway 就可以支持请求重试功能。

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: http://localhost:8504/user/info
        predicates:
        - Path=/user/**
        filters:
        - name: Retry
          args:
            retries: 3
            status: 503
        - StripPrefix=1


Retry GatewayFilter 通过四个参数来控制重试机制,参数说明如下:

  • retries:重试次数,默认值是 3 次。
  • statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
  • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有 5 个值。

使用上述配置进行测试,当后台服务不可用时,会在控制台看到请求三次的日志,证明此配置有效。

07

Gateway 限流操作

Spring Cloud Gateway 本身集成了限流操作,Gateway 限流需要使用 Redis,pom 文件中添加 Redis 依赖:

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

配置文件中配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: rate_limit_route
        uri: lb://service-consumer
        predicates:
        - Path=/user/**
        filters:
        - name: RequestRateLimiter
          args:
            key-resolver: "#{@hostAddrKeyResolver}"
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 3
        - StripPrefix=1
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        service-name: service-gateway
        instance-id: service-gateway-233

  redis:
    host: localhost
    port: 6379


在上面的配置问价中,配置了 Redis 的信息,并配置了 RequestRateLimiter 的限流过滤器,该过滤器需要配置三个参数:

  • BurstCapacity:令牌桶的总容量。
  • replenishRate:令牌通每秒填充平均速率。
  • Key-resolver:用于限流的解析器的 Bean 对象的名字。它使用 SpEL 表达式 #{@beanName}从 Spring 容器中获取 bean 对象。

注意:filter 下的 name 必须是 RequestRateLimiter。

Key-resolver 参数后面的 bean 需要自己实现,然后注入到 Spring 容器中。KeyResolver 需要实现 resolve 方法,比如根据 ip 进行限流,则需要用 hostAddress 去判断。实现完 KeyResolver 之后,需要将这个类的 Bean 注册到 Ioc 容器中。还可以根据 uri 限流,同 hostname 限流是一样的。例如以 ip 限流为例,在 gateway 模块中添加以下实现:

public class HostAddrKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

    public HostAddrKeyResolver hostAddrKeyResolver() {return new HostAddrKeyResolver();
    }
}


把该类注入到 spring 容器中:

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public HostAddrKeyResolver hostAddrKeyResolver(){return new HostAddrKeyResolver();
    }
}


基于上述配置,可以对请求基于 ip 的访问进行限流。

08

自定义 Gatewayfilter

Spring Cloud Gateway 内置了过滤器,能够满足很多场景的需求。当然,也可以自定义过滤器。在 Spring Cloud Gateway 自定义过滤器,过滤器需要实现 GatewayFilter 和 Ordered 这两个接口。

下面的例子实现了 Gatewayfilter,它可以以 log 日志的形式记录每次请求耗费的时间,具体实现如下:

public class RequestTimeFilter implements GatewayFilter, Ordered {private static final Log log = LogFactory.getLog(GatewayFilter.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
                    if (startTime != null) {log.info("请求路径:"+exchange.getRequest().getURI().getRawPath() + "消耗时间:" + (System.currentTimeMillis() - startTime) + "ms");
                    }
                })
        );
    }
    @Override
    public int getOrder() {return 0;}
}


上述代码中定义了自己实现的过滤器。Ordered 的 int getOrder()方法是来给过滤器定优先级的,值越大优先级越低。还有一个 filter(ServerWebExchange exchange, GatewayFilterChain chain)方法,在该方法中,先记录了请求的开始时间,并保存在 ServerWebExchange 中,此处是一个“pre”类型的过滤器。然后再 chain.filter()的内部类中的 run()方法中相当于 ”post” 过滤器,在此处打印了请求所消耗的时间。

接下来将该过滤器注册到 router 中,代码如下。

 @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {return builder.routes()
                .route(r -> r.path("/user/**")
                        .filters(f -> f.filter(new RequestTimeFilter())
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("http://localhost:8504/user/info")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();}

除了上述代码的方式配置我们自定义的过滤器的方式之外,也可以在 application.yml 文件中直接配置,这里不再赘述。

启动程序,通过 curl http://localhost:8098/user/info 控制台会打印出请求消耗时间,日志如下:

....
2019-05-22 15:13:31.221  INFO 19780 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter   : 请求路径:/user/info 消耗时间: 54ms
...
2019-05-22 16:46:23.785  INFO 29928 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter   : 请求路径:/user/info3 消耗时间: 5ms
....


09

自定义 GlobalFilter

Spring Cloud Gateway 根据作用范围分为 GatewayFilter 和 GlobalFilter,二者区别如下:

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

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

在上一小节中定义的是 Gatewayfilter,下面实现的是 Globalfilter:

public class TokenFilter implements GlobalFilter, Ordered {

Logger logger= LoggerFactory.getLogger(TokenFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getQueryParams().getFirst("token");
    if (token == null || token.isEmpty()) {logger.info( "token 为空,无法进行访问.");
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }
    return chain.filter(exchange);
}

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

}

上述代码实现了 Globalfilter,具体逻辑是判断请求中是否含参数 token,如果没有,则校验不通过,对所有请求都有效。如果含有 token 则转发到具体后端服务上,如果没有则校验不通过。

通过 curl http://localhost:8098/user/info 进行访问,因为路径中不含有参数 token,则无法通过校验,打印日志如下:

2019-05-22 15:27:11.078  INFO 5956 --- [ctor-http-nio-1] com.song.gateway.TokenFilter             : token 为空,无法进行访问.
...


通过 curl http://localhost:8098/user/info?token=123 进行访问时,则可以获取到后端服务返回结果。

正文完
 0