
Sentinel 面向分布式、多语言异构化服务架构的流量治理组件。
替换 Spring Cloud CircuitBreaker。
Sentinel 实现了从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助保障微服务稳定性。
Sentinel 处理的问题有 服务雪崩、服务降级、服务熔断、服务限流、服务隔离、服务超时等
启动前提:Java 环境正常且 8080 端口未占用(Sentinel 默认端口 8080)
在官网下载最新 sentinel jar 包。
使用 cmd 命令启动下载的 jar 包(双击启动也可),Sentinel 默认账号密码为 sentinel。
首先保证环境启动,启动 Nacos 服务,启动 Sentinel 服务。
在服务方模块中,添加依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>application.yml 的 spring.cloud 下添加配置
sentinel:
transport:
dashboard: localhost:8080 # 配置sentinel dashboard地址
port: 8179 # 默认8719端口,假设被占用会从8719开始依次+1扫描,直到未被占用新建测试业务类
@RestController
public class FlowLimitController {
@PostMapping("testA")
public String testA() {
return "testA";
}
@PostMapping("testB")
public String testB() {
return "testB";
}
}启动服务并使用测试工具先访问以下两个接口一次,先访问的原因是 Sentinel 懒加载,要求必须先访问接口后才能进行监控。
多次访问后,可以在 Sentinel 的可视化界面中,看到接口的请求内容。
QPS 介绍:QPS 是英文 Queries Per Seconds 的缩写,翻译成中文即每秒请求数。通常情况下,它指的是处理请求的能力,即表示一个服务器在单位时间内处理的请求数,单位为次/秒。通过 QPS 可以大致估计出一个系统在不同配置情况下所能承受的最大访问流量,是用来评价后端服务端性能的指标之一。在负载较高时,针对单台服务器可能无法满足业务需求,可以利用负载均衡等方式进行扩容解决问题。
点击 Sentinel 左侧的流控规则,点击右上角的新建流控规则,可以看到一个配置页面。
资源名:资源的唯一名称,默认请求的接口路径,可以修改但要保证唯一。
针对来源:具体针对某个微服务进行限流,默认 default,表示不区分来源全部限流。
阈值类型:针对 QPS 或并发线程数的最大阈值数量进行判断限流。
单机阈值:与阈值类型组合使用,规定阈值数量。
是否集群:选中表示集群环境,不选中则表示非集群环境。
默认的流控模式,当接口达到限流条件时直接开启限流。
创建一个 testA 的测试流控规则,按照如下配置进行验证:
资源名:/testA
针对来源:default
阈值类型:QPS
单机阈值:1
流控模式:直接
流控效果:快速失败新增完毕后快速访问 /testA,可以看到 Sentinel 的失败提示
Blocked by Sentinel (flow limiting)Sentinel 的默认失败内容也可以自行处理,使用 fallback 即可。
当关联资源打到阈值,就限流自己。(AB 关联,B 达到阈值时 A 限流)
将 testA 修改为关联,关联 testB。使用 Postman 的 Runner Collication 进行请求测试,从结果中可以看到:
首先 testA 可以正常访问,testB 可以正常访问。
之后,testA 访问时,由于 testB 在 1 个 QPS 内存在访问,第二次 testA 访问被阻止。
针对不同链路的请求,对同一个目标进行访问时,实施针对性的不同限流措施。例如 testA 访问就限流,testB 可以正常访问。
对示例模块进行修改以便于演示。
新增一个 Service 用于配合演示
@Service
public class FlowLimitService {
@SentinelResource(value = "common") // Sentinel哨兵资源
public String flowLimit() {
return "------FlowLimitService";
}
}业务类新增代码
@Resource
private FlowLimitService flowLimitService;
@PostMapping("testC")
public String testC() {
return flowLimitService.flowLimit()+"C";
}
@PostMapping("testD")
public String testD() {
return flowLimitService.flowLimit()+"D";
} 在 yml 的sentinel 下新增配置
web-context-unify: false # controller层的方法对service层的调用不认为是同一个链路我们回到流控规则,更改规则内容为如下配置
资源名:common
针对来源:default
阈值类型:QPS
单机阈值:1
流控模式:链路
流控效果:快速失败配置结束后,我们访问 testC,可以发现当访问 QPS 超过 1 时,testC 会进行链路流控,而 testD 访问则不受影响。
当阈值类型为并发线程数时,流控效果,默认快速失败。
默认的流控处理,直接抛出异常。
当流量突然增大时,我们通常会更希望系统从空闲到繁忙的切换时间长一些。即系统在此之前长期处于空闲状态,我们希望请求的数量是逐渐的增多而不是瞬时增大,经过预期的时间后再到达系统处理请求的最大值。Warm Up 冷启动就是为了实现这个目的。
这个场景主要用于启动需要额外开销的场景,例如建立数据库连接等。
默认 Warm Up 为 3,即请求 QPS 从 预热时长/3 开始,经过预热时长逐渐升至设定的 QPS 阈值。
当某一时刻有大量的请求到来,而下一时刻又处于空闲状态的场景下,我们会更希望系统能够再接下来的空闲时间逐渐处理这些请求,而不是在第一时刻直接拒绝多余的请求。
超时时间表示 N 秒之后的请求会被丢弃,在时间内的请求会被逐个处理。
Sentinel 熔断降级会在调用链路中的某个资源出现不稳定状态时,对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断(默认抛出 DegradeException)。
择以慢调用比例作为阈值,需要设置允许的慢调用 RT(最大响应时间),请求的响应时间打与该值则统计为慢调用。当单位统计时长内请求数大于设置的最小请求数,并且慢调用比例大于阈值,则接下来的熔断时长内请求会自动熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若依然大于则继续熔断。
我们在 Sentinel 页面中,点击熔断规则-新增熔断规则,并进行配置
资源名:/testA
熔断策略:慢调用比例
最大RT:200
比例阈值:0.1
熔断时长:5(s)
最小请求数:5
统计时长:1000(ms)调用:一个请求发送到服务器,服务器给与响应。一个响应就是一个调用。
最大 RT:最大响应时间,指系统对请求做出响应的业务处理时间。
慢调用:处理业务逻辑的实际时间 > 设置的最大 RT 时间,这个调用叫慢调用。
慢调用比例:在所有调用中,慢调用占有实际的比例 = 慢调用次数 / 总调用次数。
比例阈值:自己设定的,比例阈值 = 慢调用次数 / 调用次数。
统计时长:时间的判断依据。
最小请求数:设置的调用最小请求数。例如上述图内表示当 1s 内的调用数多于 5 个时,触发熔断规则。
熔断状态的判断依据:在统计时长内,实际请求数 > 设定的最小请求数且实际慢调用比例 > 比例阈值,进入熔断状态。
测试过程参考流控。
异常比例与慢调用比例的区别就是在一个统计时长内异常调用的数量 / 总调用数量超过阈值。
异常数与异常比例的区别就是在一个统计时长内异常调用的数量超过规定的数量。
SentinelResource 是一个流量防卫组件的注解,用于防护指定资源,对配置的资源进行流量控制、熔断降级等功能。
在前文的例子中,我们在流控模式-链路中使用过该注解。
SentinelResource 主要有以下需要注意的重点:
value():SentinelResource 要保护的资源名。
blockHandler():处理 blockHandler 的参数名。
fallback():用于在抛出异常时的 fallback 处理。
默认不使用注解的情况前文的测试过程已经覆盖,以下是使用注解的相关场景
业务类
@PostMapping("rateLimit/testB")
@SentinelResource(value = "testB", blockHandler = "handleBlockHandler")
public String testB() {
return "testB";
}
public String handleBlockHandler(BlockException s) {
return "handleBlockHandler to testB";
}访问一次后(懒加载),添加流控规则-直接进行多次测试,Sentinel 默认的流控结果已经被更新为我们所写的自定义限流内容。
@PostMapping("rateLimit/testB")
@SentinelResource(value = "testB", blockHandler = "handleBlockHandler", fallback = "handleFallback")
public String testB(@RequestBody PayDTO payDTO) {
// id为1时直接异常触发降级
if (payDTO.getId().equals(1)) {
throw new RuntimeException("testB");
}
return "testB";
}
public String handleFallback(@RequestBody PayDTO payDTO, Throwable s) {
return "handleFallback to testB";
}
public String handleBlockHandler(@RequestBody PayDTO payDTO, BlockException s) {
return "handleBlockHandler to testB";
}访问 testB 后入参 id 为 1时,返回 "handleFallback to testB" 。
可以理解成自定义限流返回是程序超出了 Sentinel 配置的违规情况处理,服务降级是程序 JVM 抛出的异常服务降级。
热点就是经常访问的数据,很多时候我们希望统计或限制某个热点数据中访问频次最高的 TopN 数据,并对其访问进行限流或其他操作。
业务类
@PostMapping("rateLimit/testC")
@SentinelResource(value = "testC", blockHandler = "dealHandlerTestC")
public String testC(@RequestBody PayDTO payDTO) {
return "testC";
}
public String dealHandlerTestC(@RequestBody PayDTO payDTO, BlockException s) {
return "dealHandlerTestC";
}启动服务,访问 testC 后,添加热点规则(仅支持 QPS)。
新增热点规则进行如下配置
资源名:testC
单机阈值:0
统计窗口时长:1(s)热点规则中,参数索引表示入参的参数数量,0 表示第一个参数以此类推。单机阈值表示请求调用的数量。
当参数索引为 0 时,在示例中我们给定参数请求,统计窗口时长内的调用数大于单机阈值,触发限流操作。
新建完成后,点击编辑可以看到一个额外的参数配置
参数例外项:
在某个参数被限流的情况下,可以独立设定该参数的某一些固定入参独立设置限流或不设置限流。例外项中,参数类型仅包含七种常用的数据类型(无 short、Object),可以对这些类型的某些固定入参进行独立限流。
在某些场景下,我们需要根据调用接口的来源判断是否允许执行本次调用。在 Sentinel 的授权规则中,提供了白名单与黑名单两种授权类型。
业务类
@RestController
public class EmpowerController {
@GetMapping("empower")
public String empower() {
return " empower ";
}
}新建配置
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getParameter("serverName");
}
}启动服务并访问接口,成功后添加授权规则。
点击新增授权规则,按照如下内容配置
资源名:empower
流控应用:test1,test2
授权类型:黑名单当我们的 get 请求中携带 serverName 且值为 test1 或 test2 时,会直接触发黑名单,并提示 Sentinel 的默认限流内容。
一旦我们重启整个微服务应用,Sentinel 配置的规则将直接消失,这是我们在实际场景中所不能接受的,我们需要在生产环境中将 Sentinel 的配置规则进行持久化。
我们可以将 Sentinel 配置的规则在 Nacos 进行保存。
在 Sentinel 的测试模块中新增依赖
<!--sentinel-datasource-nacos-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>application 新增配置(自行补充新内容)
server:
port: 8401
spring:
application:
name: sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置nacos地址
sentinel:
transport:
dashboard: localhost:8080 # 配置sentinel dashboard地址
port: 8179 # 默认8719端口,假设被占用会从8719开始依次+1扫描,直到未被占用
web-context-unify: false # controller层的方法对service层的调用不认为是同一个链路 默认为true
# 新添加内容,sentinel的datasource内容
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
# ruleType是一个枚举类,包含 flow流量控制规则、degrade降级规则、authority授权规则、param_flow参数流控规则、system系统规则、hot_param热点参数规则在 nacos 中新增一个 json 配置:
{
{
"resource":"empower",
"limitApp":"default",
"grade":"1",
"count":"1",
"strategy":"0",
"controlBehavior":"0",
"clusterMode":false
}
}Json 含义为:
resource:资源名称
limitApp:来源应用
grade:阈值类型,0 为线程数,1 为 QPS
count:单机阈值
strategy:流控模式,0 为直接,1 为关联,2 为链路
controlBehavior:流控效果,0 为快速失败,1 为 WarmUp,2 为排队等待
clusterMode:是否集群
配置结束后启动测试模块并访问 /empower 接口,可以看到当我们调用过一次后,Sentinel 自行生成了一条关于 empower 的流控规则配置。
对于 OpenFeign 实现服务降级,前文已经详细的进行过讲解。当外部访问出现异常时,访问者会进入对应的 fallback 方法,但通过 feign 接口调用的方法各不相同,每个方法也有各自的 fallback 方法,导致代码无法有效管理。
我们希望通过一个统一的配置来规范管理 fallback 方法,实现定义的接口都是用一个统一的服务降级。
在服务方模块中新增依赖(若依赖过多可以自行删减或启用新模块)
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>application.yml 的 spring.cloud 下新增 Sentinel 配置
sentinel:
transport:
dashboard: localhost:8080
port: 8719新增业务代码
@PostMapping(value = "/pay/nacos/sentinel")
@SentinelResource(value = "payNacos2", blockHandler = "blockHandler")
public ResultVO payNacos2(@RequestBody PayDTO payDTO) {
String result = "openfeign整合sentinel " + payDTO.getId();
return new ResultVO(200, "成功", result);
}
public ResultVO blockHandler(PayDTO payDTO, BlockException e) {
String result = "openfeign整合sentinel " + payDTO.getId();
return new ResultVO(200, "blockHandler", result);
}api 模块新增适配依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>新增 api 访问入口(value 自行调整)
@Component
@FeignClient(value = "nacos-provider", fallback = PayFeignSentinelApiFallback.class)
public interface PayFeignSentinelApi {
@PostMapping(value = "/pay/nacos/sentinel")
ResultVO payNacos2(@RequestBody PayDTO payDTO);
}新建 PayFeignSentinelApiFallback 服务降级处理类
@Component
public class PayFeignSentinelApiFallback implements PayFeignSentinelApi{
@Override
public ResultVO payNacos2(PayDTO payDTO) {
return new ResultVO(500, "触发sentinel降级", null);
}
}调用方模块中新增依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>yml 中开启 feign 配置
feign:
sentinel:
enabled: true模块启动类添加注解
@EnableFeignClients业务类新增代码
@Resource
private PayFeignSentinelApi api;
@PostMapping("consumer/pay/nacos/sentinel")
public ResultVO payNacos2(@RequestBody PayDTO payDTO) {
return api.payNacos2(payDTO);
}若此步骤启动失败,报错不支持 get 或 post 请求,请关注一下 SpringBoot、SpringCloud、SpringCloudAlibaba 三个框架稳定版本的适配问题。个人测试时选择了降低 SpringBoot 与 SpringCloud 的版本,可以自行探索解决方案。
<!-- <spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.test.version>3.2.0</spring.boot.test.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>-->
<spring.boot.version>3.0.9</spring.boot.version>
<spring.boot.test.version>3.0.9</spring.boot.test.version>
<spring.cloud.version>2022.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>启动两个服务,首先访问服务方模块的 url,请求后能够正确的在 Sentinel 中看到该资源的资源名。之后用过调用方模块调用服务,能够正确的调用并拿到返回结果。在 Sentinel 中添加流控,再通过调用方进行访问,发现能够正确的进入 BlockHandler 流控方法。此时我们关闭服务方再次进行请求,可以发现我们正确的出发了 fallback 降级,拿到了服务降级的返回结果。
新建 Gateway 测试模块,添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example.cloud</groupId>
<artifactId>SpringCloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-sentinel-gateway9528</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--simple-http-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<!--gateway适配-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<!--javax api-->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>修改 application.yml
server:
port: 9528
spring:
application:
name: cloud-alibaba-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: payment_routh
uri: http://localhost:9001
predicates:
- Path=/pay/**copy 官网的配置内容(自行研究,个人复制的时候没有详细研究内容)
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
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.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> defaultViewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> defaultViewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
this.defaultViewResolvers = defaultViewResolvers.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// 1. 配置SentinelGatewayBlockExceptionHandler
SentinelGatewayBlockExceptionHandler exceptionHandler = new SentinelGatewayBlockExceptionHandler(this.defaultViewResolvers, this.serverCodecConfigurer);
// 2. Register the block exception handler for Spring Cloud Gateway.
return exceptionHandler;
}
@Bean
@Order(-1)
public GlobalFilter sentinelGlobalFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit() {
initBlockHandler();
}
private void initBlockHandler() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map<String, String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求过于频繁,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}进行测试,当直接访问服务方模块后,通过 9528 端口访问 /pay/nacos url,可以正确访问。调试通过后,在 Sentinel 为该接口添加流控规则。
多次测试后发现异常情况:
Postman 返回的信息中,有一部分是服务方模块自身限流的信息,有一部分是通过 gateway9528 所定义的返回信息。该问题未找到正确解释,等待日后个人技术提升再做研究。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。