在閱讀本文前,建議先閱讀《Spring Cloud Alibaba | Sentinel:分佈式系統的流量防衛兵基礎實戰》。前端
Sentinel目前已經同時支持Feign和RestTemplate,須要咱們引入對應的依賴,在使用Feign的時候須要在配置文件中打開Sentinel對Feign的支持:feign.sentinel.enabled=true
,同時須要加入openfeign starter
依賴使sentinel starter
中的自動化配置類生效。在使用RestTemplate的時候須要在構造RestTemplate的Bean的時候加上@SentinelRestTemplate
註解,開啓Sentinel對RestTemplate的支持。java
父工程pom.xml以下:web
代碼清單:Alibaba/sentinel-springcloud-high/pom.xml
***spring
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
公共組件中引入Sentinel作流量控制,引入Nacos作服務中心。json
配置文件application.yml以下:api
代碼清單:Alibaba/sentinel-springcloud-high/provider_server/pom.xml
***瀏覽器
server: port: 8000 spring: application: name: spring-cloud-provider-server cloud: nacos: discovery: server-addr: 192.168.44.129:8848 sentinel: transport: dashboard: localhost:8080 port: 8720 management: endpoints: web: cors: allowed-methods: '*'
接口測試類HelloController.java以下:app
代碼清單:Alibaba/sentinel-springcloud-high/provider_server/src/main/java/com/springcloud/provider_server/controller/HelloController.java
***cors
@RestController public class HelloController { @GetMapping("/hello") public String hello(HttpServletRequest request) { return "Hello, port is: " + request.getLocalPort(); } }
子工程依賴pom.xml以下:負載均衡
代碼清單:Alibaba/sentinel-springcloud-high/consumer_server/pom.xml
***
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
配置文件application.yml以下:
代碼清單:Alibaba/sentinel-springcloud-high/consumer_server/src/main/resources/application.yml
***
server: port: 9000 spring: application: name: spring-cloud-consumer-server cloud: nacos: discovery: server-addr: 192.168.44.129:8848 sentinel: transport: dashboard: localhost:8080 port: 8719 management: endpoints: web: cors: allowed-methods: '*' feign: sentinel: enabled: true
這裏使用feign.sentinel.enabled=true
開啓Sentinel對Feign的支持。
接口測試類HelloController.java
代碼清單:Alibaba/sentinel-springcloud-high/consumer_server/src/main/java/com/springcloud/consumer_server/controller/HelloController.java
***
@RestController public class HelloController { @Autowired HelloRemote helloRemote; @Autowired RestTemplate restTemplate; @GetMapping("/helloByFeign") public String helloByFeign() { return helloRemote.hello(); } @GetMapping("/helloByRestTemplate") public String helloByRestTemplate() { return restTemplate.getForObject("http://spring-cloud-provider-server/hello/", String.class); } }
Sentinel已經對作了整合,咱們使用Feign的地方無需額外的註解。同時,@FeignClient
註解中的全部屬性,Sentinel都作了兼容。
啓動主類Ch122ConsumerServerApplication.java以下:
代碼清單:Alibaba/sentinel-springcloud-high/consumer_server/src/main/java/com/springcloud/consumer_server/ConsumerServerApplication.java
***
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class Ch122ConsumerServerApplication { public static void main(String[] args) { SpringApplication.run(Ch122ConsumerServerApplication.class, args); } @Bean @LoadBalanced @SentinelRestTemplate public RestTemplate restTemplate() { return new RestTemplate(); } }
在使用RestTemplate的時候須要增長@SentinelRestTemplate
來開啓Sentinel對RestTemplate的支持。
啓動工程provider_server和consumer_server,provider_server修改啓動配置,啓動兩個實例,打開瀏覽器訪問:http://localhost:9000/helloByFeign 和 http://localhost:9000/helloByRestTemplate ,刷新幾回,能夠看到頁面交替顯示Hello, port is: 8000
和Hello, port is: 8001
,說明目前負載均衡正常,如今查看Sentinel控制檯,如圖:
這時選擇左側的簇點流控,點擊流控,如圖:
這裏咱們配置一個最簡單的規則,配置QPS限制爲1,點擊新增,如圖:
這裏解釋一下什麼是QPS,簡單來講QPS是一個每秒訪問數,這裏咱們測試時須要重複快速刷新http://localhost:9000/helloByFeign 和 http://localhost:9000/helloByRestTemplate ,在刷新的過程當中,咱們能夠看到頁面會顯示錯誤信息,如:Blocked by Sentinel (flow limiting)
,說明咱們配置Sentinel已經限流成功,這時咱們再看一下Sentinel的控制檯,能夠看到咱們剛纔訪問的成功和限流的數量,如圖:
在上一小結,咱們介紹了Feign和RestTemplate整合Sentinel使用,而且在Sentinel控制檯上作了QPS限流,而且限流成功,限流成功後,默認狀況下,Sentinel對控制資源的限流處理是直接拋出異常。在沒有合理的業務承接或者前端對接狀況下能夠這樣,可是正常狀況爲了更好的用戶業務,都會實現一些被限流以後的特殊處理,咱們不但願展現一個生硬的報錯。這一小節,咱們介紹一下服務降級處理。
Feign服務降級類HelloRemoteFallBack.java以下:
代碼清單:Alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/fallback/HelloRemoteFallBack.java
***
@Component public class HelloRemoteFallBack implements HelloRemote { @Override public String hello() { return "Feign FallBack Msg"; } }
相對應的,這裏須要在HelloRemote.java上作一部分配置,使得限流後,觸發服務降級執行咱們的服務降級類,代碼以下:
代碼清單:ch12_2/ch12_2_consumer_fallback/src/main/java/com/springcloud/book/ch12_2_consumer_fallback/remote/HelloRemote.java
***
@FeignClient(name = "spring-cloud-provider-server", fallback = HelloRemoteFallBack.class) public interface HelloRemote { @GetMapping("/hello") String hello(); }
fallback = HelloRemoteFallBack.class
指定服務降級的處理類爲HelloRemoteFallBack.class
。
RestTemplate服務降級工具類ExceptionUtil.java以下:
代碼清單:Alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/remote/HelloRemote.java
***
public class ExceptionUtil { private final static Logger logger = LoggerFactory.getLogger(ExceptionUtil.class); public static SentinelClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) { logger.error(ex.getMessage(), ex); return new SentinelClientHttpResponse("RestTemplate FallBack Msg"); } }
這裏一樣須要修改RestTemplate註冊成爲Bean的地方,使得RestTemplate觸發服務降級之後代碼執行咱們爲它寫的處理類,Ch122ConsumerFallbackApplication.java代碼以下:
代碼清單:Alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/ConsumerFallbackApplication.java
***
@Bean @LoadBalanced @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class) public RestTemplate restTemplate() { return new RestTemplate(); }
這裏須要注意,@SentinelRestTemplate
註解的屬性支持限流(blockHandler
, blockHandlerClass
)和降級(fallback
, fallbackClass
)的處理。
其中blockHandler
或fallback
屬性對應的方法必須是對應blockHandlerClass
或fallbackClass
屬性中的靜態方法。
@SentinelRestTemplate
註解的限流(blockHandler
, blockHandlerClass
)和降級(fallback
, fallbackClass
)屬性不強制填寫。
當使用RestTemplate調用被Sentinel熔斷後,會返回RestTemplate request block by sentinel
信息,或者也能夠編寫對應的方法自行處理返回信息。這裏提供了 SentinelClientHttpResponse
用於構造返回信息。
順次啓動provider_server和consumer_fallback兩個子工程。先在瀏覽器中交替訪問http://localhost:9090/helloByFeign 和 http://localhost:9090/helloByRestTemplate ,然後打開Sentinel控制檯,在這兩個接口上增長限流信息,注意,這裏要將限流信息加在資源上,具體如圖:
在瀏覽器中刷新兩個連接,兩個限流信息均可以正常瀏覽器中顯示,測試成功,再次查看Sentinel控制檯,也能夠看到被拒接的流量統計,如圖:
Sentinel目前支持Spring Cloud Gateway、Zuul 等主流的 API Gateway 進行限流。看一下官方的結構圖,如圖:
從這張官方的圖中,能夠看到,Sentinel對Zuul的限流主要是經過3個Filter來完成的,對Spring Cloud Gateway則是經過一個SentinleGatewayFilter
和一個BlockRequestHandler
來完成的。
Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模塊,此模塊中包含網關限流的規則和自定義 API 的實體和管理邏輯:
Sentinel 提供了 Zuul 1.x 的適配模塊,能夠爲 Zuul Gateway 提供兩種資源維度的限流:
工程依賴pom.xml以下:
代碼清單:Alibaba/sentinel-springcloud-high/zuul_server/pom.xml
***
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-zuul-adapter</artifactId> </dependency>
這裏由於sentinel-zuul-adapter
未包含在spring-cloud-starter-alibaba-sentinel
,須要手動單獨引入。
代碼清單:Alibaba/sentinel-springcloud-high/zuul_server/src/main/resources/application.yml
***
server: port: 18080 spring: application: name: spring-cloud-zuul-server cloud: nacos: discovery: server-addr: 192.168.44.129:8848 sentinel: transport: dashboard: localhost:8080 port: 8720 zuul: routes: consumer-route: path: /consumer/** serviceId: spring-cloud-consumer-fallback
代碼清單:Alibaba/sentinel-springcloud-high/zuul_server/src/main/java/com/springcloud/zuul_server/fallback/ZuulFallbackProvider.java
***
public class ZuulFallbackProvider implements ZuulBlockFallbackProvider { @Override public String getRoute() { return "*"; } @Override public BlockResponse fallbackResponse(String route, Throwable cause) { RecordLog.info(String.format("[Sentinel DefaultBlockFallbackProvider] Run fallback route: %s", route)); if (cause instanceof BlockException) { return new BlockResponse(429, "Sentinel block exception", route); } else { return new BlockResponse(500, "System Error", route); } } }
代碼清單:Alibaba/sentinel-springcloud-high/zuul_server/src/main/java/com/springcloud/zuul_server/config/ZuulConfig.java
***
@Configuration public class ZuulConfig { @Bean public ZuulFilter sentinelZuulPreFilter() { // We can also provider the filter order in the constructor. return new SentinelZuulPreFilter(); } @Bean public ZuulFilter sentinelZuulPostFilter() { return new SentinelZuulPostFilter(); } @Bean public ZuulFilter sentinelZuulErrorFilter() { return new SentinelZuulErrorFilter(); } /** * 註冊 ZuulFallbackProvider */ @PostConstruct public void doInit() { ZuulBlockFallbackManager.registerProvider(new ZuulFallbackProvider()); } }
最終,啓動前須要配置JVM啓動參數,增長-Dcsp.sentinel.app.type=1
,來告訴Sentinel控制檯咱們啓動的服務是爲 API Gateway 類型。
順次啓動子工程provider_server、consumer_fallback、zuul_server,打開瀏覽器訪問:http://localhost:18080/consumer/helloByFeign ,而後咱們打開Sentinel控制檯,查看zuul_server服務,如圖:
咱們定製限流策略,依舊是QPS爲1,咱們再次刷新http://localhost:18080/consumer/helloByFeign 頁面,這時,頁面上已經能夠正產限流了,限流後顯示的內容爲:
{"code":429, "message":"Sentinel block exception", "route":"consumer-route"}
這裏注意,定義限流的是資源,千萬不要定義錯地方,限流定義如圖:
從 1.6.0 版本開始,Sentinel 提供了 Spring Cloud Gateway 的適配模塊,能夠提供兩種資源維度的限流:
工程依賴pom.xml以下:
代碼清單:Alibaba/sentinel-springcloud-high/gateway_server/pom.xml
***
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency>
代碼清單:Alibaba/sentinel-springcloud-high/gateway_server/src/main/resources/application.yml
***
server: port: 28080 spring: application: name: spring-cloud-gateway-server cloud: nacos: discovery: server-addr: 192.168.44.129:8848 sentinel: transport: dashboard: localhost:8080 port: 8720 gateway: enabled: true discovery: locator: lower-case-service-id: true routes: - id: consumer_server uri: lb://spring-cloud-consumer-fallback predicates: - Method=GET
同上一小節介紹的Zuul,這裏咱們一樣須要將兩個Sentinel有關Spring Cloud Gateway的Filter注入Spring:SentinelGatewayFilter
和SentinelGatewayBlockExceptionHandler
,這裏由於在Sentinel v1.6.0版本才加入Spring Cloud Gateway的支持,不少地方還不是很完善,異常處理SentinelGatewayBlockExceptionHandler
目前只能返回一個異常信息,在咱們的系統中沒法和上下游很好的結合,這裏筆者本身從新實現了SentinelGatewayBlockExceptionHandler
,並命名爲JsonSentinelGatewayBlockExceptionHandler
,返回參數定義成爲JSON,這裏再也不注入Sentinel提供的SentinelGatewayBlockExceptionHandler
,而是改成筆者本身實現的JsonSentinelGatewayBlockExceptionHandler
。
代碼清單:Alibaba/sentinel-springcloud-high/gateway_server/src/main/java/com/springcloud/gateway_server/config/GatewayConfig.java
***
@Configuration public class GatewayConfig { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public JsonSentinelGatewayBlockExceptionHandler jsonSentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } }
代碼清單:Alibaba/sentinel-springcloud-high/gateway_server/src/main/java/com/springcloud/gateway_server/exception/JsonSentinelGatewayBlockExceptionHandler.java
***
public class JsonSentinelGatewayBlockExceptionHandler implements WebExceptionHandler { private List<ViewResolver> viewResolvers; private List<HttpMessageWriter<?>> messageWriters; public JsonSentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolvers; this.messageWriters = serverCodecConfigurer.getWriters(); } private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { ServerHttpResponse serverHttpResponse = exchange.getResponse(); serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); byte[] datas = "{\"code\":403,\"msg\":\"Sentinel block exception\"}".getBytes(StandardCharsets.UTF_8); DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas); return serverHttpResponse.writeWith(Mono.just(buffer)); } @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { 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() { @Override public List<HttpMessageWriter<?>> messageWriters() { return JsonSentinelGatewayBlockExceptionHandler.this.messageWriters; } @Override public List<ViewResolver> viewResolvers() { return JsonSentinelGatewayBlockExceptionHandler.this.viewResolvers; } }; }
筆者這裏僅重寫了writeResponse()
方法,講返回信息簡單的更改爲了json格式,各位讀者有須要能夠根據本身的需求進行修改。
順次啓動provider_server、consumer_server和gateway_server,配置gateway_server jvm啓動參數-Dcsp.sentinel.app.type=1
,如圖:
打開瀏覽器訪問:http://localhost:28080/helloByFeign ,刷新幾回,頁面正常返回Hello, port is: 8000
,打開Sentinel控制檯,配置限流策略,QPS限制爲1,再刷新瀏覽器頁面,這時,咱們能夠看到瀏覽器返回限流信息:
{"code":403,"msg":"Sentinel block exception"}
測試成功。