Spring Cloud Gateway 实践

Spring Cloud Gateway 实践

Gateway 服务发现的路由规则

如果使用Zuul作为网关,Spring Cloud Zuul 访问后端服务时,服务发现的默认路由是:http://zuul_host:zuul_post/微服务在Eureka上的/servicesId/**,Spring Cloud Gateway 在不同的注册中心下的基于服务发现的路由规则如下所示:

注册中心 描述
Eureka 通过网关转发服务调用,访问网关URL是http://Gateway_HOST:Gateway_PORT/大些ServiceId/*,服务名默认必须大写,否则会抛404错误,如果服务名要用小写,可在属性配置文件中添加spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true配置解决
Zookeeper 通过网关转发服务调用,服务名默认小写,无需特殊处理
Consul 通过网关转发服务调用,服务名默认小写,无需特殊处理

Gateway 服务发现路由概念

通过Eureka作为注册中心实现Gateway服务发现路由规则。

服务发现路由规则案例

工程介绍

工程 端口 描述
cloud-gty-eureka-case 父包
case-consumer 8000 服务消费者
case-eureka 8761 Eureka注册中心
case-gateway 9000 基于Spring Cloud Gateway的网关server
case-provider 8001 服务提供者

启动顺序(asc)

  • case-eureka
  • case-provider
  • case-consumer
  • case-gateway

启动后访问

访问地址:localhost:9000/case-consumer/hello/frank
浏览器返回:

Hello ' frank
Thu Jan 24 16:35:58 CST 2019

