springcloud 通过gateway路由转发调用接口很慢问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springcloud 通过gateway路由转发调用接口很慢问题相关的知识,希望对你有一定的参考价值。

参考技术A 框架采用springcloud ,模块分为网关gateway,认证auth和其它子系统模块,部署后全部都注册到nacos中心,从nacos读取配置信息,前端页面部署在nginx,通过访问网关gateway地址,统一路由转发到其他子模块调取接口,发现有时候接口访问返回很慢,有时候也蛮快,通过zipkin链路追踪发现耗时主要在子模块的调用返回上,但是子模块也没有复杂逻辑,不知道是框架问题还是网络问题,服务器防火墙已将所有模块的端口添加上,有没有大佬知道原因或遇到过?

spingcloud gateway

一。简介

springcloud是第二代网关,取代zuul网关。具有强大的智能路由,过滤器功能。常见的功能有路由转发、权限校验、限流控制等作用

Spring Cloud Gateway 具有如下特性:

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成Hystrix的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

相关概念

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
  • Predicate(断言):匹配请求中的信息,与断言匹配成功则进行路由。
  • Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

二。代码

pom.xml添加依赖

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

有两种配置路由的方式:一种是在yml文件来配置,另一种是通过java bean来配置。下面例子大部分用yml

java bean配置

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

    /**
     * java配置路由会覆盖application.yml中的配置
     * @param builder
     * @return
     */
   @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder){
        String httpUri = "http://httpbin.org:80";
        return builder.routes()  //route来做路由
                .route("rout1",p -> p
                  .path("/get") // 对/get请求做处理
                  .filters(f->f.addRequestHeader("hello", "yy")) //过滤器增加header
                  .uri(httpUri)) //转发到这个请求上
                .build();
    }
}

可以看到请求/get 的header添加了参数hello=yy

技术图片

1.predicate实战

After Route Predicate Factory

可配置在指定时间后的请求,都交给router处理。否则不通过路由

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: http://httpbin.org:80/get #前面的路径替换为该值即http://xxx:8081这段
          filters: #增加请求header的过滤器
            - AddRequestHeader=X-Request-Foo, Bar
          predicates:
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: add_request_header_route

启动工程,在浏览器上访问http://localhost:8081/,会显示http://httpbin.org:80/get返回的结果,此时gateway路由到了配置的uri。

Header Route Predicate Factory

Header Route Predicate Factory需要两个参数,header名称和值,当断言匹配了请求中的header名称和值后,断言通过,就能进入路由规则

spring:
  profiles:
    active: header_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: header_route
          uri: https://www.baidu.com
          predicates:
            - Header=X-Request-Id, d+ #当请求的Header中有X-Request-Id的header名,且header值为数字时
  profiles: header_route

测试:

curl -H "X-Request-Id:1" localhost:8081

返回页面请求就通过了,404则没有通过

spring:
  profiles:
    active: cookie_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: cookie_route
          uri: http://httpbin.org:80/get
          predicates:
            - Cookie=name, yy
  profiles: cookie_route

请求带有cookie名称为name,值为yy就能通过,被转发到http://httpbin.org:80/get

测试

curl -H "Cookie:name=forezp" localhost:8081

Host Route Predicate Factory

需要以一个参数即hostname,会匹配请求头中的host值,匹配则转发

spring:
  profiles:
    active: host_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: host_route
          uri: http://httpbin.org:80/get
          predicates:
            - Host=**.yy.com
  profiles: host_route

测试:curl -H "Host:www.fangzhipeng.com" localhost:8081

Method Route Predicate Factory

需要一个参数请求类型,GET

spring:
  profiles:
    active: method_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: method_route
          uri: http://httpbin.org:80/get
          predicates:
            - Method=GET
  profiles: method_route

测试:curl localhost:8081

模拟POST请求:curl -XPOST localhost:8081

Path Route Predicate Factory

需要一个参数匹配路径

spring:
  profiles:
    active: path_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://localhost:9600/
          predicates: 
            - Path=/hello
  profiles: path_route

Query Route Predicate Factory

需要两个参数,参数名和参数值,也可以只有一个参数即参数名

spring:
  profiles:
    active: query_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: query_route
          uri: http://httpbin.org:80/get
          predicates:
            - Query=foo, ba.
  profiles: query_route

测试:curl localhost:8081?foo=bar

Predict作为断言,它决定了请求会被路由到哪个router 中。在断言之后,请求会被进入到filter过滤器的逻辑

2.filter实战

 确定哪个路由执行后,在路由处理之前,先经过”pre“类型的过滤器,路由处理后,经过”post“类型的过滤器

”pre“类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等

