Spring Could Gateway基于Reactor项目构建,是一个业务网关框架
官方介绍:This project provides a libraries for building an API Gateway on top of Spring WebFlux or Spring WebMVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.
版本信息 本文的优化方案基于springcould 2021.0.9、springboot2.7.18,理论上适用于springboot2的所有版本
更多优化方案可参考SpringCloud Gateway 在微服务架构下的最佳实践
Reactor-Netty相关优化 可以使用JVM的-D命令设置或在项目启动前通过System.setProperty()实现
NIO线程数调整 框架默认的IO线程数等于机器核心数,如果业务代码中没有网络IO的阻塞调用,不需要修改。但如果存在阻塞调用 Redis、MySQL、API等场景,可以适当将线程数调大,以提升系统的性能表现
1 2 3 4 5 6 7 int processors = Runtime.getRuntime().availableProcessors();System.setProperty(ReactorNetty.IO_WORKER_COUNT, String.valueOf(processors * 2 ));
单独开一个线程作为selector 1 System.setProperty(ReactorNetty.IO_SELECT_COUNT, "1" );
连接池获取策略调整 1 2 3 4 5 System.setProperty(ReactorNetty.POOL_LEASING_STRATEGY, "lifo" );
关闭access-log 如果网关系统接入了三方的监控系统,可以考虑关闭access log
1 System.setProperty(ReactorNetty.ACCESS_LOG_ENABLED, "false" );
关闭Netty的内存泄漏检测 1 2 System.setProperty("io.netty.leakDetection.level" , "disabled" );
优先使用原生的事件订阅技术(默认开启) 1 2 System.setProperty(ReactorNetty.NATIVE, "true" );
其他 1 2 3 4 5 6 7 8 9 10 11 12 System.setProperty("io.netty.allocator.type" , "unpooled" ); System.setProperty("reactor.schedulers.defaultPoolSize" , String.valueOf(processors)); System.setProperty("reactor.schedulers.defaultBoundedElasticSize" , String.valueOf(processors * 8 )); System.setProperty("reactor.schedulers.defaultBoundedElasticQueueSize" , "4096" );
签名校验优化 API网关通常会使用MD5等算法为请求进行签名校验,Gateway本身的线程数较少且固定,可以考虑使用FastThreadLocal缓存MessageDigest实例,不需要每次重新构建,执行速度相较于每次新建实例可提升10倍左右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private static final FastThreadLocal<MessageDigest> MD5_HOLDER = new FastThreadLocal <MessageDigest>() { @Override protected MessageDigest initialValue () throws Exception { return MessageDigest.getInstance("MD5" ); } }; private static final char [] DIGITS = {'0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' };private static MessageDigest getMd5MessageDigest () { MessageDigest messageDigest = MD5_HOLDER.get(); messageDigest.reset(); return messageDigest; } public static String md5 (String string) { char [] digits = DIGITS; byte [] bytes = string.getBytes(StandardCharsets.UTF_8); bytes = getMd5MessageDigest().digest(bytes); char [] chars = new char [32 ]; for (int i = 0 , j = 0 ; i < bytes.length; i++) { chars[j++] = digits[(bytes[i] & 0xF0 ) >>> 4 ]; chars[j++] = digits[bytes[i] & 0x0F ]; } return new String (chars); }
连接池优化 如果业务中存在调用Redis、MySQL等场景,且使用了连接池技术(一般都会用到吧)
连接池的最小活跃数量可以设置为{NIO线程数} + 1,最大数量可设置为{NIO线程数} * 2作为冗余
使用GlobalFilter和自定义断言 框架默认情况下使用遍历路由的方式进行断言,时间复杂度为O(n),随着路由数量提升,性能下降较为明显
在业务场景允许的情况下(很难做到不允许吧),建议仅使用全局过滤器(GlobalFilter)替代普通的过滤器(Filter),后面会提到为什么要这么做
1 2 3 4 5 6 7 8 9 RouteDefinition routeDefinition = new RouteDefinition ();routeDefinition.setId("xxx" ); routeDefinition.setPredicates(Collections.emptyList()); routeDefinition.setFilters(Collections.emptyList());
使用MAP缓存路由信息 自定义实现RouteLocator接口,并替换自动配置中声明的名称为‘cachedCompositeRouteLocator’的bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @Slf4j @Primary @Component("cachedCompositeRouteLocator") public class EnhancedCachingRouteLocator implements Ordered , RouteLocator, ApplicationListener<RefreshRoutesEvent> { private final RouteLocator routeLocator; private final ApplicationEventPublisher applicationEventPublisher; private Map<String, Route> cache = Collections.emptyMap(); private Flux<Route> routes; public EnhancedCachingRouteLocator (RouteLocator routeLocator, ApplicationEventPublisher applicationEventPublisher) { this .routeLocator = routeLocator; this .applicationEventPublisher = applicationEventPublisher; this .routes = this .refresh(); } public Route getRoute (String routeId) { Map<String, Route> cache = this .cache; return cache.get(routeId); } @Override public int getOrder () { return Ordered.HIGHEST_PRECEDENCE; } @Override public Flux<Route> getRoutes () { return routes; } @Override public void onApplicationEvent (RefreshRoutesEvent refreshRoutesEvent) { FunctionUtil.runnable( () -> this .refresh() .collect(Collectors.toList()) .subscribe( list -> Flux.fromIterable(list) .materialize() .collect(Collectors.toList()) .subscribe( signals -> applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent (this )), this ::handleRefreshError ), this ::handleRefreshError ), this ::handleRefreshError ); } private Flux<Route> refresh () { Flux<Route> routes = routeLocator.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE); Map<String, Route> cache = new HashMap <>(128 , 1.0F ); routes.subscribe(route -> cache.put(route.getId(), route)); this .routes = routes; this .cache = Collections.unmodifiableMap(cache); return routes; } private void handleRefreshError (Throwable throwable) { log.error("Refresh routes error" , throwable); applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent (this , throwable)); } }
自定义路由断言 与上面自定义的EnhancedCachingRouteLocator相配合,可以将断言的时间复杂度从O(n)降低到O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 /** * 修改spring cloud gateway基于断言工厂的路由处理器,替换为更高效的实现。 * <p> * 这里重写RoutePredicateHandlerMapping的lookupRoute方法, * 可以将大部分情况下(带有"x-app"请求头的请求)路由匹配的时间复杂度从O(n)降低到O(1),最差情况也可以降低到O(3) * <p> * 框架自带的断言工厂,见 {@link org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory} * * @see org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute(ServerWebExchange) * @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#routePredicateHandlerMapping(FilteringWebHandler, RouteLocator, GlobalCorsProperties, Environment) */ @Component public class CustomizedRoutePredicateHandlerMapping extends RoutePredicateHandlerMapping { private final EnhancedCachingRouteLocator enhancedCachingRouteLocator; public CustomizedRoutePredicateHandlerMapping(FilteringWebHandler filteringWebHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) { super(filteringWebHandler, routeLocator, globalCorsProperties, environment); if (routeLocator instanceof EnhancedCachingRouteLocator) { this.enhancedCachingRouteLocator = (EnhancedCachingRouteLocator) routeLocator; } else { throw new IllegalArgumentException("RouteLocator type not matched to " + EnhancedCachingRouteLocator.class.getName()); } } @Override protected Mono<Route> lookupRoute(ServerWebExchange exchange) { String routeId = this.lookupRouteId(exchange.getRequest()); if (StringUtils.isEmpty(routeId)) { return Mono.empty(); } // 直接根据routeId找到对应的route Route route = enhancedCachingRouteLocator.getRoute(routeId); if (route == null) { return Mono.error(new InterruptRequestException(HttpStatus.NOT_FOUND, "路由编码错误")); } ExchangeAttribute.REQUEST_TARGET.set(exchange, route.getId()); return Mono.just(route); } /** * 分别根据请求头、查询参数、请求路径来匹配对应的系统 * * @param request request * @return routeId */ private String lookupRouteId(ServerHttpRequest request) { // Step.1 请求头指定route String routeId = request.getHeaders().getFirst("x-app"); if (StringUtils.isNotEmpty(routeId)) { return routeId; } // Step.2 查询参数指定route routeId = this.lookupRouteIdFromQuery(request); if (StringUtils.isNotEmpty(routeId)) { return routeId; } // Step.3 请求地址指定route routeId = this.lookupRouteIdFromPath(request.getPath().value()); if (StringUtils.isNotEmpty(routeId)) { return routeId; } // Step.4 兜底逻辑,没有找到路由则默认为default return "default"; } /** * "xyz=123&x-app=abc123&key=value" -> "abc123" * 由于只需要获取"x-app"查询参数即可,不需要URLDecode,出于性能考虑,不使用框架自带的 * {@link org.springframework.http.server.reactive.ServerHttpRequest#getQueryParams()} */ private String lookupRouteIdFromQuery(ServerHttpRequest request) { String query = request.getURI().getRawQuery(); if (StringUtils.isEmpty(query)) { return null; } int i = query.indexOf("x-app="); if (i < 0) { return null; } query = query.substring(i + "x-app=".length()); i = query.indexOf('&'); // 一般来说系统编码不会出现特殊符号,不需要URLDecode return i > 0 ? query.substring(0, i) : query; } /** * "/gateway/abc123/**" -> "abc123" */ private String lookupRouteIdFromPath(String requestPath) { int i = "/gateway/".length(); requestPath = requestPath.substring(i); i = requestPath.indexOf('/'); if (i < 0) { return null; } return requestPath.substring(0, i); } }
Filter排序优化 如果每个路由单独配置了Filter,框架在每次请求时,都会整合路由的Filter和GlobalFIilter并进行一次较为复杂的排序操作,性能损失较为严重
以下是框架的源代码,从源码中的TODO标签来看,官方已经意识到了这里的问题,但是目前还没有解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public Mono<Void> handle (ServerWebExchange exchange) { Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR); List<GatewayFilter> gatewayFilters = route.getFilters(); List<GatewayFilter> combined = new ArrayList <>(this .globalFilters); combined.addAll(gatewayFilters); AnnotationAwareOrderComparator.sort(combined); if (logger.isDebugEnabled()) { logger.debug("Sorted gatewayFilterFactories: " + combined); } return new DefaultGatewayFilterChain (combined).filter(exchange); }
使用时若只使用全局过滤器(GlobalFilter),就可以对GlobalFilter进行预排序,减少处理请求时因为排序带来的性能损失
实现方案:使用类加载器的就近加载机制,在项目下新建同限定名的FilteringWebHandler替换框架的默认实现
即:在项目根目录下新建一个名为org.springframework.cloud.gateway.handler的包,并新建一个与框架自带FilteringWebHandler同名的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Slf4j public class FilteringWebHandler implements WebHandler { private final List<GatewayFilter> globalFilters; public FilteringWebHandler (List<GlobalFilter> globalFilters) { List<GatewayFilter> sortedGlobalFilters = globalFilters.stream() .map(filter -> { GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter (filter); if (filter instanceof Ordered) { return new OrderedGatewayFilter (gatewayFilter, ((Ordered) filter).getOrder()); } return gatewayFilter; }) .sorted(AnnotationAwareOrderComparator.INSTANCE) .collect(Collectors.toList()); this .globalFilters = Collections.unmodifiableList(new ArrayList <>(sortedGlobalFilters)); } @Override public Mono<Void> handle (ServerWebExchange exchange) { List<GatewayFilter> globalFilters = this .globalFilters; if (log.isDebugEnabled()) { log.debug("Sorted filters: {}" , globalFilters); } return new FlexGatewayFilterChain (globalFilters).filter(exchange); } private static class GatewayFilterAdapter implements GatewayFilter { private final GlobalFilter delegate; private GatewayFilterAdapter (GlobalFilter delegate) { this .delegate = delegate; } @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { return delegate.filter(exchange, chain); } @Override public String toString () { return "GatewayFilterAdapter{" + "delegate=" + delegate + '}' ; } } }
可以复制一份框架默认的GatewayFilterChain,也可自定义实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public final class FlexGatewayFilterChain implements GatewayFilterChain { private final List<GatewayFilter> filters; private final int index; FlexGatewayFilterChain(List<GatewayFilter> filters) { this .filters = filters; this .index = 0 ; } private FlexGatewayFilterChain (List<GatewayFilter> filters, int index) { this .filters = filters; this .index = index; } @Override public Mono<Void> filter (ServerWebExchange exchange) { return this .filter(exchange, 1 ); } public Mono<Void> filter (ServerWebExchange exchange, int offset) { return Mono.defer( () -> { int index = this .index; List<GatewayFilter> filters = this .filters; if (index < filters.size()) { FlexGatewayFilterChain chain = new FlexGatewayFilterChain (filters, index + offset); return filters.get(index).filter(exchange, chain); } else { return Mono.empty(); } } ); } }
使用Native OpenSSL 若后端系统存在https调用,可引入性能更好的openSSL依赖
1 2 3 4 <dependency > <groupId > io.netty</groupId > <artifactId > netty-tcnative-boringssl-static</artifactId > </dependency >
移除不必要的依赖 若系统通过agent等形式接入了三方的监控系统,可以关闭框架自带的监控,减少资源占用
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > <exclusions > <exclusion > <groupId > io.micrometer</groupId > <artifactId > micrometer-core</artifactId > </exclusion > </exclusions > </dependency >
异步打印日志 logback配置可以简单参考,线上环境关闭console日志输出
进阶性能调优方案可参考
分布式系统日志打印优化方案的探索与实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="false" > <property name ="log_dir" value ="logs" /> <property name ="log_file_name" value ="gateway" /> <property name ="log_pattern" value ="%d{yyy-MM-dd HH:mm:ss.SSS} %-5.5p [%-30.30t] %-45.45c{45} : %m%n" /> <property name ="log_charset" value ="utf-8" /> <appender name ="console" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > ${log_pattern}</pattern > <charset > ${log_charset}</charset > </encoder > </appender > <appender name ="file" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${log_dir}/${log_file_name}.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > ${log_dir}/${log_file_name}_%d{yyyyMMdd}.log</fileNamePattern > <maxHistory > 7</maxHistory > </rollingPolicy > <encoder > <pattern > ${log_pattern}</pattern > <charset > ${log_charset}</charset > </encoder > </appender > <appender name ="async" class ="ch.qos.logback.classic.AsyncAppender" > <discardingThreshold > 0</discardingThreshold > <queueSize > 1024</queueSize > <appender-ref ref ="file" /> </appender > <root level ="info" > <appender-ref ref ="async" /> </root > </configuration >
调整RoutePredicateHandlerMapping位置 配置文件添加,将RoutePredicateHandlerMapping排在第一位
1 spring.cloud.gateway.handler-mapping.order =-10000
关闭一些不必要的组件 看实际项目需要,选择性关闭一部分不需要的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server.forward-headers-strategy =none spring.cloud.gateway.metrics.enabled =false spring.cloud.gateway.global-filter.adapt-cached-body.enabled =false spring.cloud.gateway.global-filter.forward-path.enabled =false spring.cloud.gateway.global-filter.forward-routing.enabled =false spring.cloud.gateway.global-filter.load-balancer-client.enabled =false spring.cloud.gateway.global-filter.netty-routing.enabled =true spring.cloud.gateway.global-filter.netty-write-response.enabled =true spring.cloud.gateway.global-filter.reactive-load-balancer-client.enabled =false spring.cloud.gateway.global-filter.remove-cached-body.enabled =true spring.cloud.gateway.global-filter.route-to-request-url.enabled =true spring.cloud.gateway.global-filter.websocket-routing.enabled =false spring.cloud.gateway.forwarded.enabled =false spring.cloud.gateway.x-forwarded.enabled =false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Component public class BeanDefinitionRegisterProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException { String routePredicateFactorySuffix = RoutePredicateFactory.class.getSimpleName(); String gatewayFilterFactorySuffix = GatewayFilterFactory.class.getSimpleName(); for (String beanDefinitionName : beanDefinitionRegistry.getBeanDefinitionNames()) { if (beanDefinitionName.endsWith(routePredicateFactorySuffix)) { beanDefinitionRegistry.removeBeanDefinition(beanDefinitionName); } if (beanDefinitionName.endsWith(gatewayFilterFactorySuffix)) { beanDefinitionRegistry.removeBeanDefinition(beanDefinitionName); } } beanDefinitionRegistry.removeBeanDefinition("readBodyPredicateFactory" ); beanDefinitionRegistry.removeBeanDefinition("weightCalculatorWebFilter" ); } @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } }
跨域配置 1 2 3 4 5 6 spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping=true spring.cloud.gateway.globalcors.cors-configurations.[/**].allow-credentials=true spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-methods=* spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-origin-patterns=https://*.domain.com,http://localhost:*,http://localhost spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-headers=* spring.cloud.gateway.globalcors.cors-configurations.[/**].max-age=3600
后端服务重复进行了跨域处理,导致响应头重复的解决办法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Component public class CorsResponseHttpHeadersFilter implements Ordered , HttpHeadersFilter { private final List<String> corsResponseHeaders = Collections.unmodifiableList(Arrays.asList( HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, HttpHeaders.ACCESS_CONTROL_MAX_AGE )); private final List<String> corsVaryHeaders = Collections.unmodifiableList(Arrays.asList( HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS )); @Override public int getOrder () { return Ordered.HIGHEST_PRECEDENCE; } @Override public boolean supports (Type type) { return Type.RESPONSE == type; } @Override public HttpHeaders filter (HttpHeaders headers, ServerWebExchange exchange) { String accessControlAllowOrigin = headers.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN); if (accessControlAllowOrigin != null ) { corsResponseHeaders.forEach(headers::remove); } List<String> vary = headers.remove(HttpHeaders.VARY); if (CollectionUtils.isNotEmpty(vary)) { corsVaryHeaders.forEach(vary::remove); headers.put(HttpHeaders.VARY, vary); } return headers; } }