Spring cloud gateway是替代zuul的網關產品,基於Spring 五、Spring boot 2.0以上、Reactor, 提供任意的路由匹配和斷言、過濾功能。上一篇文章談了一下Gateway網關使用不規範,同事加班淚兩行~,這篇文章將會側重於其餘的幾個須要注意的地方。html
這裏介紹編碼方式實現前端
HystrixObservableCommand.Setter getSetter() { HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("group-accept"); HystrixObservableCommand.Setter setter = HystrixObservableCommand.Setter.withGroupKey(groupKey); HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("command-accept"); setter.andCommandKey(commandKey); HystrixCommandProperties.Setter proertiesSetter = HystrixCommandProperties.Setter(); proertiesSetter /* * * 線程策略配置 */ //設置線程模式 缺省 1000ms .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) //執行是否啓用超時時間 缺省 true .withExecutionTimeoutEnabled(true) //使用線程隔離時,是否對命令執行超時的線程調用中斷 缺省false .withExecutionIsolationThreadInterruptOnFutureCancel(false) //執行超時的時候是否要它中斷 缺省 true .withExecutionIsolationThreadInterruptOnTimeout(true) //執行的超時時間 缺省 1000ms .withExecutionTimeoutInMilliseconds(2000) /* * * 熔斷策略 */ //是否開啓溶斷 缺省 true .withCircuitBreakerEnabled(true) // 是否容許熔斷器忽略錯誤,默認false, 不開啓 ; // true,斷路器強制進入「關閉」狀態,它會接收全部請求。 // 若是forceOpen屬性爲true,該屬性不生效 .withCircuitBreakerForceClosed(false) // 是否強制開啓熔斷器阻斷全部請求, 默認爲false // 爲true時,全部請求都將被拒絕,直接到fallback. // 若是該屬性設置爲true,斷路器將強制進入「打開」狀態, // 它會拒絕全部請求。該屬性優於forceClosed屬性 .withCircuitBreakerForceOpen(false) // 用來設置當斷路器打開以後的休眠時間窗。 // 休眠時間窗結束以後,會將斷路器設置爲「半開」狀態,嘗試熔斷的請求命令, // 若是依然請求錯誤就將斷路器繼續設置爲「打開」狀態,若是成功,就設置爲「關閉」狀態 // 熔斷器默認工做時間,默認:5000豪秒. // 熔斷器中斷請求10秒後會進入半打開狀態,放部分流量過去重試. .withCircuitBreakerSleepWindowInMilliseconds(5000) // 熔斷器在整個統計時間內是否開啓的閥值. // 在metricsRollingStatisticalWindowInMilliseconds(默認10s)內默認至少請求10次, // 熔斷器才發揮起做用,9次熔斷器都不起做用。 .withCircuitBreakerRequestVolumeThreshold(100) // 該屬性用來設置斷路器打開的錯誤百分比條件。默認值爲50. // 表示在滾動時間窗中,在請求值超過requestVolumeThreshold閾值的前提下, // 若是錯誤請求數百分比超過50,就把斷路器設置爲「打開」狀態,不然就設置爲「關閉」狀態 .withCircuitBreakerErrorThresholdPercentage(50); setter.andCommandPropertiesDefaults(proertiesSetter); return setter; } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { RouteLocatorBuilder.Builder routes = builder.routes(); RouteLocatorBuilder.Builder serviceProvider = routes .route("accept", r -> r.method(HttpMethod.GET) .and() .path("/gateway-accept/**") .and() .header(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8") .filters(f -> { f.rewritePath("/gateway-accept/(?<path>.*)", "/${path}"); f.requestRateLimiter( config -> config.setKeyResolver(new GenericAccessResolver()) .setRateLimiter(redisRateLimiter())); f.hystrix(config -> config.setName("accept") .setFallbackUri("forward:/gateway-fallback") .setSetter(getSetter())); return f; }) .uri("http://localhost:8888") ); return serviceProvider.build(); }
在上面的代碼中,主要作了3件事情:限流、熔斷策略及降級方法配置java
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 1500
lettuce:
pool:
max-active: 300 #鏈接池最大鏈接數(使用負值表示沒有限制)
max-idle: 10 #鏈接池中的最大空閒鏈接
min-idle: 5 #鏈接池中的最小空閒鏈接
max-wait: -1 #鏈接池最大阻塞等待時間(使用負值表示沒有限制)
/** * @description: 按照訪問地址進行限流(也能夠安裝其餘條件進行限流),具體能夠看exchange.getRequest()的方法和屬性 **/ public class GenericAccessResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getPath().value()); } }
RedisRateLimiter redisRateLimiter() { //1000,1500對應replenishRate、burstCapacity return new RedisRateLimiter(1000, 1500); }
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { RouteLocatorBuilder.Builder routes = builder.routes(); RouteLocatorBuilder.Builder serviceProvider = routes .route("accept", r -> r.method(HttpMethod.GET) .and() .path("/gateway-accept/**") .and() .header(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8") //.and() //.readBody(String.class, readBody -> true) .filters(f -> { f.rewritePath("/gateway-accept/(?<path>.*)", "/${path}"); f.requestRateLimiter(config -> config.setKeyResolver(new GenericAccessResolver()).setRateLimiter(redisRateLimiter())); return f; }) .uri("http://localhost:8888") ); return serviceProvider.build(); }
測試react
jmeter配置redis
結果spring
其餘json
若是有多個路由,使用不一樣的限流策略,能夠自定義KeyResolver和RedisRateLimiter, 在路由定義時加入併發
//基於ip限流 public class OtherAccessResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } } RedisRateLimiter otherRedisRateLimiter() { //1000,1500對應replenishRate、burstCapacity return new RedisRateLimiter(100, 500); } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { RouteLocatorBuilder.Builder routes = builder.routes(); RouteLocatorBuilder.Builder serviceProvider = routes .route("accept", r -> r.method(HttpMethod.GET) .and() .path("/gateway-accept/**") .and() .header(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8") .filters(f -> { f.rewritePath("/gateway-accept/(?<path>.*)", "/${path}"); f.requestRateLimiter( config -> config.setKeyResolver(new GenericAccessResolver()) .setRateLimiter(redisRateLimiter())); f.hystrix(config -> config.setName("accept") .setFallbackUri("forward:/gateway-fallback") .setSetter(getSetter())); return f; }) .uri("http://localhost:8888")) .route("sign", r -> r.method(HttpMethod.POST) .and() .path("/gateway-sign/**") .and() .header(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8") .filters(f -> { f.rewritePath("/gateway-sign/(?<path>.*)", "/${path}"); f.requestRateLimiter( config -> config.setKeyResolver(new OtherAccessResolver()) .setRateLimiter(otherRedisRateLimiter())); f.hystrix(config -> config.setName("sign") .setFallbackUri("forward:/gateway-fallback") .setSetter(getSetter())); return f; }) .uri("http://localhost:7777") ); return serviceProvider.build(); }
熔斷策略主要是線程配置和熔斷配置,上面已經說明很清楚了。在上篇文章中,爲了解決網關調用後臺服務Connection prematurely closed BEFORE response的問題,要設置後臺服務線程的空閒時間和網關線程池線程的空閒時間,並讓網關線程池線程的空閒時間小於後臺服務的空閒時間app
spring: cloud: gateway: httpclient: pool: max-connections: 500 max-idle-time: 10000
翻閱Spring Cloud Gateway英文資料,知道路由提供一個metadata方法,能夠設置路由的元數據(https://docs.spring.io/spring-cloud-gateway/docs/2.2.6.RELEASE/reference/html/#route-metadata-configuration),這些元數據在RouteMetadataUtils中定義:ide
package org.springframework.cloud.gateway.support; public final class RouteMetadataUtils { public static final String RESPONSE_TIMEOUT_ATTR = "response-timeout"; public static final String CONNECT_TIMEOUT_ATTR = "connect-timeout"; private RouteMetadataUtils() { throw new AssertionError("Must not instantiate utility class."); } }
其中沒有我要的線程數量(max-connection)和空閒時間(max-idle-time)的設置,沒有關係,本身加上去:
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { RouteLocatorBuilder.Builder routes = builder.routes(); RouteLocatorBuilder.Builder serviceProvider = routes .route("accept", r -> r.method(HttpMethod.GET) .and() .path("/gateway-accept/**") .and() .header(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8") .filters(f -> { f.rewritePath("/gateway-accept/(?<path>.*)", "/${path}"); f.requestRateLimiter( config -> config.setKeyResolver(new GenericAccessResolver()) .setRateLimiter(redisRateLimiter())); f.hystrix(config -> config.setName("accept") .setFallbackUri("forward:/gateway-fallback") .setSetter(getSetter())); return f; }) .uri("http://localhost:8888") .metadata("max-idle-time", 10000) //網關調用後臺線程空閒時間設置 .metadata("max-connections", 200) //網關調用後臺服務線程數量設置 ); return serviceProvider.build(); }
測試果真和yml配置同樣有效果。
降級方法自己沒有什麼特別,有一個問題須要注意,調用降級方法也是使用線程池的,缺省在HystrixThreadPoolProperties中定義:
public abstract class HystrixThreadPoolProperties { /* defaults */ static int default_coreSize = 10; // core size of thread pool static int default_maximumSize = 10; // maximum size of thread pool static int default_keepAliveTimeMinutes = 1; // minutes to keep a thread alive static int default_maxQueueSize = -1; // size of queue (this can't be dynamically changed so we use 'queueSizeRejectionThreshold' to artificially limit and reject) // -1 turns it off and makes us use SynchronousQueue
若是上面的限流設置比較大,好比1000,最大突發2000,網關調用後臺服務發生熔斷降級, 熔斷後降級的方法調用太頻繁,10個線程不夠用,會致使如下500錯誤:
2021-02-01 14:29:45.076 ERROR 64868 --- [ioEventLoop-5-1] a.w.r.e.AbstractErrorWebExceptionHandler : [a0ed6911-18982] 500 Server Error for HTTP GET "/gateway-accept/test"
com.netflix.hystrix.exception.HystrixRuntimeException: command-accept fallback execution rejected.
at com.netflix.hystrix.AbstractCommand.handleFallbackRejectionByEmittingError(AbstractCommand.java:1043) ~[hystrix-core-1.5.18.jar:1.5.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
|_ checkpoint ⇢ HTTP GET "/gateway-accept/test" [ExceptionHandlingWebHandler]
com.netflix.hystrix.exception.HystrixRuntimeException: command-accept fallback execution rejected.
at com.netflix.hystrix.AbstractCommand.handleFallbackRejectionByEmittingError(AbstractCommand.java:1043) ~[hystrix-core-1.5.18.jar:1.5.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
|_ checkpoint ⇢ HTTP GET "/gateway-accept/test" [ExceptionHandlingWebHandler]
因此要在yml中設置合適的調用降級方法的線程池, 合理的配置可以杜絕網關500錯誤的發生。
hystrix: threadpool: group-accept: #代碼裏面設置的HystrixCommandGroupKey.Factory.asKey("group-accept") coreSize: 50 #併發執行的最大線程數,默認10 maxQueueSize: 1500 #BlockingQueue的最大隊列數 #即便maxQueueSize沒有達到,達到queueSizeRejectionThreshold該值後,請求也會被拒絕 queueSizeRejectionThreshold: 1400
上面的異常後,沒有捕獲異常直接返回前端500錯誤,通常狀況下須要返回一個統一接口,好比:
@Data @ToString @EqualsAndHashCode @Accessors(chain = true) public class Result<T> implements Serializable { private Integer code; private String message; private T data; private String sign; public static final String SUCCESS = "成功"; public static final String FAILURE = "失敗"; public Result(int code, String message) { this.code = code; this.message = message; } public Result(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public Result(int code, String message, T data, String sign) { this.code = code; this.message = message; this.data = data; this.sign = sign; } public static Result<Object> success() { return new Result<Object>(200, SUCCESS); } public static Result<Object> success(Object data) { return new Result<Object>(200, SUCCESS, data); } public static Result<Object> success(Object data, String sign) { return new Result<Object>(200, SUCCESS, data, sign); } public static Result<Object> failure() { return new Result<Object>(400, FAILURE); } public static Result<Object> failure(Object data) { return new Result<Object>(400, FAILURE, data); } public static Result<Object> failure(Object data, String sign) { return new Result<Object>(400, FAILURE, data, sign); } }
建立GlobalExceptionConfiguration 實現ErrorWebExceptionHandler(這一段是來者網友提供的)
@Slf4j @Order(-1) @Component @RequiredArgsConstructor public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler { private final ObjectMapper objectMapper; @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { return Mono.error(ex); } response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); if (ex instanceof ResponseStatusException) { response.setStatusCode(((ResponseStatusException) ex).getStatus()); } return response .writeWith(Mono.fromSupplier(() -> { DataBufferFactory bufferFactory = response.bufferFactory(); try { return bufferFactory.wrap(objectMapper.writeValueAsBytes(Result.failure(ex.getMessage()))); } catch (JsonProcessingException e) { log.warn("Error writing response", ex); return bufferFactory.wrap(new byte[0]); } })); } }
這樣,就會把網關異常統一包裝在接口中返回:如:
後臺日誌已經沒有以前的錯誤日誌了。
因爲Spring Cloud Gateway 中的 Hystrix採用的是HystrixObservableCommand.Setter, 沒有采用 HystrixCommand.Setter, 在 HystrixCommand.Setter中是能夠編碼實現線程池配置的, 可是在HystrixObservableCommand.Setter沒有提供:
final public static class Setter { protected final HystrixCommandGroupKey groupKey; protected HystrixCommandKey commandKey; protected HystrixThreadPoolKey threadPoolKey; //有屬性可是沒有set方法 protected HystrixCommandProperties.Setter commandPropertiesDefaults; protected HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults; //有屬性沒有set方法 protected Setter(HystrixCommandGroupKey groupKey) { this.groupKey = groupKey; // default to using SEMAPHORE for ObservableCommand commandPropertiesDefaults = setDefaults(HystrixCommandProperties.Setter()); } public static Setter withGroupKey(HystrixCommandGroupKey groupKey) { return new Setter(groupKey); } public Setter andCommandKey(HystrixCommandKey commandKey) { this.commandKey = commandKey; return this; } public Setter andCommandPropertiesDefaults(HystrixCommandProperties.Setter commandPropertiesDefaults) { this.commandPropertiesDefaults = setDefaults(commandPropertiesDefaults); return this; } private HystrixCommandProperties.Setter setDefaults(HystrixCommandProperties.Setter commandPropertiesDefaults) { if (commandPropertiesDefaults.getExecutionIsolationStrategy() == null) { // default to using SEMAPHORE for ObservableCommand if the user didn't set it commandPropertiesDefaults.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE); } return commandPropertiesDefaults; } }
因爲本人水平有限,沒有找到Setter中設置HystrixThreadPoolKey和HystrixThreadPoolProperties.Setter的方法,因此只能在yml中配置。有知道的同窗告訴我一聲,不勝感激。
因此在Spring Cloud Gateway網關的配置中,須要綜合考慮限流大小、網關調用後臺鏈接池設置大小、後臺服務的鏈接池以及空閒時間,包括網關調用降級方法的線程池配置,都須要在壓測中調整到一個合理的配置,才能發揮最大的功效。
本人水平有限,跟深刻的研究還在繼續,若是文章有表達錯誤或者不周,請你們指正,謝謝!