Gateway Sentinel 做网关降级/流控,转发header和cookie
大家好,我是烤鴨:
???Springcloud Gateway 使用 Sentinel 流量控制。
環境
springcloud-gateway的網關應用,springboot的服務,nacos作為注冊中心
sentinel-dashboard-1.8.2
最新版下載地址:
https://github.com/alibaba/Sentinel/releases
目標
在網關層根據qps對指定路由降級到其他接口。
sentinel 接入的官方wiki:
https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81
網關代碼:
https://gitee.com/fireduck_admin/scg-sentinel
業務代碼就不貼了,就是普通的springboot服務,集成nacos注冊中心。
dashboard
dashboard 需要有訪問才能顯示,訪問幾次。
由于沒配置網關類型,sentinel 默認是服務,是沒有API管理的。
網關服務啟動參數中添加 -Dcsp.sentinel.app.type=1,這回有了。
API 管理和流控規則配置
API 添加的地址我這邊選的是精確匹配路由,路由就是上圖實時監控的地址。
流程規則選按API分組和QPS進行降級,模式選快速失敗。(在網關代碼里實現具體快速失敗的邏輯,比如調第三方接口降級)
scg 代碼開發
默認流程:
GatewayConfiguration,配置降級時觸發異常
package com.maggie.demo.scgsentinel.config;import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.maggie.demo.scgsentinel.handler.SentinelBlockRequestHandler; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.handler.FilteringWebHandler; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver;import javax.annotation.PostConstruct; import java.util.Collections; import java.util.List;@Order(1) @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public class GatewayConfiguration implements ApplicationContextAware {@Autowiredprotected FilteringWebHandler filteringWebHandler;@Value("${spring.application.name}")private String PJ_NAME;public String getPJ_NAME() {return PJ_NAME;}public void setPJ_NAME(String PJ_NAME) {this.PJ_NAME = PJ_NAME;}private final List<ViewResolver> viewResolvers;private final ServerCodecConfigurer serverCodecConfigurer;private final BlockRequestHandler blockRequestHandler;private ApplicationContext applicationContext;public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,ServerCodecConfigurer serverCodecConfigurer) {this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);this.serverCodecConfigurer = serverCodecConfigurer;this.blockRequestHandler = new SentinelBlockRequestHandler();}// 不配置異常處理,采用默認的提示 // @Bean // @Order(Ordered.HIGHEST_PRECEDENCE) // public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // // Register the block exception handler for Spring Cloud Gateway. // return new GatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); // }@Bean@Order(-1)public GlobalFilter sentinelGatewayFilter() {return new SentinelGatewayFilter();}@PostConstructpublic void doInit() throws Exception {initBlockStrategy();}private void initBlockStrategy() {String[] beanNamesForType = applicationContext.getBeanNamesForType(BlockRequestHandler.class);if (beanNamesForType != null && beanNamesForType.length > 0) {GatewayCallbackManager.setBlockHandler(applicationContext.getBean(beanNamesForType[0], BlockRequestHandler.class));} else {GatewayCallbackManager.setBlockHandler(blockRequestHandler);}}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}/*** @Description 加這個注解可以直接通過注冊中心的application.name直接調用服務* @Date 2021/8/12 14:28**/@Bean@LoadBalancedpublic WebClient.Builder loadBalancedWebClientBuilder() {return WebClient.builder();} }下圖是觸發降級和默認的限流異常提示
自定義流程:
增加異常處理和降級邏輯
GatewayBlockExceptionHandler
package com.maggie.demo.scgsentinel.handler;import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.util.function.Supplier; import org.springframework.http.HttpStatus; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;import java.util.List;public class GatewayBlockExceptionHandler extends SentinelGatewayBlockExceptionHandler {private List<ViewResolver> viewResolvers;private List<HttpMessageWriter<?>> messageWriters;public GatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {super(viewResolvers, serverCodecConfigurer);this.viewResolvers = viewResolvers;this.messageWriters = serverCodecConfigurer.getWriters();}private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {return response.writeTo(exchange, contextSupplier.get());}@Overridepublic Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {/*** 處理一下504*/if (ex != null && ex instanceof ResponseStatusException) {ResponseStatusException responseStatusException = (ResponseStatusException) ex;//只處理超時的情況if (HttpStatus.GATEWAY_TIMEOUT.equals(responseStatusException.getStatus())) {return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));}}if (exchange.getResponse().isCommitted()) {return Mono.error(ex);}// This exception handler only handles rejection by Sentinel.if (!BlockException.isBlockException(ex)) {return Mono.error(ex);}return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));}private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);}private final Supplier<ServerResponse.Context> contextSupplier = () -> new ServerResponse.Context() {@Overridepublic List<HttpMessageWriter<?>> messageWriters() {return GatewayBlockExceptionHandler.this.messageWriters;}@Overridepublic List<ViewResolver> viewResolvers() {return GatewayBlockExceptionHandler.this.viewResolvers;}}; }SentinelBlockRequestHandler
package com.maggie.demo.scgsentinel.handler;import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;import java.util.HashSet; import java.util.Set;import static org.springframework.web.reactive.function.BodyInserters.fromValue;@Component @Slf4j public class SentinelBlockRequestHandler implements BlockRequestHandler, Ordered {public static final Set<String> FLOW_LIMIT_API = new HashSet<>(32);static {FLOW_LIMIT_API.add("/test/api/tab/test");}public static final String CODE_SYSTEM_BUSY = "11004";public static final String MSG_SYSTEM_BUSY = "網絡開小差了,請稍后重試.";@Overridepublic int getOrder() {return LOWEST_PRECEDENCE;}@Autowiredprivate WebClient.Builder webClientBuilder;@Autowiredprivate ApplicationContext applicationContext;@Overridepublic Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable ex) {String requestUrl = exchange.getRequest().getPath().toString();Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);if (FLOW_LIMIT_API.contains(requestUrl)) {return handleRequestDetail(exchange,ex);}// JSON result by default.return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(fromValue(buildErrorResult(ex)));}private ErrorResult buildErrorResult(Throwable ex) {return new ErrorResult(CODE_SYSTEM_BUSY, MSG_SYSTEM_BUSY);}private static class ErrorResult {private final String status;private final String message;public String getStatus() {return status;}ErrorResult(String status, String message) {this.status = status;this.message = message;}public String getMessage() {return message;}}public Mono<ServerResponse> handleRequestDetail(ServerWebExchange exchange, Throwable ex) {String requestUrl = exchange.getRequest().getURI().getRawPath();System.out.println("requestUrl = " + requestUrl);Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);// post請求的參數需要自己寫filter 獲取,這里只是獲取的get請求參數MultiValueMap<String, String> getParams = exchange.getRequest().getQueryParams();// 轉發到第三方接口,這里是配置的時候route-id和注冊中心服務的application.name(lb://data-ballast-api) 一致了// 原地址 localhost:8083/test/api/tab/test, 降級地址 http://data-ballast-api/api/tab/test/sentinelStringBuilder newUri = new StringBuilder("http://").append(route.getId()).append(requestUrl+"/sentinel");log.info("限流uri: {}, query參數: {}, 降級至: uri {}", requestUrl, getParams, newUri.toString());//return webClientBuilder.build().post() //.uri(newUri.toString())// 傳遞 request header.headers(newHeaders -> newHeaders.putAll(exchange.getRequest().getHeaders())).bodyValue(getParams).exchangeToMono(response -> {return response.bodyToMono(String.class).defaultIfEmpty("").flatMap(body -> {return ServerResponse.status(response.statusCode())// 傳遞 response header,避免cookie在網關層丟失.headers(it -> {it.addAll(response.headers().asHttpHeaders());}).bodyValue(body);});});} }自定義三方降級接口:
降級成功:
待優化
上面已經基本實現了網關層面進行流控。
還有幾個地方可以優化:
-
流控規則等配置的持久化:sentinel 存的規則是默認存到內存里的,一旦重啟了服務(網關或者普通的業務服務),規則需要重新配置。持久化可以選擇 apollo或者 nacos。(一般的配置中心)
-
針對不同方式的參數獲取待完善(POST請求、文件上傳 等等)
-
針對不同的route走不同的降級策略(代碼優化,可以使用策略模式)
-
不同異常的處理,比如通用的 GatewayConfiguration 是處理了所有異常進行的 handleRequest 處理,可能有些請求不適合按降級處理(比如超時之類的)
總結
以上是生活随笔為你收集整理的Gateway Sentinel 做网关降级/流控,转发header和cookie的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jeDate实现日期联动
- 下一篇: 在db2数据库上模拟死锁场景 还是z上的