”post“类型的过滤器可以做响应内容,响应头修改,日志输出,流量监控等。

springcloud包含许多内置的GatewayFilter工厂,和predicate一样配置在application.yml,遵循了约定大于配置的原则,只需要配置filter工厂的名称,不需要配置全类名。

比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名

AddRequestHeader GatewayFilter Factory

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route #指定配置文件
---
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar #AddRequestHeaderGatewayFilterFactory
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver] #AfterPredictFactory
  profiles: add_request_header_route

模拟请求:curl localhost:8081

最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host="localhost:8081";for="0:0:0:0:0:0:0:1:56248"",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.58.0",
    "X-Forwarded-Host": "localhost:8081",
    "X-Request-Foo": "Bar"
  },
  "origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
  "url": "http://localhost:8081/get"
}

从响应知道,请求头中加入了 "X-Request-Foo": "Bar"

RewritePath GatewayFilter Factory

重写路径的功能,类似nginx的功能

spring:
  profiles:
    active: rewritepath_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: rewritepath_route
          uri: http://localhost:9600/
          predicates:
            - Path=/foo/**
          filters: #将/foo/(?.*)重写为{segment},然后转发
            - RewritePath=/foo/(?<segment>.*), /${segment}
  profiles: rewritepath_route

比如在网页上请求localhost:8081/foo/forezp,此时会将请求转发到https://blog.csdn.net/forezp的页面

Hystrix GatewayFilter

允许将断路器功能添加到网关中,使服务免受级联故障??这个如何实现??, 提供服务降级处理

pom.xml增加hystrix

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 增加服务降级处理类
  • @RestController
    public class FallbackController {
    
        @GetMapping("/fallback")
        public Object fallback() {
            Map<String,Object> result = new HashMap<>();
            result.put("data",null);
            result.put("message","Get request fallback!");
            result.put("code",500);
            return result;
        }
    }

     

  • 在application-filter.yml中添加相关配置,当路由出错时会转发到服务降级处理的控制器上:
spring:
  cloud:
    gateway:
      routes:
        - id: hystrix_route
          uri: http://localhost:9600
          predicates:
            - Method=GET
          filters:
            - name: Hystrix
              args:
                name: fallbackcmd
                fallbackUri: forward:/fallback

访问http://localhost:8081/user转发到http://localhost:9600/user, 404错误转发到服务降级处理的控制器上,返回降级处理的信息

现在yml中配置测试不行,在java bean中配置可以返回

@Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder){
        String httpUri = "http://httpbin.org:80";
        return builder.routes()  //route来做断言
                .route(p -> p
                  .host("*.hystrix.com")
                  .filters(f->f.hystrix(config -> config
                            .setName("mycmd")
                            .setFallbackUri("forward:/fallback")))
                  .uri(httpUri))
                .build();
    }

测试: curl -H "Host:www.hystrix.com" localhost:8081?token=1
返回: {"code":500,"data":null,"message":"Get request fallback!"}

自定义过滤器

 springcloud内置了19种过滤器工厂,也可以自动逸过滤器。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered2个接口

/**
 * 自定义实现过滤器
 */
public class RequestTimeFilter implements GatewayFilter, Ordered {
    private static final Log log = LogFactory.getLog(RequestTimeFilter.class);
    private static final String REQUEST_TIME_BEGIN = "reqTimeBegin";
    @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");
                    }
                }));
    }

    /**
     * 给过滤器设定优先级别的,值越大则优先级越低
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

java中配置路由,使用自定义RequestTimeFilter

 @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        // @formatter:off
        return builder.routes()
                .route(r -> r.path("/customer/**")
                        .filters(f -> f.filter(new RequestTimeFilter())
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("http://httpbin.org:80/get")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();
        // @formatter:on
    }

测试:curl localhost:8081/customer/123

控制台输出请求时间的日志:

2020-10-14 22:17:48.528  INFO 107912 --- [ctor-http-nio-8] com.yy.gateway.RequestTimeFilter         : /customer:28ms 

自定义过滤器工厂

这样就能在配置文件中配置,自定义过滤器了

public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {
    private static final Log log = LogFactory.getLog(RequestTimeGatewayFilterFactory.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
    private static final String KEY = "withParams";

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(KEY); //给config传参
    }

    public RequestTimeGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, 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){
                            StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                                    .append(":")
                                    .append(System.currentTimeMillis() - startTime)
                                    .append("ms");
                            if(config.isWithParams()){
                                sb.append("params:").append(exchange.getRequest().getQueryParams());
                            }
                            System.out.println(sb.toString());
                            log.info(sb.toString());
                        }
                    }));
        });
    }

    /**
     * 接收filter参数
     */
    public static class Config{
        private boolean withParams;

        public boolean isWithParams() {
            return withParams;
        }

        public void setWithParams(boolean withParams) {
            this.withParams = withParams;
        }
    }
}