知识点

  • case-gateway(application.yml
.....
        locator:
          # 是否与服务发现组件进行结合,通过serviceId转发到具体的服务实例。默认为false,为true代表开启基于服务发现的路由规则。
          enabled: true
          # 配置之后访问时无需大写
          lower-case-service-id: true
......

总结

程序启动时将服务都注册到EurekaServer中,Gateway作为入口,访问consumer,consumer通过feign向eureka-server发现对应的provider

Gateway Filter 和 Global Filter

Spring Cloud Gateway Filter从接口实现上分为两种:Gateway FilterGlobal Filter

Gateway Filter 和 Global Filter 概念

  • Gateway Filter
    Gateway Filter通过复制Web Filter实现,是一个Filter过滤器,可以对访问的URL过滤,进行横切处理(切面处理),应用场景:超时、安全等。

  • Global Filter
    Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。Global Filter是一个全局的Filter,作用于所有路由。

Gateway Filter 和 Global Filter 区别

应用场景:

  • Gateway Filter:
    全局系统统计,例如请求服务、请求时长、请求过滤等
  • Global Filter:
    应用在有针对性服务(例如用户服务、订单服务、商品服务),单业务自定义过滤时

自定义Gateway Filter 案例

  • CustomGatewayFilter(输出请求响应时长)
public class CustomGatewayFilter implements GatewayFilter, Ordered {

    private static final Log log = LogFactory.getLog(GatewayFilter.class);

    private static final String COUNT_Start_TIME = "countStartTime";

    /**
     * 对路由转发的耗时进行统计
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(COUNT_Start_TIME, System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    Long startTime = exchange.getAttribute(COUNT_Start_TIME);
                    Long endTime=(System.currentTimeMillis() - startTime);
                    if (startTime != null) {
                        log.info(exchange.getRequest().getURI().getRawPath() + ": " + endTime + "ms");
                    }
                })
        );
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}
  • GatewayServerApplication.java(Gateway Filter配置到路由断言)
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(r -> r.path("/test")
                    .filters(f -> f.filter(new CustomGatewayFilter()))
                    .uri("http://localhost:8001/customFilter?name=xujin")
                    .order(0)
                    .id("custom_filter")
            )
            .build();
}
  • 启动,可以查看网关对请求的处理及耗时统计情况。
    在这里插入图片描述
    控制台显示:

2019-01-25 16:01:17.274  INFO 32023 --- [ctor-http-nio-5] o.s.cloud.gateway.filter.GatewayFilter   : /test: 5633ms

自定义Global Filter 案例

定义一个全局过滤器,对请求到网关的URL进行权限校验,对请求到网关的URL进行权限校验,判断请求的URL是否是合法请求。例子 AuthSignatureFilter.java 中全局过滤器处理逻辑是通过从Gateway上下文ServerWebExchange对象中获取authToken对应的值进行判Null处理,实际投产时要按实际生产环境逻辑编写。

  • AuthSignatureFilter.java
@Component
public class AuthSignatureFilter implements GlobalFilter, Ordered {

    /**
     * 拦截请求,获取authToken,并校验
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("authToken");
        if (null==token  || token.isEmpty()) {
            //当请求不携带Token或者token为空时,直接设置请求状态码为401,返回
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -400;
    }
}
  • 启动应用进行测试
    分别启动 filter 和 filter-provider,当请求没有token时,过滤器会输出规定错误401
    在这里插入图片描述

项目介绍

工程 端口 描述
cloud-gty-gateway-global-filter 父包
filter 9000 过滤器程序,代码在package com.sc.gty.gg.filter
filter-provider 8001 路由断言成功匹配后跳转的项目

Spring Cloud Gateway 实战

Spring Cloud Gateway 权重路由

WeightRoutePredicateFactory 是一个路由断言工厂,下面通过使用WeightRoutePredicateFactory对URL进行权重路由。

权重路由场景

权重最常见的情况就如下图所示,在做金丝雀灰度测试时,旧系统接收95%的流量,新系统接收5%的流量,需要通过网关动态实时推送路由权重信息。
在这里插入图片描述

权重路由场景

权重路由实例

工程 端口 描述
cloud-gty-wcmp 父包 权重多路径路由
gateway 8080 配置权重路由的应用程序
provider 8081 路由到的程序,为了演示只有一个,可以设置多端口或多应用体现效果
  • gateway/…/…Application.java
@SpringBootApplication
public class WcmpGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(WcmpGatewayApplication.class, args);
    }
}
  • gateway/…/application.yml
server:
  port: 8080
spring:
  application:
    name: gty_wcmp
  cloud:
    gateway:
      routes:
      - id: service1_v1
        # 这里可以配置为其它要路由的地址
        uri: http://localhost:8081/v1
        predicates:
        - Path=/test
        # 设置权重 为95%
        - Weight=service1, 95
      - id: service1_v2
        # 这里可以配置为其它要路由的地址
        uri: http://localhost:8081/v2
        predicates:
        - Path=/test
        # 设置权重 为5%
        - Weight=service1, 5

logging:
  level:
    org.springframework.cloud.gateway: TRACE
    org.springframework.http.server.reactive: DEBUG
    org.springframework.web.reactive: DEBUG
    reactor.ipc.netty: DEBUG
  • provider/…controller/…Controller.java
@RestController
public class ServiceController {

    @GetMapping(value = "/v1")
    public String v1() {
        return "v1";
    }

    @GetMapping(value = "/v2")
    public String v2() {
        return "v2";
    }
}
  • 启动
    启动gateway/provider访问http://localhost:8080/test,多次请求查看权重已否生效。

Spring Cloud Gateway 中 Https使用技巧

Spring Cloud Gateway还可以将https证书配置在网关中,以前我们都是配置在Nginx中,如果我们要用网关来作为所有API和程序的入口的话,便可使用这种技术来实现此目的

工程 端口 描述
cloud-gty-https 父包 gateway https协议实例
cloud-gty-eureka 8761 注册中心
cloud-gty-gateway-https 8080 含https证书的网关,需要用https访问。
cloud-gty-controller-sourece 8071 目标客户端程序
cloud-gty-controller-ectype 8072 source的副本,用于负载均衡

生成ssh证书

Java keytool生成https证书

cloud-gty-gateway-https

  • pom.xml
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>

<dependencies>
    <!-- spring cloud gateway的核心依赖starter-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>

    <!--Eureka Client Starter用于将gateway注册到Eureka上-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>4.3.17.RELEASE</version>
    </dependency>

</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
  • cloud-gty-gateway-https/…/application.yml
spring:
  application:
    name: gateway-https
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
server:
  ssl:
    # 在生成证书时键入的别名
    key-alias: certificatekey
    enabled: true
    # 证书密码,我设置的相同的
    key-password: 12979613
    # 证书,与application.yml统计目录下
    key-store: classpath:shfqkeystore.jks
    key-store-type: JKS
    key-store-provider: SUN
    # 证书存储密码,我设置的相同的
    key-store-password: 12979613
  port: 8080

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/


logging:
  level:
    org.springframework.cloud.gateway: TRACE
    org.springframework.http.server.reactive: DEBUG
    org.springframework.web.reactive: DEBUG
    reactor.ipc.netty: DEBUG

【请注意阅读yml的注释】

cloud-gty-eureka

常规Eureka-Server,将Gateway与Controller注册到Eureka,这样通过Gateway端口即可访问到Controller,详细请查看代码。

cloud-gty-controller-source / cloud-gty-controller-ectype

这两个controller是除端口不同以外,其它都相同,都注册到Eureka-Server当中。

问题及解决方案

  • 访问问题
    按顺序启动,并访问页面会出现无法访问Gateway(网关)对应的Controller,网关是Https,可路由到的Controller并不是https,https转发调用http了,所以会出现此问题,详细代码我就不粘贴来,可以把源码中的两种解决方案Filter,Gateway Filter删除,再访问即可体现
    • 源码分析
      通过断点跟踪,查看LoadBalancerClientFilter # filter()函数,发现Spring Cloud Gateway进来的请求是Https,它就用Https封装,如果是Http就用Http,并没有封装直接修改的方法,只能通过自定义Gateway Filter方式对其修改。
 URI uri = exchange.getRequest().getURI();
 String overrideScheme = null;
 if (schemePrefix != null) {
 	 //这里获取访问策略
     overrideScheme = url.getScheme();
 }

 URI requestUrl = this.loadBalancer.reconstructURI(new LoadBalancerClientFilter.DelegatingServiceInstance(instance, overrideScheme), uri);
 //直接使用策略
 log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
  • (解决方案1)cloud-gty-gateway-https/…/filter/HttpSchemeFilter.java
/**
 * 在LoadBalancerClientFilter执行之后将Https修改为http
 */
@Component
public class HttpSchemeFilter implements GlobalFilter, Ordered {

    private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10101;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Object uriObj = exchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR);
        if (uriObj != null) {
            URI uri = (URI) uriObj;
            uri = this.upgradeConnection(uri, "http");
            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
        }
        return chain.filter(exchange);
    }

    private URI upgradeConnection(URI uri, String scheme) {
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri).scheme(scheme);
        if (uri.getRawQuery() != null) {
            // When building the URI, UriComponentsBuilder verify the allowed characters and does not
            // support the '+' so we replace it for its equivalent '%20'.
            // See issue https://jira.spring.io/browse/SPR-10172
            uriComponentsBuilder.replaceQuery(uri.getRawQuery().replace("+", "%20"));
        }
        return uriComponentsBuilder.build(true).toUri();
    }

    /**
     * 由于LoadBalancerClientFilter的order是10100,
     * 所以设置HttpSchemeFilter的的order是10101,
     * 在LoadBalancerClientFilter之后将https修改为http
     * @return
     */
    @Override
    public int getOrder() {
        return HTTPS_TO_HTTP_FILTER_ORDER;
    }
}
  • (解决方案2)cloud-gty-gateway-https/…/filter/HttpsToHttpFilter.java
