Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战
在阅读本文前,建议先阅读《Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵基础实战》。
1. Sentinel 整合 Feign 和 RestTemplate
Sentinel 目前已经同时支持 Feign 和 RestTemplate,需要我们引入对应的依赖,在使用 Feign 的时候需要在配置文件中打开 Sentinel 对 Feign 的支持:feign.sentinel.enabled=true
,同时需要加入 openfeign starter
依赖使 sentinel starter
中的自动化配置类生效。在使用 RestTemplate 的时候需要在构造 RestTemplate 的 Bean 的时候加上 @SentinelRestTemplate
注解,开启 Sentinel 对 RestTemplate 的支持。
1.1 创建父工程 sentinel-springcloud-high:
父工程 pom.xml 如下:
代码清单:Alibaba/sentinel-springcloud-high/pom.xml
<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 做服务中心。
1.2 创建子工程 provider_server:
配置文件 application.yml 如下:
代码清单: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 如下:
代码清单:Alibaba/sentinel-springcloud-high/provider_server/src/main/java/com/springcloud/provider_server/controller/HelloController.java
@RestController
public class HelloController {@GetMapping("/hello")
public String hello(HttpServletRequest request) {return "Hello, port is:" + request.getLocalPort();
}
}
1.3 创建子工程 consumer_server:
子工程依赖 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 的支持。
1.4 测试
启动工程 provider_server 和 consumer_server,provider_server 修改启动配置,启动两个实例,打开浏览器访问:http://localhost:9000/helloByFeign 和 http://localhost:9000/helloByRestTemplate,刷新几次,可以看到页面交替显示 Hello, port is: 8000
和Hello, port is: 8001
,说明目前负载均衡正常,现在查看 Sentinel 控制台,如图:
1.5 流量控制测试
这时选择左侧的簇点流控,点击流控,如图:
这里我们配置一个最简单的规则,配置 QPS 限制为 1,点击新增,如图:
这里解释一下什么是 QPS,简单来说 QPS 是一个每秒访问数,这里我们测试时需要重复快速刷新 http://localhost:9000/helloByFeign 和 http://localhost:9000/helloByRestTemplate,在刷新的过程中,我们可以看到页面会显示错误信息,如:Blocked by Sentinel (flow limiting)
,说明我们配置 Sentinel 已经限流成功,这时我们再看一下 Sentinel 的控制台,可以看到我们刚才访问的成功和限流的数量,如图:
2. 服务降级
在上一小结,我们介绍了 Feign 和 RestTemplate 整合 Sentinel 使用,并且在 Sentinel 控制台上做了 QPS 限流,并且限流成功,限流成功后,默认情况下,Sentinel 对控制资源的限流处理是直接抛出异常。在没有合理的业务承接或者前端对接情况下可以这样,但是正常情况为了更好的用户业务,都会实现一些被限流之后的特殊处理,我们不希望展示一个生硬的报错。这一小节,我们介绍一下服务降级处理。
2.1 创建子工程 consumer_fallback
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
用于构造返回信息。
2.2 测试
顺次启动 provider_server 和 consumer_fallback 两个子工程。先在浏览器中交替访问 http://localhost:9090/helloByFeign 和 http://localhost:9090/helloByRestTemplate,而后打开 Sentinel 控制台,在这两个接口上增加限流信息,注意,这里要将限流信息加在资源上,具体如图:
在浏览器中刷新两个链接,两个限流信息都可以正常浏览器中显示,测试成功,再次查看 Sentinel 控制台,也可以看到被拒接的流量统计,如图:
3. 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 的实体和管理逻辑:
- GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
- ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/ 和 /baz/ 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
3.1 Zuul 1.x
Sentinel 提供了 Zuul 1.x 的适配模块,可以为 Zuul Gateway 提供两种资源维度的限流:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 route ID(对应 RequestContext 中的 proxy 字段)
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
3.1.1 创建子工程 zuul_server
工程依赖 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
,需要手动单独引入。
3.1.2 配置文件 application.yml 如下:
代码清单: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
3.1.3 定义降级处理类 ZuulFallbackProvider.java 如下:
代码清单: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);
}
}
}
3.1.4 同时,我们需要将 3 个 Sentinel 的 Filter 注入 Spring,配置类如下:
代码清单: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 类型。
3.1.5 测试
顺次启动子工程 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"}
这里注意,定义限流的是资源,千万不要定义错地方,限流定义如图:
3.2 Spring Cloud Gateway
从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
3.2.1 创建子工程 gateway_server
工程依赖 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>
3.2.2 配置文件 application.yml 如下:
代码清单: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
3.2.3 全局配置类 GatewayConfig.java 如下:
同上一小节介绍的 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();
}
}
3.2.4 降级处理类 JsonSentinelGatewayBlockExceptionHandler.java 如下:
代码清单: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 格式,各位读者有需要可以根据自己的需求进行修改。
3.2.5 测试
顺次启动 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"}
测试成功。