目录
2.11 ParamFlowCheckSlot(热点参数,令牌桶)
2.12 FlowSlot(流控限流,滑动窗口--快速失败,warm-up;漏桶-排队等待)
一 Sentinel定义及代码整合
1.1 雪崩问题及解决方案
1.1.1 雪崩及解决方法
微服务架构中,D服务如果宕机,会影响调用它的服务A,再联动影响调用服务A的服务,使整个微服务收到影响。
解决雪崩问题的常见方式有四种:
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
- 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。将tomcat的池子进行划分。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
只有第四个是防止雪崩,前三个都是发生需崩后进行解决。
1.1.2 总结
什么是雪崩问题?
微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
如何避免因瞬间高并发流量而导致服务故障?
流量控制
如何避免因服务故障引起的雪崩问题?
超时处理 线程隔离 降级熔断
1.2 认识及代码整合Sentinel
1.2.1 Sentinel
Sentinel是基于信号量进行的隔离,信号量访问接口的次数。支持自定义扩展,有控制台页面进行实时监控。
1.2.2 启动及整合Sentinel
1.2.2.1 jar包下载,启动
前往官网下载jar包。Releases · alibaba/Sentinel · GitHub
启动命令:默认是8080端口,可自定义端口,用户名,密码
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8090
1.2.2.2 代码整合
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
然后请求controller接口就可以在页面上看见
1.3 限流规则
1.3.1 簇点链路
簇点链路:就是项目内的调用链路,链路中被监控的每个接口就是一个资源。我们代码中的每一个controller。默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。 流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
order/query是一个普通的方法调用;
/order/queryUserData/101用到了feign接口的调用;
这两个接口不在sentinel_default_context根目录下,是因为配置文件中添加了下面这个配置。
cloud:
sentinel:
web-context-unify: false #关闭context整合,controller接口就不属于sentinel_spring_web_context这个根目录下了
1.3.2 流控模式
流控模式包括直接,关联,链路限流三种。
1.3.2.1 直接限流
对这个资源(接口)增加限流规则,一秒内的QPS达到5,就限制剩下的请求,根据流控效果进行返回;加入一秒有10个请求,那么5个成功,剩下的5个将会失败。
下图代表一秒10个请求,结果5个成功,5个失败, 5个成功,5个失败。
1.3.2.2 关联限流
统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流。
使用场景:比如用户查询订单信息和生成订单信息,这两个操作都会争抢数据库锁,因此当修改订单业务触发阈值时,需要对查询订单业务限流。修改订单业务优先保证执行。
当write接口并发达到5时,对query接口限流,query接口直接返回失败,优先保证write接口执行。可以对write接口进行压测,并发超过5即可,然后调用query接口,这时,query接口会报错。
1.3.2.3 链路限流
只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。这种模式需要给资源添加一个注解,使其能注册在sentinel上。@SentinelResource ,加在下面场景中的货品信息查询service上。
有两个接口,查询订单和保存订单,都要查询货品信息,利用货品信息去返回或者保存到订单表;这两个接口保存接口需要优先执行,所以给查询订单方法入口进行限流,保证保存订单业务正常执行。
给这个货品查询接口资源添加限流规则。保存订单和查询订单在sentinel上是两个簇点链路,每一个下面都有这个queryGoodsData,任意选择一个去添加限流规则就行。
1.3.3 流控效果
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
1.3.3.1 warm up
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 threshold / coldFactor,持续指定时长后,逐渐提高到threshold值。而coldFactor的默认值是3. 例如,我设置QPS的threshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
服务刚启动时,各个微服务的调用第一次调用都比较慢,超时的概率较大。
1.3.3.2 排队等候
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝(排队也有一定的时间限制,队列后面的超过等待时候的,也是要拒绝的)。 例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常
1.3.4 热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
热点参数限流需要在左侧列表中的热点规则中去新增热点规则,上图表示,对query这个接口的第一个参数进行流控,第一个参数的值为101的,QPS限制为5;102的QPS限制为10,剩下的其他参数值,QPS为2。这种参数只测试了在请求路径的参数,未测试body的。
1.4 隔离和降级
流控都是防止服务崩溃,但是还有一些其他原因(网络,自然灾害)导致服务崩溃。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。都是对客户端(调用方)的保护。所以,就需要feign整合sentinel。微服务中,都是feign进行的远程调用。
1.4.1 feign整合sentinel
order服务去调用goods服务,保护order服务,在order服务中添加。
1:配置文件开启feign对sentinel的支持;
feign:
sentinel:
enabled: true
2:给FeignClient编写失败后的降级逻辑
方式一:FallbackClass,无法对远程调用的异常做处理
方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种,实现FallbackFactory接口。
package com.dj.cloudDemo.config;
import com.dj.cloudDemo.common.dto.ResultObject;
import com.dj.cloudDemo.goodsInfo.api.GoodsInfoFeignService;
import com.dj.cloudDemo.goodsInfo.dto.request.GoodsInfoRequest;
import com.dj.cloudDemo.goodsInfo.dto.response.GoodsInfoResponse;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
/**
* @author jingdj
* @version 1.0
* @date 2023/10/24 12:16 下午
* @desc
*/
@Slf4j
public class GoodsFeignFallBack implements FallbackFactory<GoodsInfoFeignService> {
@Override
public GoodsInfoFeignService create(Throwable throwable) {
return new GoodsInfoFeignService() {
@Override
public ResultObject<GoodsInfoResponse> getGoodsInfo(GoodsInfoRequest request) {
log.error("未查询到货品信息", throwable);
ResultObject<GoodsInfoResponse> resultObject = new ResultObject<>();
GoodsInfoResponse response = new GoodsInfoResponse();
resultObject.setResult(response);
return resultObject;
}
@Override
public ResultObject decrGoodsStockData(GoodsInfoRequest goodsInfoRequest) {
return null;
}
@Override
public ResultObject tryGoodsStock(GoodsInfoRequest goodsInfoRequest) {
return null;
}
@Override
public ResultObject deleteGoodsFreezeData(GoodsInfoRequest goodsInfoRequest) {
return null;
}
@Override
public ResultObject cancelGoodsFreezeData(GoodsInfoRequest goodsInfoRequest) {
return null;
}
};
}
}
3:将上面配置的fallbak注册到spring容器。
@Bean
public GoodsFeignFallBack getGoodsFeignFallBack(){
return new GoodsFeignFallBack();
}
然后在feign上添加这个fallback
@FeignClient(value = "goodsservice/goods/", fallbackFactory = GoodsFeignFallBack.class)
1.4.2 线程隔离
线程隔离,将服务1tomcat的线程池划分,不同的被调用服务有各自对应的线程池,这种对cpu消耗比较高,涉及到线程的来回切换。涉及线程,优点可以异步调用,主动超时;缺点对cpu开销大;适合低扇出的场景。(如上图,从服务1去调用它在一层的服务,服务较少的就是低扇出,多的就是高扇出(gateway))
信号量,一个请求代表一个信号量,执行完会减下去。这种简单,正好和线程隔离的优缺点相反。 适用于高扇出场景。
使用:在配置流控规则时,将QPS改成线程数即可。
1.4.3 熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
初始是closed状态,熔断器是关闭状态的,请求可以正常发送,在某段时间内,接口的失败率达到阀值,熔断器就会开启,熔断一定时间,截断访问该服务的一切请求,在熔断期间,所有过来的请求直接报错,不会到达该服务;当熔断时间结束时,熔断器会尝试放行一次请求,然后根据这次请求的结果去决定熔断器打开(这次请求成功)还是关闭(失败,则熔断器接着处于开启状态)。
熔断器有三种执行模式:慢调用、异常比例、异常数。
注意:异常比例和异常数是feign接口报500错误,不要用全局异常处理器进行拦截。
1.4.3.1 慢调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。熔断是给feign接口进行的熔断规则定义。
下图内容解析:RT超过50ms的调用是慢调用,统计最近1000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.4(出现起码4次慢调用),则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
1.4.3.2 异常比例和异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.5或者异常数不低于4的,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
1.5 授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。比如,我们只允许从网关过来的请求。在网关的配置文件添加AddRequestHeader,增加一个请求头参数。在order服务中增加一个配置,实现RequestOriginParser 类,对在网关中增加的参数进行判断。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
spring:
application:
name: gatewayservice
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=origin,gateway
order服务增加配置类
package com.dj.cloudDemo.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @author jingdj
* @version 1.0
* @date 2023/10/24 8:46 下午
* @desc
*/
@Component
public class HeadOriginParserConfig implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getHeader("origin");
if (StringUtils.isEmpty(origin)){
return "blank";
}
return origin;
}
}
不泄露这个origin的情况下,用户不能在请求头中加这个参数,就可以实现只有网关的转发请求才能调通服务。如果用户知道这个参数,加载请求头,就能正常访问了。
1.6 自定义异常结果
可以根据上面的五种类型定义异常结果。
package com.dj.cloudDemo.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author jingdj
* @version 1.0
* @date 2023/10/24 9:27 下午
* @desc sentinel自定义异常结果
*/
@Component
public class SentinelBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了!";
} else if (e instanceof DegradeException) {
msg = "请求被降级了!";
} else if (e instanceof ParamFlowException) {
msg = "热点参数限流!";
} else if (e instanceof AuthorityException) {
msg = "请求没有权限!";
status = 401;
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(status);
httpServletResponse.getWriter().println("{\"message\": \"" + msg + "\", \"status\": " + status + "}");
}
}
二 源码解析
Sentinel实现限流、隔离、降级、熔断等功能,本质要做的就是两件事情:
-
统计数据:统计某个资源的访问数据(QPS、RT等信息)
-
规则判断:判断限流规则、隔离规则、降级规则、熔断规则是否满足
这里的资源就是希望被Sentinel保护的业务,例如项目中定义的controller方法或者通过@SentinelResource就是默认被Sentinel保护的资源。其中,@SentinelResource注解是由我们引入的sentinel依赖中的切面实现的。
2.1 各种插槽定义
NodeSelectorSlot
负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,后续可以用于根据调用路径来限流降级(链路限流);ClusterBuilderSlot
则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;在树状结构中,节点A可以出现在不同的路径中,但是节点A的clusterNode只有一个;StatisticSlot
则用于记录、统计不同纬度的 runtime 指标监控信息;进行数据统计;FlowSlot
则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;AuthoritySlot
则根据配置的黑白名单和调用来源信息,来做黑白名单控制;DegradeSlot
则通过统计信息以及预设的规则,来做熔断降级;SystemSlot
则通过系统的状态,例如 load1 等,来控制总的入口流量;
2.2 Node(节点)
Sentinel中的簇点链路是由一个个的Node组成的,Node是一个接口,包括下面的实现:
所有的节点都可以记录对资源的访问统计数据,所以都是StatisticNode的子类。
按照作用分为两类Node:
-
DefaultNode:代表链路树中的每一个资源,一个资源出现在不同链路中时,会创建不同的DefaultNode节点。而树的入口节点叫EntranceNode,是一种特殊的DefaultNode
-
ClusterNode:代表资源,一个资源不管出现在多少链路中,只会有一个ClusterNode。记录的是当前资源被访问的所有统计数据之和。
DefaultNode记录的是资源在当前链路中的访问数据,用来实现基于链路模式的限流规则(根据请求的不同入口来进行限流)。ClusterNode记录的是资源在所有链路中的访问数据,实现默认模式、关联模式的限流规则(这个节点的数据合计)。
如上图所示,有两个controller入口,一个查询订单接口,需要去查询货品信息封装返回,一个保存订单信息,也需要查询货品信息去进行一些业务逻辑判断,queryGoodsService就会有两个链路,两个不同的defaultNode,但是只会有一个clusterNode去进行总计统计。上边的两个entranceNnode也会对应生成他们自己的clusterNode节点。
2.3 Entry(资源)
entry就是需要被保护的资源,controller入口就是默认的,然后也可以将代码通过下面这个方式包裹,下面的写法属于try--with--resource,省略了finally;也可以通过注解@SentinelResource实现,会由sentinel源码中的一个切面去实现。
2.4 context(上下文对象)
通过拦截器实现的创建;
-
Context 代表调用链路上下文,贯穿一次调用链路中的所有资源(
Entry
),基于ThreadLocal。 -
Context 维持着入口节点(
entranceNode
)、本次调用链路的 curNode(当前资源节点)、调用来源(origin
)等信息。 -
后续的Slot都可以通过Context拿到DefaultNode或者ClusterNode,从而获取统计数据,完成规则判断
-
Context初始化的过程中,会创建EntranceNode,contextName就是EntranceNode的名称
// 创建context,包含两个参数:context名称、 来源名称
ContextUtil.enter("contextName", "originName");
2.4.1 代码流程
最后会一直找到这个类AbstractSentinelInterceptor,它实现了一个拦截器接口,在preHandle方法中进行数据处理;resource就是我们的接口路径(order/query),origin会用来进行权限判断,ContextUtil.enter方法内部会创建一个context对象,这个方法中,先从threadlocal中获取context,刚进来都是空的,然后会去判断之前是否已经有线程将这个路径处理过了,处理过的会存放在map对象中,加锁前后进行两次判断,增加代码健壮性;然后,会根据请求的路径创建entranceNode入口节点,并且添加到根目录下,然后通过copy--on--wire将这个路径保存到map中,并且将当前这个context对象放到ThreadLocal中。
SphU.entry方法,里面会利用创建entry对象(之前的@SentinelResource注解里面也有这段代码,最后都会走到下面这段代码),并且将节点数据进行关联,构建簇点链路,然后找到对应的规则插槽,进行规则判断。
2.5 ProcessorSlot(链路构造)
lookProcessChain方法,里面会根据请求的路径寻找对应的链路,第一次进来,这个map是空的,所以就需要将进来的这个路径加入到这个map中,加入到map的这个操作利用了copy-on-write,方法结束会返回一个ProcessorSlot对象;
得到ProcessorSlot对象后,这个对象里面就是后续要挨个执行的规则任务。
第一个执行的插槽是NodeSelectorSlot,它负责去构建簇点链路中的节点,形成链路树,就是在页面上看见的簇点链路;然后去执行下一个节点的代码。这个Slot完成了这么几件事情:
-
为当前资源创建 DefaultNode
-
将DefaultNode放入缓存中,key是contextName,这样不同链路入口的请求,将会创建多个DefaultNode,相同链路则只有一个DefaultNode
-
将当前资源的DefaultNode设置为上一个资源的childNode
-
将当前资源的DefaultNode设置为Context中的curNode(当前节点)
2.6 ClusterNodeSlot(数据记录)
下一个执行的节点是clusterNodeSlot,这个插槽也是通过copy-on-write去给clusterNodeMap添加当前资源;这个负责构建某个资源的ClusterNode,这个不管后续有多少个链路,这个节点的clusterNode节点只会有一个。
2.7 Logslot(日志记录)
下一个插槽是 logSlot,如果这个插槽后续的一些插槽有异常抛出,它负责记录日志;
2.8 StasticSlot (统计)
下一个是statisticSlot,负责统计实时调用数据,包括运行信息(访问次数、线程数)、来源信息等。StatisticSlot是实现限流的关键,其中基于滑动时间窗口算法维护了计数器,统计进入某个资源的请求次数。
但是它不会直接去计数,而是先去执行其它插槽代码;
2.9 AuthoritySlot(授权)
下一个是AuthoritySlot,用来鉴权;如果我们配置了授权规则,就会执行;
2.10 SystemSlot(系统规则)
下一个是SystemSlot,会进行一些QPS,响应时间,CPU利用率等等进行判断。
2.11 ParamFlowCheckSlot(热点参数,令牌桶)
2.11.1 源码流程
接口及热点规则设置。第一次资源跑的是queryId的,第二次加载的才是自定义的hot,热点规则是设置在hot上的,所以在hot资源加载的时候才会进入热点规则控制的代码中。
总的规则阀值是5,但是这个接口中的参数1和2的阀值会根据他们自己的特定设置重新赋值。
下一个是ParamFlowCheckSlot,热点参数限流;判断当前资源是否有热点参数限流规则。测试时的方法必须有请求参数。
有热点参数规则配置了,进去校验方法;默认走的是passLocalCheck。然后进入passDefaultLocalCheck方法。
令牌桶实际上就是维护了一个计数器,根据这个计数器来进行规则判断;
- tokenConters:维护了当前资源规则的令牌数;
- timeCounters:维护了上一次请求的时间。
特定参数(1和2)的的限流阀值会根据自己的设置重新赋值,1和2之外的其他参数的阀值会按照rule的阀值进行流控。
如果阀值是0,直接返回false;反之,用阀值加上请求允许的波动值(这个默认是0,暂时也不允许修改,这个值在后续令牌生成时有用到) ,得到的就是当前规则允许的最大阀值;
如果请求需要的count>最大阀值,返回false;反之,获取当前时间,如果当前请求参数的对应的最后一次访问时间存在,就返回,反之就返回null;这个putIfAbsent已经将当前时间存进去了。
如果最后一次请求时间不存在,代表是第一次请求,放行,然后将当前请求参数的令牌数减一;(tokenCounter赋值)
最后一次请求时间不为null,代表不是第一次请求,需要判断当前请求与上一次请求是否为同一时间间隔内的请求;
如果是同一时间间隔的, 判断tokenCounter中剩余的令牌数是否能满足当前请求数的需要,不满足就返回false,反之,将tokenCounter的剩余令牌数减去请求数;
如果不是同一时间间隔内的,获取tokenCounter的剩余数,并且将最新的数存进map,如果剩余数返回null,代表之前的令牌数用完了,直接放行,并且设置最近访问时间;
剩余数不是空, 获取这个时间段内应该生成多少令牌toAddCount,通过时间除以时间间隔,再乘以阀值;
如果生成的令牌数加上之前剩余的令牌数大于最大阀值maxCount,令牌数就用最大阀值减去此次请求消耗的数量,反之就用生成的令牌数加上之前剩余的令牌数再减去当前请求消耗的数量;
这个请求默认都是采用的最大阀值减去此次请求消耗的数量,因为maxCount是由阀值加上波动值,这个波动值是0,暂时没有配置去修改它,所以只要超过了设置的时间,计算出的要生成的令牌数加上此次请求数一定大于maxCount;
特定情况下,加入这个波动值可以配置了,就会存在maxCount大于阀值的情况,就会出现请求波动,超过阀值的场景。例如,阀值是5,波动值是15,超过时间为4秒,计算出的需生成的令牌数就是:4/1*5=20,20+1> maxCount,(空在上面判断了,这里加上任意数都会大于maxCount),tokenCounters就会取(剩余数 + 需要生成的令牌数 - 此次请求数)。
2.11.2 流程图
2.12 FlowSlot(流控限流,滑动窗口--快速失败,warm-up;漏桶-排队等待)
下一个是限流插槽,会根据页面配置的限流方式进行流控,选择对应的实现类。
流控规则需要根据各自的流控模式选择对应的算法;快速失败(DefaultController),warm-up采用的滑动窗口(RateLimiterController);排队等候采用的是漏桶模式(WarmUpController)。
三种流控模式,从底层数据统计角度,分为两类:
-
对进入资源的所有请求(ClusterNode)做限流统计:直接模式、关联模式
-
对进入资源的部分链路(DefaultNode)做限流统计:链路模式
根据不同的流控模式选择不同的node,只有链路模式比较特殊,它需要根据每一个不同的链路的不同请求数进行流控。
三种流控效果,从限流算法来看,分为两类:
-
滑动时间窗口算法:快速失败、warm up
-
漏桶算法:排队等待效果
2.12.1 滑动窗口算法
一个在1250毫秒的请求,时间窗口是1秒,这个就是按秒进行的限流,1250-1000=250,在第一区间,但是应该从它的下一个区间进行统计,也就是从500到1500的这两个区间算。
滑动窗口采用的是环形数组的存储方式,环形,就会出现新值覆盖旧值的现象,我们的统计时间周期一般都是一秒中,覆盖了多秒之前的数据也没影响。
2.12.2 快速失败和预热模式
获取当前节点的已通过数量,加上本次请求需要的量,小于页面设置的阀值,就放行,反之就返回false。所以这个关键就是获取当前已通过的数量。
2.12.2.1 窗口请求数累加
从上面的代码可以看出,关键的是获取当前节点通过的QPS数。 QPS的统计是放StatisticSlot插槽下统计的;addPassRequest,会对statisticNode和defaultNode都自增。
从下图看出,关键是找出当前窗口,找出后增加请求数即可;
根据传入的当前时间,算出当前属于数组中的哪一个索引;
- intervalInMs:默认是1秒;
- sampleCount:每秒要划分的区间,默认是2,这个可以修改;
当前时间除以每一个小窗口的时间间隔(1/2),按照默认的走,一般都是500ms一个小区间;可以得到一共可以划分成多少个小区间,再对16取余,得到的永远都是0--15,就是我们需要的索引下标;
例如:时间是16000,除以500得到30,再对16取余,得14;
然后通过calculateWindowStart方法,获取其对应区间的开始时间;
先根据角标获取数组中保存的 oldWindow 对象,可能是旧数据,需要判断.。
- oldWindow 不存在, 说明是第一次,创建新 window并存入,然后返回即可
- oldWindow的 starTime = 本次请求的 windowStar, 说明正是要找的窗口,直接返回.
- oldWindow的 starTime < 本次请求的 windowStar, 说明是旧数据,需要被覆盖,创建新窗口,覆盖旧窗口
得到对应的窗口后,就可以进行add操作。
2.12.2.2 获取请求QPS
获取QPS,需要在StatisticNode中获取;
获取到符合要求的窗口区间list(一般上是2,这个可以修改),然后遍历求和即可;这个list从this.data.values()获取;
获取符合要求的区间窗口;获取array的size(16),然后开始遍历循环;
判断出符合条件的区间,一个在1250毫秒的请求,时间窗口是1秒,这个就是按秒进行的限流,1250-1000=250,在第一区间,但是应该从它的下一个区间进行统计,也就是从500到1500的这两个区间算。
2.12.3 漏桶算法
排队等待模式主要的就是两个参数,阀值和超时时间。漏桶算法允许突发请求暴增,它只是给请求添加延时时间,类似于实现了队列的先进先出,通过的请求是一条平滑的直线。
当前请求数小于0,返回false;
当前规则的阀值小于0,返回false;
costTime:每次请求中间的时间差,例如:阀值是5,时间间隔就是200;
expectedTime:预期等待时间,lastpassedTime初始化是-1;第一次等待时间就是199;
预期等待时间小于等于当前时间,第一次请求,刷新最后一次请求时间,放行;反之,非第一次请求,等待时间间隔+最后一次通行时间-当前时间,得到需要等待的时间,如果这个等待时间超过了设置了队列最大等待时间,返回false,反之就设置线程等待时间。
2.13 DegradeSlot(熔断降级)
下一个是熔断降级,如果状态是关闭的,直接放行,状态是非开启的(半开half-open),返回失败;剩下的就是open。
如果当前时间大于再一次的尝试时间,熔断时间结束,表明可以放行一次请求,去尝试,成功就将状态切换成closed,反之就是open;
会通过cas,将open切换成half-open,能切换,然后会生成一个观察者,看看是否有在熔断规则下从open切换成half-open的;然后就是得到资源,给资源设置监听器,在资源Entry销毁时(资源业务执行完毕时)触发,判断资源业务是否异常,有,则再次进入OPEN状态。
这里出现了从OPEN到HALF_OPEN、从HALF_OPEN到OPEN的变化,但是还有几个没有:
-
从CLOSED到OPEN
-
从HALF_OPEN到CLOSED
接下来会进到exit方法,有异常,会进入next的exit方法,反之,就会进入onRequestComplete方法,这个方法有两个实现类,对应异常比例和慢调用;
1.13.1 异常数和异常比例
异常比例:基于滑动窗口实现,拿到当前窗口的请求数;
当前状态非open,如果是half-open,并且放行的这次请求的结果没有失败,状态就从half-open切换到close;反之就切换到open状态。
如果当前状态不是half-open,那就只能是close了;达到异常数后,触发熔断。
strategy=1,异常比例;strategy默认是0,异常数。
2.13.2 慢响应
根据页面上设置的最小请求时间和异常比例,响应时间和最少请求数计算。
状态切换和异常数的差不多;
当前状态非open,如果是half-open,并且放行的这次请求的响应时间没有超过页面设置的时间,状态就从half-open切换到open;反之就切换到closed状态。
如果当前状态不是half-open,那就只能是close了;当其在指定时间内的触发慢响应的请求达到阀值,就触发熔断。
判完之后defaultNode和StatisticSlot,ClusterNode的线程数和通过的请求数自增1;