/**
 * 在LoadBalancerClientFilter执行之前将Https修改为Http
 * https://github.com/spring-cloud/spring-cloud-gateway/issues/378
 */
@Component
public class HttpsToHttpFilter implements GlobalFilter, Ordered {

    private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI originalUri = exchange.getRequest().getURI();
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();
        String forwardedUri = request.getURI().toString();
        if (forwardedUri != null && forwardedUri.startsWith("https")) {
            try {
                URI mutatedUri = new URI("http",
                        originalUri.getUserInfo(),
                        originalUri.getHost(),
                        originalUri.getPort(),
                        originalUri.getPath(),
                        originalUri.getQuery(),
                        originalUri.getFragment());
                mutate.uri(mutatedUri);
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
        ServerHttpRequest build = mutate.build();
        return chain.filter(exchange.mutate().request(build).build());
    }

    /**
     * 由于LoadBalancerClientFilter的order是10100,
     * 要在LoadBalancerClientFilter执行之前将Https修改为Http,需要设置
     * order为10099
     * @return
     */
    @Override
    public int getOrder() {
        return HTTPS_TO_HTTP_FILTER_ORDER;
    }
}

Spring Cloud Gateway 集成 Swagger

Swagger可视化API测试工具。
我用paw和postman,所以这里就先空着,未来补全。

Spring Cloud Gateway 限流

名称 描述
缓存 提升系统访问速度和增大系统处理容量,解决高并发流量所带来的缺陷。
降级 当服务出现问题或者影响到核心流程时,需要暂时将其屏蔽掉,待高峰过去之后或者问题解决后再打。
限流 应用在不便于使用缓存和降级技术时,如秒杀抢购评论下单频繁复杂查询等稀缺资源时
  • 目的
    对高频请求、并发请求进行限速或者对一个时间窗口内对请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或友好页)、排队或等待(如秒杀、评论、下单等场景)、降级(返回兜底数据默认数据
    常见的限流场景如天猫双11,双12高并发场景
    【限流学习】 CSN-服务限流
  • 兜底容错实践设计
    在这里插入图片描述

自定义过滤器实现限流

Spring Cloud Gateway实现限流只需实现一个过滤器Filter即可。
【Google Guava】中的RateLimiter、Bucket4j、RateLimitJ都是基于令牌桶算法实现的限流工具。

项目介绍
工程 端口 描述
cloud-gty-limiting gateway 限流实例
cloud-gty-custom-filter-limiting 自定义过滤器实现限流(令牌桶)
custom-filter 8080 自定义过滤器实现限流实现
cloud-gty-redis-lua-limiting 通过内置过滤器实现限流(令牌桶)
redis-lua-request-rate 8081 内置过滤器(redis + lua),通过yml配置
cloud-gty-cpu-limiting 判断CPU使用情况,限流
cpu-limiting 8082 通过spring-boot-starter-actuator获取CPU信息,并设置CPU最大阀值,做到限流
limiting-provider 8000 用于过滤路由断言转发节点(公用provider

cloud-gty-custom-filter-limiting

  • pom.xml
<dependencies>
    <!-- Spring Cloud Gateway的依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- Bucket4j限流依赖-->
    <dependency>
        <groupId>com.github.vladimir-bukhtoyarov</groupId>
        <artifactId>bucket4j-core</artifactId>
        <version>4.0.0</version>
    </dependency>
</dependencies>
  • Application.java
......
/**
 * 通过流式API配置路由规则,当访问/test/rateLimit时,自动转发到http://localhost:8000/hello/rateLimit
 * @param builder
 * @return
 */
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(r -> r.path("/test/rateLimit")
                    .filters(f -> f.filter(new GatewayRateLimitFilterByIp(10,1, Duration.ofSeconds(1))))
                    .uri("http://localhost:8000/hello/rateLimit")
                    .id("rateLimit_route")
            ).build();
}
......
  • GatewayRateLimitFilterByIp核心过滤器,实现接口GatewayFilter, Ordered,通过过滤器的方式实现令牌桶限流
public class GatewayRateLimitFilterByIp implements GatewayFilter, Ordered {

    private final Logger log = LoggerFactory.getLogger(GatewayRateLimitFilterByIp.class);

    /**
     * 单机网关限流用一个ConcurrentHashMap来存储 bucket,
     * 如果是分布式集群限流的话,可以采用 Redis等分布式解决方案
     */
    private static final Map<String, Bucket> LOCAL_CACHE = new ConcurrentHashMap<>();

    /**
     * 桶的最大容量,即能装载 Token 的最大数量
     */
    int capacity;

    /**
     * 每次 Token 补充量
     */
    int refillTokens;

    /**
     *补充 Token 的时间间隔
     */
    Duration refillDuration;

    public GatewayRateLimitFilterByIp() {
    }

    public GatewayRateLimitFilterByIp(int capacity, int refillTokens, Duration refillDuration) {
        this.capacity = capacity;
        this.refillTokens = refillTokens;
        this.refillDuration = refillDuration;
    }

    private Bucket createNewBucket() {
        Refill refill = Refill.of(refillTokens, refillDuration);
        Bandwidth limit = Bandwidth.classic(capacity, refill);
        return Bucket4j.builder().addLimit(limit).build();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        Bucket bucket = LOCAL_CACHE.computeIfAbsent(ip, k -> createNewBucket());
        System.out.println("IP:{} ,令牌桶可用的Token数量:{} " + "ip -" +ip +
                "tokens - " + bucket.getAvailableTokens());
        if (bucket.tryConsume(1)) {
            return chain.filter(exchange);
        } else {
            //当可用的令牌书为0是,进行限流返回429状态码
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }
    }

    @Override
    public int getOrder() {
        return -1000;
    }

    public static Map<String, Bucket> getLocalCache() {
        return LOCAL_CACHE;
    }

    public int getCapacity() {
        return capacity;
    }

    public void setCapacity(int capacity) {
        this.capacity = capacity;
    }

    public int getRefillTokens() {
        return refillTokens;
    }

    public void setRefillTokens(int refillTokens) {
        this.refillTokens = refillTokens;
    }

    public Duration getRefillDuration() {
        return refillDuration;
    }

    public void setRefillDuration(Duration refillDuration) {
        this.refillDuration = refillDuration;
    }
}

访问

(Jmeter请求20次的结果)

在这里插入图片描述

(控制台输出20次的结果)
  • 结论
    在Filter启动类中设置桶的最大容量为10,当超过这个数并且之前的请求未释放时,无法请求,并返回给客户端429错误。

Gateway内置过滤工厂限流

基于RequestRateLimiterGatewayFilterFactory做到限流,要结合Redis,运用Lua脚本。

基于CPU的使用率进行限流

当项目实际投产时,利用网关限流要考虑多方面情况,例如通过CPU使用率来进行限流,Spring Boot Actuator提供的Metrics获取当前CPU的使用情况,当CPU使用率高于某个阀值就开启限流,反之不开启。

Spring Cloud Gateway 实战配置

Gateway路由到API接口程序

  • 配置如下
spring:
  application:
    name: case-gateway-server
  cloud:
    gateway:
      # 设置路由:所有的请求都会路由到http://127.0.0.1:8081/**
      routes:
      - id: cloud-client-app
        uri: http://127.0.0.1:8081
        order: 0
        predicates:
        - Path=/**

所有的请求都会路由到8081端口到API应用上

Spring Cloud Gateway 动态路由

网关重要的两个概念

概念 描述
路由配置 配置某请求路径路由到指定的目的地址。
路由规则 匹配到路由配置之后,再根据路由规则进行转发处理。

动态路由的实现

猜你喜欢

转载自blog.csdn.net/Cy_LightBule/article/details/86622780