静态内部类类Config就是为了接收参数的,里边的变量名可以随意写,但是要重写List shortcutFieldOrder()这个方法‘

注册RequestTimeGatewayFilterFactory  bean

    @Bean
    public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
        return new RequestTimeGatewayFilterFactory();
    }

application.yml 增加配置

spring:
  profiles:
    active: elapse_route
---
spring:
  cloud:
    gateway:
      routes:
        - id: elapse_route
          uri: http://httpbin.org:80/get #前面的路径替换即http://xxx:8081这段
          filters: #增加自定义Factory的filter
            - RequestTime=true
          predicates:
            - Method=GET
  profiles: elapse_route

测试:curl localhost:8081/customer/123

控制台输出请求时间的日志

global filter

全局过滤器,不用在配置文件中配置,作用在所有路由上

自定义全局路由,不包含token的请求不转发,需要实现GlobalFilter和Ordered接口

/**
 * 全局过滤器,如果不包含token参数直接返回不路由
 */
public class TokenGlobalFilter implements GlobalFilter, Ordered {
    private static final Log log = LogFactory.getLog(TokenGlobalFilter.class);
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if(token == null || token.isEmpty()){
            log.info("token is empty");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

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

注入到容器

/**
     * 将自定义全局过滤器注入到IOC容器
     * @return
     */
    @Bean
    public TokenGlobalFilter tokenGlobalFilter(){
        return new TokenGlobalFilter();
    }

测试: curl localhost:8081

控制台打印日志:2020-10-14 22:29:50.985  INFO 107912 --- [ctor-http-nio-2] com.yy.gateway.TokenGlobalFilter         : token is empty

Spring Cloud Gateway限流

RequestRateLimiterGatewayFilterFactory这个类,RequestRateLimiter 过滤器可以用于限流,使用RateLimiter实现来确定是否允许当前请求继续进行,如果请求太大默认会返回HTTP 429-太多请求状态。使用Redis和lua脚本实现了令牌桶的方式

pom.xml引入redis依赖

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

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

application.yml

---
spring:
  cloud:
    gateway:
      routes:
        - id: limit_route
          uri: http://localhost:9600/ #前面的路径替换即http://xxx:8081这段
          predicates:
            - Method=GET
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@hostAddrKeyResolver}" #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率, 即平均访问速率每秒
                redis-rate-limiter.burstCapacity: 2 #令牌桶总容量 即每秒最大访问速率
  profiles: limit_route
  redis:
    host: 192.168.31.211
    port: 6379

KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。 

/**
 * 根据Hostname进行限流,则需要用hostAddress去判断
 */
public class HostAddrKeyResolver implements KeyResolver {
    Log log = LogFactory.getLog(HostAddrKeyResolver.class);
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}
@Bean
    public HostAddrKeyResolver hostAddrKeyResolver(){
        return new HostAddrKeyResolver();
    }

多次访问http://localhost:8081,会返回429错误

可以用其他维度限流,例如用户维度

    @Bean
    KeyResolver userKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

 

三。服务的注册与发现

 上面都是用硬编码方式进行路由转发,gateway可以配合注册中心进行路由转发

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.yml

server:
  port: 8081

spring:
  application:
    name: sc-gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true
          
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/

spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,并且自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。

spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),

测试:http://localhost:8081/springcloud-eureka-client/hello 就被转发到http://localhost:9400/helllo

 

如果不想写服务名来路由,自定义请求路劲

server:
  port: 8081
spring:
  application:
    name: sc_gateway_client
  cloud:
    gateway:
      discovery:
        locator:
          enabled: false #gateway开启服务注册和发现的功能,并且自动根据服务发现为每一个服务创建了一个router
          lower-case-service-id: true #请求路径上的服务名配置为小写
      routes:
        - id: springcloud-eureka-client
          uri: lb://SPRINGCLOUD-EUREKA-CLIENT #服务名
          predicates:
            - Path=/client/**
          filters:
            - StripPrefix=1 # 在转发之前将/client去掉
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka

现在http://localhost:8081/client/hello 会被转发到http://localhost:9400/hello

 

 


以上是关于springcloud 通过gateway路由转发调用接口很慢问题的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud系列之网关gateway-4.路由功能详解

springcloud gateway概念精讲

Gateway实战:SpringCloud-Gateway组件使用

springcloud gateway 实现服务转发 2

已解决Gateway路由转发-报503 Service Unavailable

SpringCloud学习第八篇:gateway学习(Hoxton.SR4)