如果你也在做SpringCloudGateway网关开发,希望这篇文章能给你带来一些启发
背景
先说背景,某油项目,通过SpringCloudGateway
配置了1.6万个路由规则,实际接口调用过程中,会偶现部分接口从发起请求到业务应用处理间隔了大概5秒
的时间,经排查后发现是SpringCloudGateway
底层在查找对应的Route
时采用了遍历+断言匹配的方式,路由规则太多时就会出现耗时太久的问题,对应的源码如下:
protected Mono lookupRoute(ServerWebExchange exchange) {
return this.routeLocator
.getRoutes()
//individually filter routes so that filterWhen error delaying is not a problem
.concatMap(route -> Mono
.just(route)
.filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
//instead of immediately stopping main flux due to error, log and swallow it
.doOnError(e -> logger.error("Error applying predicate for route: "+route.getId(), e))
.onErrorResume(e -> Mono.empty())
)
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.empty().log("noroute"))
.next()
//TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
/* TODO: trace logging
if (logger.isTraceEnabled()) {
logger.trace("RouteDefinition did not match: " + routeDefinition.getId());
}*/
}
目标
找到了问题,就需要对这块路由查找的代码进行优化,我们分析了下路由规则,发现可以用请求方法Method+请求路径Path作为key
,把对应的Route作为缓存值
,通过ServerWebExchange
直接命中对应的路由对象(这里要注意下,如果有同样的问题,需要根据实际情况设计缓存键,比如/person/**
这种断言Path就不适用了),对应的路由规则如下:
{
"predicates": [
{
"args": {
"_genkey_0": "/v1/structuredData/serviceData/cestc_dportal/MH_GX_JS_SJCZQX3080"
},
"name": "Path"
},
{
"args": {
"_genkey_0": "GET"
},
"name": "Method"
}
],
"filters": [
{
"args": {
"_genkey_1": "/myapi/v1.0/zhyApi/getDataForGet",
"_genkey_0": "/v1/structuredData/serviceData/cestc_dportal/MH_GX_JS_SJCZQX3080"
},
"name": "RewritePath"
}
],
"id": "02024012311262643900000101579677",
"uri": "lb://myapi",
"order": 0
}
定义路由缓存策略
接口定义
/**
* 路由断言缓存实现
* 通过ServerWebExchange快速查找Route
* @author changxy
*/
public interface RoutePredicateCacheable {
/**
* 更新缓存路由
* @param routeDefinition
*/
void update(List routeDefinition);
/**
* 根据请求上下文匹配对应路由
* @param exchange
* @return
*/
Optional getRoute(ServerWebExchange exchange);
static RoutePredicateCacheable empty() {
return new BlankRoutePredicateCacheable();
}
}
使用本地内存存放路由缓存
/**
* 本地内存Route对象缓存器
* RouteDefinitionRouteLocator类中处理RouteDefinition到Route的转换
* @author changxy
*/
public class InMemoryRoutePredicateCacheable implements RoutePredicateCacheable {
private final RouteDefinitionRouteLocator routeLocator;
private final Map routes = new ConcurrentHashMap(1024);
protected final static String CACHE_KEY_FORMAT = "%s:%s";
public InMemoryRoutePredicateCacheable(RouteDefinitionRouteLocator routeLocator) {
this.routeLocator = routeLocator;
}
@Override
public void update(List routeDefinitions) {
if (CollectionUtils.isEmpty(routeDefinitions)) {
return ;
}
// 清空缓存
routes.clear();
Map routeMap = this.routeLocator
.getRoutes()
.toStream()
.collect(Collectors.toMap(Route::getId, r -> r));
for (RouteDefinition routeDefinition : routeDefinitions) {
routes.put(key(routeDefinition), routeMap.get(routeDefinition.getId()));
}
System.out.println(1);
}
@Override
public Optional getRoute(ServerWebExchange exchange) {
return Optional.ofNullable(routes.get(key(exchange)));
}
public Optional lookupRoute(String routeId) {
return this.routeLocator
.getRoutes()
.toStream()
.filter(route -> Objects.equals(route.getId(), routeId))
.findFirst();
}
/**
* 根据路由定义生成key
* @param routeDefinition
* @return
*/
protected String key(RouteDefinition routeDefinition) {
Map routeDefinitionParams = routeDefinition.getPredicates()
.stream()
.collect(
Collectors.toMap(
PredicateDefinition::getName,
p -> p.getArgs().get("_genkey_0"),
(k1, k2) -> k2
)
);
if (null != routeDefinitionParams
&& routeDefinitionParams.containsKey("Method")
&& routeDefinitionParams.containsKey("Path")) {
return String.format(CACHE_KEY_FORMAT, routeDefinitionParams.get("Method"), routeDefinitionParams.get("Path"));
}
return StringUtils.EMPTY;
}
/**
* 根据请求对象生成key
* @param exchange
* @return
*/
protected String key(ServerWebExchange exchange) {
String method = exchange.getRequest().getMethodValue();
String paths = exchange.getRequest().getPath().value();
return String.format(CACHE_KEY_FORMAT, method, paths);
}
}
我们的路由规则存放在Nacos配置中心,网关服务启动时、Nacos配置发生变更时,同步刷新路由缓存,这块可以根据实际情况定义缓存更新策略,部分伪代码如下:
List routeDefinitions = list.stream().map(DynamicRoutingConfig.this::assembleRouteDefinition).collect(Collectors.toList());
// 20240124 更新Route缓存,优化路由匹配速度
routePredicateCacheable.update(routeDefinitions);
重写RoutePredicateHandlerMapping
public class CachingRoutePredicateHandlerMapping extends RoutePredicateHandlerMapping {
private final RoutePredicateCacheable routePredicateCacheable;
public CachingRoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment, RoutePredicateCacheable routePredicateCacheable) {
super(webHandler, routeLocator, globalCorsProperties, environment);
this.routePredicateCacheable = routePredicateCacheable;
}
@Override
protected Mono lookupRoute(ServerWebExchange exchange) {
Optional route = routePredicateCacheable.getRoute(exchange);
if (route.isPresent()) {
return Mono.just(route.get());
} else {
return super.lookupRoute(exchange);
}
}
}
定义AutoConfiguration
@Configuration
@ConditionalOnProperty(name = "route.cache.enabled", matchIfMissing = false)
@AutoConfigureBefore(GatewayAutoCon服务器托管网figuration.class)
public class FastRoutePredicateHandlerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RoutePredicateCacheable routePredicateCacheable(RouteDefinitionRouteLocator routeLocator) {
return new InMemoryRoutePredicateCacheable(routeLocator);
}
@Bean("cachingRoutePredicateHandlerMapping")
public RoutePredicateHandlerMapping routePredicateHandlerMapping(
FilteringWebHandler webHandler, RouteLocator routeLocator,
GlobalCorsProperties globalCorsProperties, Environment environment, RoutePredicateCacheable routePredicateCacheable) {
return new CachingRoutePredicateHandlerMapping(webHandler, routeLocator,
globalCorsProperties, environment, routePredicateCacheable);
}
}
不加载SpringCloudGateway自己的RoutePredicateHandlerMapping
@Configuration
@ConditionalOnProperty(name = "route.cache.enabled", matchIfMissing = false)
public class RoutePredicateBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
re服务器托管网gistry.removeBeanDefinition("routePredicateHandlerMapping");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
优化效果测试
毫秒级响应了
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net