• 重写SpringCloudGateway路由查找算法,性能提升100倍!


    只发博客园,严谨转载,盗版必究

    如果你也在做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()));
            }
    
        }
    
        @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(GatewayAutoConfiguration.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 {
            registry.removeBeanDefinition("routePredicateHandlerMapping");
        }
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    
        }
    }

    优化效果测试

    毫秒级响应了

  • 相关阅读:
    聊一下C#中的lock
    SpringBoot应用程序部署生产环境需要设置哪些参数?
    JFrame中有关于DefaultCloseOperation的使用及参数说明(含源码阅读)
    @CacheInvalidate(name = “xxx“, key = “#results.![a+b]“,multi = true)是什么意思
    MFC Windows 程序设计[224]之双列表拾取器(附源码)
    华为、苹果竞争未完,平板赛道再成舞台
    基于Python通信程序的设计与实现
    CockroachDB-备份与恢复(4)管理备份计划
    fundamental notes in 3D math
    (项目笔记)opencv人脸识别
  • 原文地址:https://www.cnblogs.com/changxy-codest/p/17985365