分布式系统面临的问题:复杂的体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败,比如如下的例子中,当我们调用A、E、F、J、K这几个服务的时候如果其中一个服务出现问题会造成什么问题?其实就会出现整体系统效率全部下降,而且严重就会出现服务雪崩的问题!
多个微服务之间调用的时候,假设A调用B和C,B和C又调用其他的微服务,这就是所谓的扇出。如果扇出的某个链路上某个微服务调用的响应时间过程或者不可用,微服务A的调用就用占用越来越多的系统资源,从而引起系统崩溃,这也就是服务雪崩。其实就是服务的高可用遭到了破坏。
对于高流量的应用来说,单一的后端依赖可能会导致服务器上的所有资源都在几秒钟内饱和。同时还有可能造成这些应用程序导致服务之间的延迟增加,备份列队,线程和其他的系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系失败,不能取消整个应用程序或系统,所以通常发生了一个模块的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用其他的模块,这样就会发生级联故障,或者叫做雪崩。
要解决这种问题的出现我们就需要用到服务降级,而Sentinel就可以保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,提高分布式系统的弹性。
断路器:它本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法抛出的异常,这样就保证了服务调用方的不会被长时间、不必要的占用,从而避免了故障在分布式系统中蔓延(类似于病毒传染),从而避免了故障在系统中蔓延,乃至崩溃。
对比与其他的产品而言,比如说Hystrix,他不需要我们自己手动搭建监控平台,而且它有一套类似于Nacos的Web界面,可以让我们进行更加细粒度的配置流控、速率、服务熔断、服务降级等
目前主流编程都是 约定>配置>代码,虽然我们的配置都可以写在代码中,但是我们还是要大面积的学习配置和注解的方式,尽量少些代码,这也是Sentinel的理念和初衷。
可以选择下载jar包,也可以下载源码包进行编译
下载地址:https://github.com/alibaba/Sentinel/releases
官方提供的手册:Spring Cloud 阿里巴巴参考文档 (spring-cloud-alibaba-group.github.io)
注意:Sentinel控制台默认端口8080,保证8080端口不会被其他服务占用
首次访问web页面是空白的。因为sentinel默认是懒加载的方式,我们手动访问某个接口,web控制台就会出现信息。
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
server:
port: 8042
spring:
application:
name: cloud-sentinel-8042
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
# 配置Sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口,键入被占用会自动从8719+1,直到找到未被占用的端口
port: 8719
web-context-unify: false
management:
endpoint:
web:
exposure:
include: '*'
解释:我们现在单机阈值设定为1,代表的是当前这个接口只能被1秒访问一次,超过这个阈值,就会被Sentinel阻塞,现在默认为直接失败,会在浏览器直接抛出异常
QPS和并发线程数规则详解
让一个线程再进来办理的时候需要0.8秒,但是这个时候后面的线程也在疯狂的访问,所以后面的线程就会不会生效。
@GetMapping("/testA")
public String testA(){
try {
TimeUnit.MILLISECONDS.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "testA";
}
通过开启两个访问窗口,快速切换实现两个线程同时访问,这时候直接阻塞限流
注意:这个时候虽然效果一致,但是是两种完全不同的规则,一种是QPS,一种是并发线程!
当关联的资源达到阈值时,就限流自己。
比如拿我们的程序,现在有testA接口和testB接口,当A关联的资源B达到阈值后,就限流自己,也就是B到达阈值,限流A本身
当关联资源/testB的qps阈值超时1时,就限流/testA**的Rest访问地址,当关联资源到阈值后限制配置好的资源名
这时候我们利用postman工具,模拟20个线程间隔0.3毫秒去请求testB接口。
然后这时候我们在去浏览器访问testA 接口,此时就会限流
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流,它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
比如在一个微服务中,两个接口都调用了同一个Service中的方法,并且该方法用SentinelResource(用于定义资源)注解标注了,然后对该注解标注的资源(方法)进行配置,则可以选择链路模式。
@Service
public class TestService {
// 定义限流资源
@SentinelResource("common")
public String common(){
return "common";
}
}
controller
@GetMapping("/testC")
public String testC(){
return testService.common();
}
@GetMapping("/testD")
public String testD(){
return testService.common();
}
这里要注意不要对/testA或者/testB进行限流规则的配置,要给用SentinelResource注解标注的资源进行配置限流规则,这里的意思为当我们用入口资源访问被SentinelResource注解标注的资源方法时,当超过阈值就会被限流。
Warm Up方式,即预热/冷启动方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
预热公式:阈值/coldFactor(默认值为3),经过预热时间后才会达到阈值
冷启动的过程系统允许通过的QPS曲线如下图:
默认coldFactor为3,当发起请求即请求QPS从(阈值/3)开始,经过多长预热时长才逐步升至设定的QPS阈值,当前阈值设置为10,预热时长设置为5秒。
设置流控规则
最终的效果,系统初始化时阈值/3约等于3,即阈值在此时为3,经过5秒后阈值才慢慢升高到10
匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式。这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
中心思想:
以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过。否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
Sentinel 匀速排队等待策略是漏桶算法结合虚拟队列等待机制实现的。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
可以通过压测工具模拟大量请求验证效果。10个请求匀速通过
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
慢调用比例 (SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例 (ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%
异常数 (ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
==注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException
)
熔断状态有三种状态,非别为OPEN、HALF_OPEN、CLOSED
状态 | 说明 |
---|---|
OPEN | 表示熔断开启,拒绝所有请求 |
HALF_OPEN | 探测恢复状态,如果接下来的一个请求顺利通过则表示结束熔断,否则继续熔断 |
CLOSE | 表示熔断关闭,请求顺利通过 |
熔断降级规则包含下面几个重要的属性:
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 引入) |
官方文档网址:https://sentinelguard.io/zh-cn/docs/circuit-breaking.html
概念:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
简单理解: 1QPS(1秒钟的请求) 比如说1秒钟请求了6次,大于设置的最小请求数(5),并且设置的最大响应时间大于设置的比例阈值,那么这个请求就会被熔断。接下来在设置的这个熔断时长的范围内,所有请求继续熔断。当过了熔断时长后,如果在来一个请求,响应时长大于RT那么继续熔断,否则放行通过
@GetMapping("/testC")
public String testC(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "--testC";
}
设置熔断策略,1QPS>5 并且这些请求的RT>300 并且大于比例阈值触发熔断
通过JMeter测试,1秒钟发起10个线程请求/testC,此时就会触发熔断效果,停止测试以后,10秒钟以后恢复正常
异常比例 (ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException
)不生效。
简单理解就是:
@GetMapping("/testC")
public String testC(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "--testC";
}
灵JMeter压测工具,一秒钟发送10个请求,启动后就会触发熔断
异常数 (ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
注意:异常降级仅针对业务异常,对Sentinel限流降级本身的异常(BlockException
)不生效。
@GetMapping("/testD")
public String testD(Long id ){
if(id > 5){
throw new RuntimeException("运行时异常");
}
return testService.common();
}
设置异常数策略,当1秒钟内请求超过5并且异常数大约5个的时候触发熔断
浏览器验证触发熔断异常
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效
官网:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html
其实这个热点限流其实就是更加细粒度的流控规则,那么如果想使用它就必须要配合对应SentinelResource注解。
Sentinel 提供了 @SentinelResource 注解用于定义资源,它有很多的参数,我们这里主要关注两个参数:
value:代表资源名称,必需项,因为需要通过resource name找到对应的规则,这个是必须配置的
blockHandler:blockHandler 对应处理 BlockException 的方法名称,可选项,访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
@GetMapping("/testHotKey")
@SentinelResource("testHotKey")
public String testE(@RequestParam(value = "hot1",required = false) String hot1,
@RequestParam(value = "hot2",required = false)String hot2,
@RequestParam(value = "hot13",required = false) String hot3){
return "----testHotKey";
}
配置热点规则
注意:这里是给资源配置热点规则,不是接口名称
这个参数索引,指的就是我们参数下标
浏览器快速访问一秒多个请求,就会出现限流
刚刚浏览器返回错误页面,对用户不友好。可以利用@SentinelResource注解提供的另外一个参数blockHandler,这个参数是可以指定当出现异常时的处理方法
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "handler_HotKey")
public String testE(@RequestParam(value = "hot1",required = false) String hot1,
@RequestParam(value = "hot2",required = false)String hot2,
@RequestParam(value = "hot13",required = false) String hot3){
return "----testHotKey";
}
//处理异常方法,方法签名要和对应的接口方法保持一致
public String handler_HotKey(String hot1, String hot2, String hot3, BlockException exception){
return "系统繁忙稍后重试。。";
}
注意:blockHandler = “handler_HotKey” 要保证和接口名称的返回类型,方法签名保持一致,并且在后面多增加一个参数BlockException
其实参数例外项就是可以达到更加细粒度的控制,比如我们当前的例子中,目前hot1参数在访问时超过阈值就会被限流,但是我们可以通过参数例外项设置hot1具体等于特殊的某个值的时候,触发不同的限流效果。假如hot1的值等于5时,它的阈值可以达到200。
**注意:参数例外项中的参数类型仅支持一下7种数据类型
当前我们需要让hot1的值为5的时候阈值可以达到200,首先Sentinel页面中修改对应热点规则
此时的规则为:如果当前hot1值为除5以外的其他值,都会走普通的阈值规则,但是如果一旦hot1的值为5的时候,将会走参数例外项,此时的阈值为200,我们通过浏览器测试,当hot1的值等于5是只要阈值不超过200就不会出现限流。
注意:我们到现在代码中使用了@SentinelResource注解,此注解处理的是Sentinel控制台配置的异常,通过blockHandler属性设置对应方法来处理和程序本身异常无关。如果是程序异常还是会报程序异常
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量,比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
maxQps(秒级统计的最大QPS) * minRt(秒级统计的最小响应时间)
估算得出。设定参考值一般是 CPU cores * 2.5
。入口QPS来进行测试,直接设置规则
最后我们不管访问那个接口都会出现限流
Sentinel 提供了@SentinelResource注解用于定义资源,并提供了AspectJ的扩展用于自定义资源,处理BlockException等。
编写一个新的控制器类型SentinelResourceTestController,使用@SentinelResource注解同时使用blockHandler属性
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handler_resource")
public String byResource(){
return "-----byResource";
}
public String handler_resource(BlockException exception){
return "系统繁忙";
}
配置流控规则
快速访问浏览器,触发限流规则
核心点:使用@SentinelResource注解,但是不使用blockHandler属性,系统会调用默认限流异常处理方法。
其实这个注解,我们还可以更换请求地址为资源,比如我们在新建一个测试接口方法
@GetMapping("/byRest")
@SentinelResource(value = "/byRest")
public String byRest(){
return "-----byRest";
}
给这个接口地址添加流控
如果没有这个接口增加自定义限流处理方法,则还是会走默认的
由此我们得出:
@SentinelResource 既可以配置资源名称也可以配置URL
如果配置了@SentinelResource的blockHandler属性对应方法,出现限流会调用对应方法
如果没有配置@SentinelResource的blockHandler属性,系统会走默认的限流处理。
@SentinelResource除了blockHandler可以设置自定义限流处理逻辑方法以外,还提供另外一个属性来设置限流处理逻辑类型blockHandlerClass属性,此属性中设置的方法必需为 static 函数,否则无法解析。
这样就不用把一个自定义处理限流的方法写在一个类里面,解耦出来
/**
* 此类型用来处理限流自定义逻辑
*/
public class CustomerBlockHandler {
public static String handlerException1(BlockException exception){
return "handlerException1:系统异常,请稍后重试!";
}
public static String handlerException2(BlockException exception){
return "handlerException2:网络崩溃了,请稍后重试!";
}
}
/**
* 此方法用到了自定义限流处理类型CustomerBlockHandler
* 中的handlerException1方法来处理限流逻辑。
*/
@GetMapping("/bycustomer")
@SentinelResource(value = "bycustomer",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException1")
public String bycustomer(){
return "-----bycustomer";
}
给bycustomer资源添加限流规则,然后来测试在超过限流阈值时处理方法是否为CustomerBlockHandler中handlerException1来进行处理。
fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
Throwable
类型的参数用于接收对应的异常。fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。**注意:fallback属性和blockHandler属性的本质不同在于他们作用的异常不同:
其实fallback和blockHandler的用法基本一样。可以在当前方法的类中,也可以通过fallbackClass指定某一个类中去写异常的处理
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。基本用法如下所示:
@SentinelResource(value = "falllback",fallback = "fallbackHandler",blockHandler = "blockHandler", exceptionsToIgnore = {NullPointerException.class})//被标注的异常将会被 原样抛出
如果对你有帮助,可以关注博主(不定期更新各种技术文档)
给博主一个免费的点赞以示鼓励,谢谢 ! 欢迎各位点赞评论收藏⭐️