解决Spring Gateway配置单个路由超时时间不生效的问题

之前springcloud gateway项目是的路由配置都是静态配置在项目的application.yml文件中,不能实现路由的热更新。前期业务发展也比较缓慢,新增路由的场景频率不是很高,最近业务越来越广,新增项目频率明显升高,所以想着把路由配置提到apollo中,本想着很简单的一个事情,结果谁知坑一个比一个深,接下来讲一讲都有啥坑,又该如何填这些坑。

如何接入apollo

springcloud gateway支持通过propertiesyml进行配置,下文简称gateway。

properties示例

spring.cloud.gateway.routes[0].id = geektao
spring.cloud.gateway.routes[0].uri = http://www.geektao.ai
spring.cloud.gateway.routes[0].predicates[0] = Path=/geektao/**
spring.cloud.gateway.routes[0].filters[0] = StripPrefix=1

yml示例

spring:
  cloud:
    gateway:
      routes:
      - id: geektao
        uri: http://www.geektao.ai
        predicates:
        - Path=/geektao/**
        filters:
        - StripPrefix=1

使用properties进行配置的话,为了遵循springboot项目的规范需要指定routes的索引,还得有序递增,太麻烦,这时候yml格式就很香,所以建议使用yml格式进行配置。

首先,创建namesapce
在这里插入图片描述

apollo默认的namespaceapplicationproperties格式的,不符合我们的使用场景,所以新建一个yml格式的namespace
在这里插入图片描述

gateway项目引入apollo客户端依赖。

<dependency>
  <groupId>com.ctrip.framework.apollo</groupId>
  <artifactId>apollo-client</artifactId>
  <version>${apollo.version}</version>
</dependency>

在gateway项目resources目录下新建META_INF目录,创建app.properties配置文件,这里有个在apollo中,properties格式的namespace在引入时只需要用名称就可以,但是其它格式的namespace在引入时需要按照「名称.格式」的规则来,否则无法正常加载

# apollo
apollo.bootstrap.enabled=true
apollo.meta=http://xxxx
apollo.accesskey.secret=xxxx
apollo.bootstrap.namespaces=application,base-yml.yml
app.id=xxxx

开启apollo注册。这里也需要指定value的值,不指定默认只加载application。

@EnableApolloConfig(value = {"application","base-yml.yml"})

启动项目,访问被代理服务的地址,成功响应表示完成。

实现动态路由配置

完成上述步骤,路由配置还是静态的,我们修改完apollo后,除非再次从apollo获取配置文件,这时候才能获取到最新的值,但是无法同步更新运行中的springboot项目中的上下文配置数据,所以我们需要监听apollo中配置的变化,然后手动更新springboot和getaway的配置数据。

核心代码贴出来了,这里在配置@ApolloConfigChangeListener(value = ROUTE_NAMESPACE)记着也得使用base-yml.yml的namspace,不然无法正常监听。

@Configuration
@Slf4j
public class RuteDynamicRefresher implements ApplicationContextAware, ApplicationEventPublisherAware {

    private static final String ROUTE_NAMESPACE = "base-yml.yml";

    private ApplicationContext applicationContext;

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    @ApolloConfigChangeListener(value = ROUTE_NAMESPACE)
    public void routeChange(ConfigChangeEvent changeEvent) {
        refreshGatewayProperties(changeEvent);
    }

    private void refreshGatewayProperties(ConfigChangeEvent changeEvent) {
        try {
            // 打印变更日志
            log.info("[RuteDynamicRefresher] update {}", JSONObject.toJSONString(changeEvent));

            // 更新 项目环境配置
            EnvironmentChangeEvent environmentChangeEvent = new EnvironmentChangeEvent(changeEvent.changedKeys());
            this.applicationContext.publishEvent(environmentChangeEvent);
            log.info("[RuteDynamicRefresher] publish environmentChangeEvent {}", environmentChangeEvent);

            // 更新路由配置
            RefreshRoutesEvent refreshRoutesEvent = new RefreshRoutesEvent(this);
            log.info("[RuteDynamicRefresher] publish refreshRoutesEvent {}", refreshRoutesEvent);
            this.publisher.publishEvent(refreshRoutesEvent);
        } catch (Exception e) {
            log.error("[refreshGatewayProperties] 更新失败!", e);
        }
    }
}

这时候可以尝试新增、删除、修改路由的配置,就可以正常使用了。

还有坑,上边我们只是配置了必要的路由配置,实际生产使用过程中,不同的服务之间接口响应的时长阈值也是不一样的,耗时长的可能需要好几秒(当然这种建议改成异步或轮训的方式),短的可能几十毫秒,那就需要为每个路由单独设置超时时间。

gateway支持配置全局超时时间,所有路由都生效。

# 全局的响应超时时间,网络链接后,后端服务多久不返回网关就报错
#response-timeout: PT5S
# 全局的TCP连接超时时间,多长时间获取不到超时就报错
#connect-timeout: 5000

还支持配置单个路由超时时间,单位时间毫秒。

spring:
  cloud:
    gateway:
      routes:
      - id: geektao
        uri: http://www.geektao.ai
        predicates:
        - Path=/geektao/**
        filters:
        - StripPrefix=1
        metadata:
          response-timeout: 2000
          connect-timeout: 2000

但是在apollo中配置之后进行测试,发现未生效。走的还是全局的超时时间5秒。这里我直接说原因了,大家下来可以自己debug看一看。

问题就出在org.springframework.cloud.gateway.filter.NettyRoutingFilter这个类的getResponseTimeout(Route route)方法。

private Duration getResponseTimeout(Route route) {
		Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
		if (responseTimeoutAttr != null && responseTimeoutAttr instanceof Number) {
			Long routeResponseTimeout = ((Number) responseTimeoutAttr).longValue();
			if (routeResponseTimeout >= 0) {
				return Duration.ofMillis(routeResponseTimeout);
			}
			else {
				return null;
			}
		}
		return properties.getResponseTimeout();
	}

其实直接使用项目中的yml配置文件是没有问题的,因为yml配置文件中的value值是有类型的,Integer就是Integer,String就是String。所以在项目中配置的yml文件中读取到的response-timeout值就是Number类型,这个方法就可以正常返回值。

但是在apollo配置中,底层使用的是Properties进行配置数据的存储,value都被转成了String类型,那走到if (responseTimeoutAttr != null && responseTimeoutAttr instanceof Number)时必然就是false了,那就返回了全局的超时时间。

那既然知道了这里有问题,我搜了搜从apollo下手有些困难,所以我们从gateway下手,因为这个类不是jvm中原生类,那就意味着是通过应用类加载器加载的,那我们可以试着在自己项目中创建org.springframework.cloud.gateway.filter.NettyRoutingFilter类,然后重写getResponseTimeout方法,当然我这里直接暴力改了,默认这个配置只会配置为Long类型,大家实际用时最好加上异常处理。

private Duration getResponseTimeout(Route route) {
        Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
        Long routeResponseTimeout;
        if (responseTimeoutAttr != null) {
            Long routeResponseTimeout = Long.parseLong((String)responseTimeoutAttr)

            if (routeResponseTimeout >= 0) {
                return Duration.ofMillis(routeResponseTimeout);
            } else {
                return null;
            }
        }
        return properties.getResponseTimeout();
    }

再次测试单路由的超时配置,问题解决。

关注公众号「极客涛」,同步更新文章。

  • 22
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值