Sentinel
本专栏学习内容来自尚硅谷周阳老师的视频
有兴趣的小伙伴可以点击视频地址观看
简介
Sentinel是Alibaba公司推出的一个熔断与限流工具,相当于我们之前学习的Hystrix,可以解决服务使用中的各种问题,例如:服务雪崩、服务降级、服务熔断、服务限流。中文文档
下载与安装
直接去github上下载即可,下载链接
小黄下载的是1.8.6的版本,直接下载jar包即可
下载完之后,直接启动jar包即可,这里要注意Sentinel占用的是8080端口
下载完成,可以通过localhost:8080
访问可视化界面,默认用户名密码都是sentinel
初始化演示工程
为了更方便的理解Sentinel以及后续的使用,我们先来玩一个小demo
接下来的微服务,我们都采用SpringCloud Alibaba那一套
启动Nacos
学习阶段,为了方便启动单机版的Nacos即可
startup.cmd -m standalone
创建工程
新建cloudalibaba-sentinel-service8401
服务
添加依赖
<dependencies>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel持久化,之后会用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
编写配置文件
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
dashboard: localhost:8080 # 配置Sentinel dashboard地址
port: 8719 # 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
management:
endpoints:
web:
exposure:
include: '*' # 暴露所有端点
启动类
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
业务类
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
启动Sentinel
直接运行jar包即可
java -jar sentinel-dashboard-1.8.6.jar
测试
此时我们启动8401服务,发现sentinel控制台空空如也
这是因为sentinel是懒加载机制,当你第一次访问接口之后,就会显示了,如下图所示
流量控制
流量控制,简称流控,主要是对高并发请求数量多的接口进行流量管控,防止服务器垮掉。
感兴趣的同学可以看一下 官方文档
使用
比如我要对/testA
接口进行流控,可以点击这个按钮,新增流控规则
流量规则新增时有几个名词,我们需要先来提前了解一下
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default,不区分来源
- 阈值类型/单机阈值:
- QPS:每秒钟的请求数量,当调用该api的QPS达到阈值时,进行限流
- 线程数:当调用该api的线程数达到阈值时,进行限流
- 是否集群:不需要集群
- 流控模式:
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
流控模式
直接(默认)
系统默认是直接->快速失败
两者搭配
配置说明
当我们对/testA
进行如下配置时,表示1秒钟只能有一次请求,如果请求过快,直接抛异常
测试
疯狂请求/testA
会出现以下错误
关联
配置说明
以下配置表示:当/testB
一秒钟之内访问超过一次,那么就对/testA
进行限流
举个例子:当一个接口调用支付微服务时,支付微服务需要调用第三方接口,假如支付微服务达到阈值了,我们可以直接限制最前面的接口
测试
使用postman工具,对/testB
接口进行调用,如下设置表示,每个0.8秒发送1一个请求,一共发送10个
运行时,访问/testA
会报错
链路
与关联类似
流控效果
快速失败(默认)
这个已经演示过了,直接抛异常
是由com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
类进行处理的
Warm Up
配置说明
当我进行如下配置时表示,刚开始时,QPS为10/冷加载因子(3)
就是3,也就是说刚开始访问/testA
时,一秒钟最多允许访问3次,而在5秒后单机阈值提高,一秒钟最多允许访问10次
是由com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
进行处理的,并且他默认冷加载因子为3
排队等待
配置说明
当我们对/testA
进行如下设置表示:每隔200ms才允许通过下一个请求
测试
通过postman并发10个请求
每个请求至少都间隔了200ms
熔断降级
概念
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
熔断策略
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
降级策略实战
慢调用比例
配置说明
在1000毫秒,也就是1秒内,如果发送到/testC
的请求数数量大于5,并且在这些请求中,所有请求的响应时长(因为比例与阈值为1,所以是所有的请求响应时长)都大于200毫秒,也就是都大于0.2秒的时候,进入熔断状态,熔断时长为2秒,若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
测试
模拟接口调用超时
@GetMapping("/testC")
public String testC() throws InterruptedException {
Thread.sleep(1000);
return "------testC";
}
并发20个请求
在五个请求后,服务被熔断降级
异常比例
配置说明
在1000毫秒,也就是1秒内,如果发送到/testC
的请求数数量大于5,并且在这些请求中,有50%的请求异常,则进入熔断状态
测试
模拟接口调用超时
@GetMapping("/testC")
public String testC() throws InterruptedException {
int i = 10 /0 ;
return "------testC";
}
异常数
配置说明
在1000毫秒,也就是1秒内,如果发送到/testC
的请求数数量大于5,并且在这些请求中,有2个请求异常,则进入熔断状态,异常数的熔断时长是分钟级别的,也就是说最小为1分钟!
热点Key限流
概念
热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作
实操
现在我们有这么一个接口,我希望在他带有参数p1时对其进行限流
@GetMapping("/testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "------testHotKey";
}
修改代码以及sentinel
我们需要借助到@SentinelResource
注解
- 资源名:与value值对应
- 参数索引:0代表第一个参数,也就是
p1
测试
启动服务,对该接口请求,当请求带有p1
时,达到阈值会限流,控制台以及页面都会报错
自定义异常
和Hystrix类似,通过blockHandler
属性来定义处理方法
兜底方法参数BlockException
不可省略!
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception){
return "------testHotKey error :" + exception;
}
参数例外项
除了上述对有无参数的限流之外,Sentinel还提供了对指定值的特殊处理,例如当p1='abc'
时,阈值放大到200,打开高级选项进行如下设置
需要注意的是参数类型只支持基本类型以及String
多提一句,热点Key限流只处理他自己的异常,程序中出现异常还是由程序本身进行处理,并不会走到兜底方法中
系统规则
概述
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
实操
在生产环境中慎用系统保护规则!
一旦达到阈值,整个服务都无法访问
我们玩一个简单一点的,入口QPS,当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
设置
对Sentinel进行如下设置,当一秒钟请求次数达到一次意思,就会触发系统保护,所有的接口都会禁止访问
@SentinelResource
概述
有如下接口,注册到Sentinel中,可以按照Url地址或者资源名对其进行限流
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2023, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
如下图所示/byResource
是按照Url地址限流,对应着GetMapping
,byResource
是按照资源名限流,对应着value
问题
通过@SentinelResource(value = "byResource", blockHandler = "handleException")
中blockHandler
属性,可以设置服务的兜底解决方案,但还是会出现跟Hystix中一样的问题
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
- 每个业务方法都添加一个兜底的,那代码膨胀加剧。
- 全局统一的处理方法没有体现。
解决方案
创建自定义处理器
处理方法必须是public static
public class CustomerBlockHandler {
public static CommonResult handleException(BlockException exception){
return new CommonResult(2023,"自定义的限流处理信息......CustomerBlockHandler");
}
}
修改接口
- blockHandlerClass:代表使用哪个类来处理
- blockHandler:类中的哪个方法来处理
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2023, "serial001"));
}
服务熔断
前期准备
使用sentinel整合ribbon+openFeign+fallback实现服务熔断
准备两个服务提供者9003/9004,一个消费者84
提供者9003/9004
pom文件
<dependencies>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel持久化,之后会用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.yellowstar.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Integer,Payment> hashMap = new HashMap();
static {
hashMap.put(1, new Payment(1, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2, new Payment(2, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3, new Payment(3, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult paymentSQL(@PathVariable("id") int id) {
Payment payment = hashMap.get(id);
CommonResult result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment);
return result;
}
}
消费者84
pom文件
与提供者一致
配置文件
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
RestTemplate配置
对RestTemplate配置负载均衡
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
业务类
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
public CommonResult fallback(@PathVariable int id) {
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
Ribbon系列
说明
@SentinelResource有两个属性
- fallback:管运行异常
- blockHandler:管Sentinel配置违规
没有任何配置
在没有任何配置的情况下,程序报错会直接返回error页面
只配置fallback
修改84业务代码
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback" , fallback = "handlerFallback")
public CommonResult fallback(@PathVariable int id) {
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
public CommonResult handlerFallback(@PathVariable int id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
测试
当程序出现异常时,会交给兜底方法来处理
只配置blockHandler
此配置需要综合Sentinel,和之前演示的效果一样
修改业务代码
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback" , blockHandler = "blockHandler")
public CommonResult fallback(@PathVariable int id) {
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
public CommonResult blockHandler(@PathVariable int id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
同时配置fallback和blockHandler
修改业务代码
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback" , blockHandler = "blockHandler", fallback = "handlerFallback")
public CommonResult fallback(@PathVariable int id) {
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
public CommonResult blockHandler(@PathVariable int id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
public CommonResult handlerFallback(@PathVariable int id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
测试
通过测试可以发现blockHandler挡在fallback之前,一旦触发了blockHandler就不会进入程序执行
忽略属性
通过以下属性配置,可以让Sentinel忽略指定错误,不交给fallback处理
@SentinelResource(value = "fallback" , blockHandler = "blockHandler", fallback = "handlerFallback"
,exceptionsToIgnore = {IllegalArgumentException.class})
Feign系列
激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
主启动类激活Feign
@EnableFeignClients
Feign远程调用业务接口
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult paymentSQL(@PathVariable("id") int id);
}
兜底方法处理类
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult paymentSQL(int id) {
return new CommonResult(444, "服务降级返回,没有该流水信息", new Payment(id, "errorSerial......"));
}
}
测试
此时关闭提供者服务,发现可以被兜底方法处理
规则持久化
小黄在学习的过程中遇到了很烦的问题,当我们程序重启时,Sentinel定义的规则就会消失,在生产环境中我们肯定需要将配置规则持久化。
做法
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
实操
添加相关依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改配置文件
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
datasource: ## 以下是新增
ds1:
nacos:
server-addr: localhost:8848
data-id: ${spring.application.name}
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
nacos创建配置
json配置介绍
-
resource:资源名称;
-
limitApp:来源应用;
-
grade:阈值类型,0表示线程数,1表示QPS;
-
count:单机阈值;
-
strategy:流控模式,0表示直接,1表示关联,2表示链路;
-
controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
-
clusterMode:是否集群。
测试
当我们重启服务时Sentinel会读取Nacos中的相关配置