Sentinel(一)Sentinel简介
1 高并发系统关注的问题
-
服务单一职责+独立部署:秒杀服务即使自己扛不住压力,挂掉,也不要影响别人
-
秒杀链接加密:防止恶意攻击,模拟秒杀请求1000次/秒攻击;防止链接暴露,防止自己工作人员提前秒杀商品
-
库存预热+快速扣减:无需每次进行实时校验,库存预热放到redis中,使用信号量控制秒杀请求
-
动静分离:使用CDN网络访问静态资源,分担集群的压力
-
恶意攻击拦截:识别非法攻击请求在网关层进行拦截
-
流量错峰:使用各种手段,将流量分担到更大宽度的时间点,比如验证码,加入购物车
-
限流、熔断、降级:前端限流+后端限流,限制次数、限制总量,快速失败降级运行,熔断隔离防止雪崩
-
队列削峰:1w个商品,每1000件秒杀,双11所有秒杀成功的请求先进入队列,慢慢创建订单再扣减库存即可
2 Spring Cloud Alibaba - Sentinel
2.1 熔断、降级与限流
-
熔断:A服务调用B服务的某个功能,由于网络不稳定的原因或者B服务卡机导致功能时间延长,如果超时次数过多就直接将B断路(即A不再请求B的接口),直接返回降级的数据,这样就不必等待B的超长执行了,B的故障问题也就不会影响到A了
-
降级:整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况和流量,对一些服务和页面进行有策略的降级(停止服务,所有的调用直接返回降级服务),以此缓解服务器资源的压力,以保证核心业务的正常运行,同时也保持了客户和绝大部分客户得到正确的响应。
-
熔断和降级的相同点:
- 都是为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
- 用户最终都是体验到某个功能不可用
-
熔断和降级的不同点:
- 熔断是被调方故障,触发的是系统的主动规则
- 降级是基于全局的考虑,停止一切正常服务,释放资源
-
限流:对打入服务的请求流量进行限制,使服务能够承担不超过自己能力的流量压力
2.2 Sentinel简介
- 随着微服务的流行,服务和服务之间的稳定性变得更加重要,Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性
2.3 Sentinel和Hystrix的对比
- Hystrix在隔离策略上使用的是
线程池隔离
,线程池隔离
的做法就是为同一批请求分配不同的线程实现的隔离,但是这么做导致线程切换损失太大,但是线程池隔离比较彻底,线程之间相互不受影响(后面也支持了信号量隔离),而Sentinel支持信号量隔离
,主要是使用信号量对并发线程数进行限流 - Sentinel支持基于响应时间、异常比率、异常数的熔断降级策略,而Hystrix只支持基于异常比率的熔断降级策略
- 两者的实时统计都是基于滑动窗口的
- 动态规则配置也都支持多个数据源
- 扩展性方面,Sentinel支持多个扩展点,但是Hystrix支持插件的形式
- 两者都支持注解
- 限流机制Sentinel可以支持QPS、基于调用关系链的限流
- Sentinel支持预热模式、匀速器模式、预热排队模式的流量整形,Hystrix不支持
- Sentinel支持系统自适应保护,Hystrix不支持
- Sentinel的控制台可以配置规则、查看秒级监控、机器发现等,Hystrix只能进行简单的监控查看
2.4 整合Sentinel
-
引入Sentinel依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
下载对应版本的DashBoard:https://github.com/alibaba/Sentinel/releases
-
启动DashBoard:
java -jar --add-opens java.base/java.lang=ALL-UNNAMED sentinel-dashboard-1.8.0.jar --server.port=8333
我的电脑环境装的是jdk19,版本较高,由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了。
为了兼容原先的版本,需要在运行项目时添加 --add-opens java.base/java.lang=ALL-UNNAMED 选项来开启这种默认不被允许的行为。 -
配置Sentinel控制台地址信息
spring: cloud: sentinel: transport: # Sentinel控制台地址 dashboard: localhost:8333 # Sentinel数据传输端口 port: 8719
-
启动服务,访问后即可在localhost:8333控制台查看访问情况:
3 Sentinel流控
3.1 流控规则
- 流控主要是针对controller的堵塞,流控生效之后,整个controller方法都不会被调用,这样就防止了大面积的阻塞和资源占用的情况
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 模式(1)或并发线程数模式(0) | QPS 模式 |
limitApp | 流控针对的调用来源 | default ,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流 | 直接拒绝 |
clusterMode | 是否集群限流 | 否 |
-
阈值类型:根据上面的流控规则,设置控制台,如下主要设置的阈值类型为QPS为1,即每秒只允许一个请求,流控效果为快速失败
此时访问该接口超过1QPS后,就会出现下面的效果:
-
集群模式:选择集群模式后,流控规则会出现均摊阈值和集群阈值模式选择,集群阈值模式包括:
单机均摊
:即每个机器的流控阈值都为均摊阈值
的值总体阈值
:所有机器的流量加起来为均摊阈值
的值
-
流控模式:即对调用服务进行流量控制的模式,分为:
-
直接模式
:直接对输入的流控资源进行流量控制 -
关联模式
:选择关联模式后还需要输入关联资源
,关联资源是指两个资源之间具有资源争抢或者依赖关系的情况,如果放任两个资源竞争,则争抢本身带来的消耗也会降低整体的吞吐量,使用关联模式
就可以避免关联资源之间的过度争抢,因此可以给一个资源设置限流规则来达到使另一个资源优先的目的。(比如数据库的读和写操作) -
链路模式
:选择链路模式后还需要输入入口资源
,意思是只有通过入口资源调用想要流控的资源,才会使资源的流控生效,中途不经过入口资源对流控资源的调用或者其他链路的调用是不会生效的
-
-
流控效果:即流量控制之后的效果,包括:
- 快速失败:快速失败也是直接拒绝,是默认的流控效果,当QPS或者线程数超过规定的阈值,就对请求进行立刻拒绝,比较适合对系统的处理能力确切已知的情况
- Warm Up:即预热、冷启动方式,通过预热的方式,让通过的流量缓慢地增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,防止冷系统被流量压垮;需要指定一个
预热时长
,表示经过多久预热之后才到达峰值(上面设置的阈值) - 排队等待:低于阈值的请求被直接处理,高于的部分加入队列排队等待;需要设置一个
超时时间
,表示请求超过多久得不到处理就会失败
3.3 Sentinel 流控自定义响应
Sentinel的自定义响应有两种方式,一种是写一个BlockExceptionHandler
的实现类,另一个是在配置类的构造方法中写一下WebCallBackManager.setUrlBlockHandler()
的实现;后者新版Sentinel已经不支持了,所以这里使用老版:
/**
* Sentinel自定义限流返回信息
*/
public class SentinelUrlBlockHandler implements BlockExceptionHandler{
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
// 降级业务处理
R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS);
httpServletResponse.getWriter().write(JSON.toJSONString(error));
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
}
}
效果(乱码了不知道为啥。。):
3.4 实时监控
-
为了开启Sentinel控制台的实时监控效果,需要引入actuator审计依赖,用于统计服务运行的健康状态信息以及整个请求的调用信息等
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
然后暴露这些信息给Sentinel做实时监控
# 信息暴露 management: endpoints: web: exposure: include: *
4 Sentinel的熔断与降级
4.1 调用方的熔断保护
-
上面的流控都是针对请求进行的流量控制,而熔断则是针对服务之间的远程调用出现服务宕机的情况
-
使用Sentinel来保护Feign的远程调用,即熔断
-
Sentinel适配了Feign组件,需要进行下面的步骤:
-
首先引入sentinel依赖(前面引入了)和feign依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
开启feign的sentinel配置,使得sentinel能够得到远程调用的信息:
feign: sentinel: enabled: true
-
然后针对调用者的
feignService
添加熔断方法:@FeignClient(value = "grainmall-product", fallback = TestFeignServiceFallBack.class) public interface TestFeignService { @GetMapping("/product/test") R test(); }
-
熔断方法即实现该
feignService
接口的本地实现:@Service @Slf4j public class TestFeignServiceFallBack implements TestFeignService { @Override public R test() { log.info("熔断方法调用:TestFeignServiceFallBack"); return R.error(BizCodeEnum.TOO_MANY_REQUESTS); } }
-
如此一来,当调用远程方法的时候,如果远程服务宕机,sentinel就会熔断该调用,转而调用本地方法,避免服务调用失败
-
4.2 调用手动指定远程服务的熔断降级策略
-
远程服务被降级处理,Sentinel则默认触发熔断回调方法
-
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 状态)
,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
-
4.3 熔断降级规则
Field | 说明 | 默认值 |
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
4.4 服务提供方的降级
-
当出现超大浏览的时候,必须牺牲一些远程服务,在服务的提供方(远程服务)指定降级策略
-
这样提供方虽然在运行,但是针对降级的接口不会真正去运行业务逻辑而是返回降级的数据
-
这时候提供方降级后,接收方会自己去调用
BlockExceptionHandler
实现类执行降级业务(上面的SentinelUrlBlockHandler),不需要再去写降级的返回方法了@Component public class SentinelUrlBlockHandler implements BlockExceptionHandler{ @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { // 降级业务处理 R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS); httpServletResponse.getWriter().write(JSON.toJSONString(error)); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); } }
5 Sentinel自定义受保护资源
-
上面的方式主要是主流框架的默认适配(直接对请求进行流控),Sentinel对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等都做了适配,只需要直接引入然后定义流控规则即可
-
而对于普通的方法,则可以通过:
-
抛出异常的方式定义资源:
SphU
包含了 try-catch 风格的 API。用这种方式,当资源发生了限流之后会抛出BlockException
。这个时候可以捕捉异常,进行限流之后的逻辑处理。特别地,若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(
exit(count, args)
),否则可能会有统计错误。这个时候不能使用 try-with-resources 的方式。另外通过Tracer.trace(ex)
来统计异常信息时,由于 try-with-resources 语法中 catch 调用顺序的问题,会导致无法正确统计异常数,因此统计异常信息时也不能在 try-with-resources 的 catch 块中调用Tracer.trace(ex)
。SphU.entry()
的参数描述:参数名 类型 解释 默认值 entryType EntryType
资源调用的流量类型,是入口流量( EntryType.IN
)还是出口流量(EntryType.OUT
),注意系统规则只对 IN 生效EntryType.OUT
count int
本次资源调用请求的 token 数目 1 args Object[]
传入的参数,用于热点参数限流 无 注意:
SphU.entry(xxx)
需要与entry.exit()
方法成对出现,匹配调用,否则会导致调用链记录异常,抛出ErrorEntryFreeException
异常。常见的错误:- 自定义埋点只调用
SphU.entry()
,没有调用entry.exit()
- 顺序错误,比如:
entry1 -> entry2 -> exit1 -> exit2
,应该为entry1 -> entry2 -> exit2 -> exit1
如在下面代码添加异常定义保护资源后,
Sentinel
就能够检测到这个需要受保护的资源:@RestController @Slf4j public class TestController { // //@Autowired //TestFeignService testFeignService; @GetMapping("/test") public R test() { try(Entry entry = SphU.entry("testResource")) { } catch (BlockException e) { log.error("testResource资源被限流"); e.printStackTrace(); } //return testFeignService.test(); return R.ok(); } }
这时候以超过1QPS的速度访问该接口,就会出现:
2023-08-08 22:59:15.259 ERROR 5788 --- [nio-9000-exec-1] c.h.g.member.controller.TestController : testResource资源被限流 com.alibaba.csp.sentinel.slots.block.flow.FlowException
- 自定义埋点只调用
-
返回布尔值方式定义资源
-
注解方式定义资源:Sentinel 支持通过
@SentinelResource
注解定义资源并配置blockHandler
和fallback 函数
来进行限流之后的处理。@SentinelResource(value = "testResource", blockHandler = "blockHandler") @GetMapping("/test") public R test() { //return testFeignService.test(); return R.ok(); } /** * 堵塞方法,要求和保护资源的返回值和参数都相同 * @return */ public R blockHandler(BlockException e) { log.error("参数被限流了"); return R.error(BizCodeEnum.TOO_MANY_REQUESTS); }
注意
blockHandler
函数会在原方法被限流/降级/系统保护的时候调用,而fallback
函数会针对所有类型的异常。请注意blockHandler
和fallback
函数的形式要求,更多指引可以参见 Sentinel 注解支持文档。(使用fallback的时候指定的是fallbackClass
,并且其中的方法必须是静态方法) -
异步调用支持:
-
5.5 Sentinel的熔断、降级总结
-
Sentinel的熔断降级,都需要设置好熔断降级后的数据
-
熔断需要使用一个实例,实现feignService的远程方法,以返回熔断数据:
@Component public class SentinelUrlBlockHandler implements BlockExceptionHandler{ @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { // 降级业务处理 R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS); httpServletResponse.getWriter().write(JSON.toJSONString(error)); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); } }
-
而对于降级服务,如果保护资源是url请求,则Sentinel自动对其进行的配置,可以实现
BlockExceptionHandler
接口做降级数据统一返回/** * Sentinel自定义限流返回信息 */ @Component public class SentinelUrlBlockHandler implements BlockExceptionHandler{ @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { // 降级业务处理 R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS); httpServletResponse.getWriter().write(JSON.toJSONString(error)); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); } }
-
而如果降级的是自定义的资源(通过
try catch
或者@SentinelResource
注解进行的资源降级),则需要手动编写降级方法返回降级数据:@SentinelResource(value = "testResource", blockHandler = "blockHandler") @GetMapping("/test") public R test() { //return testFeignService.test(); return R.ok(); } /** * 堵塞方法,要求和保护资源的返回值和参数都相同 * @return */ public R blockHandler(BlockException e) { log.error("参数被限流了"); return R.error(BizCodeEnum.TOO_MANY_REQUESTS); }