文章目录
思考
1、当服务访问量达到一定程度,流量扛不住的时候,该如何处理?
2、服务之间相互依赖,当服务A出现响应时间过长,影响到服务B的响应,进而产生连锁反应,直至影响整个依赖链上的所有服务,该如何处理?
这是分布式、微服务开发不可避免的问题。
分布式系统遇到的问题
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务

调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩

服务雪崩效应:因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,就叫服务雪崩效应
导致服务不可用的原因: 程序Bug,大流量请求,硬件故障,缓存击穿
- 【大流量请求】:在秒杀和大促开始前,如果准备不充分,瞬间大量请求会造成服务提供者的不可用。
- 【硬件故障】:可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问。
- 【缓存击穿】:一般发生在缓存应用重启, 缓存失效时高并发,所有缓存被清空时,以及短时间内大量缓存失效时。大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用。
在服务提供者不可用的时候,会出现大量重试的情况:用户重试、代码逻辑重试,这些重试最终导致:进一步加大请求流量。所以归根结底导致雪崩效应的最根本原因是:大量请求线程同步等待造成的资源耗尽。当服务调用者使用同步调用时, 会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了。
解决方案
超时机制
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题。
服务限流(资源隔离)
限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题,对于一些出问题的服务可以限制流量访问,只分配固定线程资源访问,这样能使整体的资源不至于被出问题的服务耗尽,进而整个系统雪崩。那么服务之间怎么限流,怎么资源隔离?例如可以通过线程池+队列的方式,通过信号量的方式。
如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用。

服务熔断
远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断。
现实世界的断路器大家肯定都很了解,断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸,从而防止电路被烧毁。
软件世界的断路器可以这样理解:实时监测应用,如果发现在一定时间内失败次数/失败率达到一定阈值,就“跳闸”,断路器打开——此时,请求直接返回,而不去调用原本调用的逻辑。跳闸一段时间后(例如10秒),断路器会进入半开状态,这是一个瞬间态,此时允许一次请求调用该调的逻辑,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——通过”跳闸“,应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的“自我修复“。
所以,同样的道理,当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源。比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个依赖了,这个时候就应该使用断路器避免资源浪费。

服务降级
有服务熔断,必然要有服务降级。
所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据) 。这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。
Sentinel: 分布式系统的流量防卫兵
Sentinel 是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
源码地址:https://github.com/alibaba/Sentinel
官方文档:https://github.com/alibaba/Sentinel/wiki
Sentinel具有以下特征:
- 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。
阿里云提供了 企业级的 Sentinel 服务,应用高可用服务 AHAS

Sentinel和Hystrix对比
https://github.com/alibaba/Sentinel/wiki/Sentinel-%E4%B8%8E-Hystrix-%E7%9A%84%E5%AF%B9%E6%AF%94

Sentinel 工作原理
基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel工作主流程
https://github.com/alibaba/Sentinel/wiki/Sentinel%E5%B7%A5%E4%BD%9C%E4%B8%BB%E6%B5%81%E7%A8%8B
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

Sentinel快速开始
在官方文档中,定义的Sentinel进行资源保护的几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
代码示例:
Entry entry = null;
// 务必保证 finally 会被执行
try {
// 资源名可使用任意有业务语义的字符串 开启资源的保护
entry = SphU.entry("自定义资源名");
// 被保护的业务逻辑 method
// do something...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级 Sentinel定义异常 流控规则,降级规则,热点参数规则。。。。 服务降级(降级规则)
// 进行相应的处理操作
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常 RuntimeException 服务降级 mock feign:fallback
Tracer.traceEntry(ex, entry);
} finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
Sentinel资源保护的方式
API实现
引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>
编写测试逻辑
@RestController
@Slf4j
public class HelloController {
private static final String RESOURCE_NAME = "hello";
@RequestMapping(value = "/hello")
public String hello() {
Entry entry = null;
try {
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
entry = SphU.entry(RESOURCE_NAME);
// 被保护的业务逻辑
String str = "hello world";
log.info("====="+str);
return str;
} catch (BlockException e1) {
// 资源访问阻止,被限流或被降级
//进行相应的处理操作
log.info("block!");
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}
/**
* 定义流控规则
*/
@PostConstruct
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置受保护的资源
rule.setResource(RESOURCE_NAME);
// 设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值
// Set limit QPS to 20.
rule.setCount(1);
rules.add(rule);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}
测试效果:

缺点:
- 业务侵入性很强,需要在controller中写入非业务代码.
- 配置不灵活 若需要添加新的受保护资源 需要手动添加 init方法来添加流控规则
@SentinelResource注解实现
@SentinelResource 注解用来标识资源是否被限流、降级。
- blockHandler: 定义当资源内部发生了BlockException应该进入的方法(捕获的是Sentinel定义的异常)
- fallback: 定义的是资源内部发生了Throwable应该进入的方法
- exceptionsToIgnore:配置fallback可以忽略的异常
源码入口:com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
1.引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.0</version>
</dependency>
2.配置切面支持
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3.UserController中编写测试逻辑,添加@SentinelResource,并配置blockHandler和fallback
@RequestMapping(value = "/findOrderByUserId/{id}")
@SentinelResource(value = "findOrderByUserId",
fallback = "fallback",fallbackClass = ExceptionUtil.class,
blockHandler = "handleException",blockHandlerClass = ExceptionUtil.class
)
public R findOrderByUserId(@PathVariable("id") Integer id) {
//ribbon实现
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
if(id==4){
throw new IllegalArgumentException("非法参数异常");
}
return result;
}
4.编写ExceptionUtil,注意如果指定了class,方法必须是static方法
public class ExceptionUtil {
public static R fallback(Integer id,Throwable e){
return R.error(-2,"===被异常降级啦===");
}
public static R handleException(Integer id, BlockException e){
return R.error(-2,"===被限流啦===");
}
}
5.流控规则设置可以通过Sentinel dashboard配置
客户端需要引入 Transport 模块来与 Sentinel 控制台进行通信。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.0</version>
</dependency>
6.启动 Sentinel 控制台
链接:https://pan.baidu.com/s/1IZn9OADEgAcUisUKP72CYw?pwd=2022
提取码:2022
下载控制台 jar 包并在本地启动
#启动控制台命令
java -jar sentinel-dashboard-1.8.0.jar
用户可以通过如下参数进行配置:
用于指定控制台的登录用户名为 sentinel;用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel;用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;
-Dsentinel.dashboard.auth.username=sentinel
-Dsentinel.dashboard.auth.password=123456
-Dserver.servlet.session.timeout=7200
访问http://localhost:8080/#/login ,默认用户名密码: sentinel/sentinel

Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包,所以要确保客户端有访问量;
实时监控:监控接口的通过的QPS和拒绝的QPS
簇点链路:用来显示微服务的所监控的API

流控规则
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。 ==== FlowRule RT(响应时间) 1/0.2s =5
同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果。
参考文档: https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
限流阈值类型
流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由 FlowRule 的 grade 字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。
QPS(Query Per Second):每秒请求数,就是说服务器在一秒的时间内处理了多少个请求。
QPS:进入簇点链路选择具体的访问的API,然后点击流控按钮
测试:http://localhost:8800/user/findOrderByUserId/1

BlockException异常统一处理
springwebmvc接口资源限流入口在HandlerInterceptor的实现类AbstractSentinelInterceptor的preHandle方法中,对异常的处理是BlockExceptionHandler的实现类。sentinel 1.7.1 引入了sentinel-spring-webmvc-adapter.jar。自定义BlockExceptionHandler 的实现类统一处理BlockException。
@Slf4j
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e)
throws Exception {
log.info("BlockExceptionHandler BlockException================"+e.getRule());
R r = null;
if (e instanceof FlowException) {
r = R.error(100,"接口限流了");
} else if (e instanceof DegradeException) {
r = R.error(101,"服务降级了");
} else if (e instanceof ParamFlowException) {
r = R.error(102,"热点参数限流了");
} else if (e instanceof SystemBlockException) {
r = R.error(103,"触发系统保护规则了");
} else if (e instanceof AuthorityException) {
r = R.error(104,"授权规则不通过");
}
//返回json数据
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(), r);
}
}
测试:

并发线程数
并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
可以利用jmeter测试

流控模式
基于调用关系的流量控制。调用关系包括调用方、被调用方;一个方法可能会调用其它方法,形成一个调用链路的层次关系。
直接
资源调用达到设置的阈值后直接被流控抛出异常

关联
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

链路
根据调用链路入口限流。
NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:

上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。
测试会发现链路规则不生效
注意,高版本此功能直接使用不生效,如何解决?
从1.6.3版本开始,Sentinel Web filter默认收敛所有URL的入口context,导致链路限流不生效。
从1.7.0版本开始,官方在CommonFilter引入了WEB_CONTEXT_UNIFY参数,用于控制是否收敛context,将其配置为false即可根据不同的URL进行链路限流。
1.8.0 需要引入sentinel-web-servlet依赖
<!--- 解决流控链路不生效的问题-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
</dependency>
添加配置类,配置CommonFilter过滤器,指定WEB_CONTEXT_UNIFY=false,禁止收敛URL的入口context
@Configuration
public class SentinelConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合 解决流控链路不生效的问题
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
再次测试链路规则,链路规则生效,但是出现异常
控制台打印FlowException异常
原因分析:
- Sentinel流控规则的处理核心是 FlowSlot, 对getUser资源进行了限流保护,当请求QPS超过阈值2的时候,就会触发流控规则抛出FlowException异常
- 对getUser资源保护的方式是@SentinelResource注解模式,会在对应的SentinelResourceAspect切面逻辑中处理BlockException类型的FlowException异常
(解决方案: 在@SentinelResource注解中指定blockHandler处理BlockException)
// UserServiceImpl.java
@Override
@SentinelResource(value = "getUser",blockHandler = "handleException")
public UserEntity getUser(int id){
UserEntity user = baseMapper.selectById(id);
return user;
}
public UserEntity handleException(int id, BlockException ex) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("===被限流降级啦===");
return userEntity;
}
如果此过程没有处理FlowException, AOP就会对异常进行处理,核心代码在CglibAopProxy.CglibMethodInvocation#proceed中,抛出UndeclaredThrowableException异常,属于RuntimeException
3.异常继续向上抛出,引入CommonFilter后,CommonFilter添加了对异常的处理机制,所以会在CommonFilter中进行处理。
(注意此处对BlockException异常的处理是UrlBlaockHandler的实现类,而在AbstractSentinelInterceptor拦截器中是使用BlockExceptionHandler的实现类处理)
会抛出一个RuntimeException类型的UndeclaredThrowableException异常,然后打印到控制台显示
此处又是有坑: FlowException不会被BlockException异常机制处理,因为FlowException已经被封装为RuntimeException类型的UndeclaredThrowableException异常
测试:
自定义CommonFilter对BlockException异常处理逻辑,用于处理经过CommonFilter处理的spring webmvc接口的BlockException
// SentinelConfig.java
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合 解决流控链路不生效的问题
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
//CommonFilter的BlockException自定义处理逻辑
WebCallbackManager.setUrlBlockHandler(new MyUrlBlockHandler());
return registration;
}
// UrlBlockHandler的实现类
@Slf4j
public class MyUrlBlockHandler implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e)
throws IOException {
log.info("UrlBlockHandler BlockException================"+e.getRule());
R r = null;
if (e instanceof FlowException) {
r = R.error(100,"接口限流了");
} else if (e instanceof DegradeException) {
r = R.error(101,"服务降级了");
} else if (e instanceof ParamFlowException) {
r = R.error(102,"热点参数限流了");
} else if (e instanceof SystemBlockException) {
r = R.error(103,"触发系统保护规则了");
} else if (e instanceof AuthorityException) {
r = R.error(104,"授权规则不通过");
}
//返回json数据
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(), r);
}
}
测试,此场景拦截不到BlockException,对应@SentinelResource指定的资源必须在@SentinelResource注解中指定blockHandler处理BlockException
总结: 为了解决链路规则引入ComonFilter的方式,除了此处问题,还会导致更多的问题,不建议使用ComonFilter的方式。 流控链路模式的问题等待官方后续修复,或者使用AHAS。
流控效果
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:快速失败(直接拒绝)、Warm Up(预热)、匀速排队(排队等待)。对应 FlowRule 中的 controlBehavior 字段。

快速失败
(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
冷加载因子: codeFactor 默认是3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示

测试用例
@RequestMapping("/test")
public String test() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "========test()========";
}
编辑流控规则
jmeter测试

查看实时监控,可以看到通过QPS存在缓慢增加的过程

匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
该方式的作用如下图所示:

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

jemeter压测

查看实时监控,可以看到通过QPS为5,体现了匀速排队效果

降级规则
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
熔断降级规则(DegradeRule)包含下面几个重要的属性:


熔断策略
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
测试用例
@RequestMapping("/test")
public String test() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "========test()========";
}
jemeter压测/test接口,保证每秒请求数超过配置的最小请求数

查看实时监控,可以看到断路器熔断效果
此时浏览器访问会出现服务降级结果

异常比例
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
测试用例
@RequestMapping("/test2")
public String test2() {
atomicInteger.getAndIncrement();
if (atomicInteger.get() % 2 == 0){
//模拟异常和异常比率
int i = 1/0;
}
return "========test2()========";
}
配置降级规则
查看实时监控,可以看到断路器熔断效果

异常数
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。
配置降级规则
jemeter测试

查看实时监控,可以看到断路器熔断效果

热点参数限流
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

注意:
热点规则需要使用@SentinelResource(“resourceName”)注解,否则不生效
参数必须是7种基本数据类型才会生效
测试用例
@RequestMapping("/info/{id}")
@SentinelResource(value = "userinfo",
blockHandlerClass = CommonBlockHandler.class,
blockHandler = "handleException2",
fallbackClass = CommonFallback.class,
fallback = "fallback"
)
public R info(@PathVariable("id") Integer id){
UserEntity user = userService.getById(id);
return R.ok().put("user", user);
}
配置热点参数规则
注意: 资源名必须是@SentinelResource(value=“资源名”)中 配置的资源名,热点规则依赖于注解
具体到参数值限流,配置参数值为3,限流阈值为1
测试:
http://localhost:8800/user/info/1 限流的阈值为3
http://localhost:8800/user/info/3 限流的阈值为1

系统规则
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- 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 达到阈值即触发系统保护。
编写系统规则
jemeter配置

测试结果

授权控制规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
来源访问控制规则(AuthorityRule)非常简单,主要有以下配置项:
- resource:资源名,即限流规则的作用对象。
- limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB。
- strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。
配置授权规则

第一步:实现com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser接口,在parseOrigin方法中区分来源,并交给spring管理
注意:如果引入CommonFilter,此处会多出一个
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author Fox
*/
@Component
public class MyRequestOriginParser implements RequestOriginParser {
/**
* 通过request获取来源标识,交给授权规则进行匹配
* @param request
* @return
*/
@Override
public String parseOrigin(HttpServletRequest request) {
// 标识字段名称可以自定义
String origin = request.getParameter("serviceName");
// if (StringUtil.isBlank(origin)){
// throw new IllegalArgumentException("serviceName参数未指定");
// }
return origin;
}
}
测试:origin是order的请求不通过。


集群规则
为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
https://github.com/alibaba/Sentinel/wiki/%E9%9B%86%E7%BE%A4%E6%B5%81%E6%8E%A7
集群流控中共有两种身份:
- Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
- Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。
Sentinel 集群流控支持限流规则和热点规则两种规则,并支持两种形式的阈值计算方式:
- 集群总体模式:即限制整个集群内的某个资源的总体 qps 不超过此阈值。
- 单机均摊模式:单机均摊模式下配置的阈值等同于单机能够承受的限额,token server 会根据连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30),按照计算出的总的阈值来进行限制。这种方式根据当前的连接数实时计算总的阈值,对于机器经常进行变更的环境非常适合。
启动方式
Sentinel 集群限流服务端有两种启动方式:
- 独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
- 嵌入模式(Embedded),即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。
云上版本 AHAS Sentinel 提供开箱即用的全自动托管集群流控能力,无需手动指定/分配 token server 以及管理连接状态,同时支持分钟小时级别流控、大流量低延时场景流控场景,同时支持 Istio/Envoy 场景的 Mesh 流控能力。
Spring Cloud Alibaba整合Sentinel
1.引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2.添加yml配置,为微服务设置sentinel控制台地址
添加Sentinel后,需要暴露/actuator/sentinel端点,而Springboot默认是没有暴露该端点的,所以需要设置。
测试http://localhost:8800/actuator/sentinel
server:
port: 8800
spring:
application:
name: mall-user-sentinel-demo
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 添加sentinel的控制台地址
dashboard: 127.0.0.1:8080
# 指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
# port: 8719
#暴露actuator端点
management:
endpoints:
web:
exposure:
include: '*'
3.在sentinel控制台中设置流控规则
- 资源名: 接口的API
- 针对来源: 默认是default,当多个微服务都调用这个资源时,可以配置微服务名来对指定的微服务设置阈值
- 阈值类型: 分为QPS和线程数 假设阈值为10
- QPS类型: 只得是每秒访问接口的次数>10就进行限流
- 线程数: 为接受请求该资源分配的线程数>10就进行限流

测试: 因为QPS是1,所以1秒内多次访问会出现如下情形:

访问http://localhost:8800/actuator/sentinel, 可以查看flowRules

微服务和Sentinel Dashboard通信原理
Sentinel控制台与微服务端之间,实现了一套服务发现机制,集成了Sentinel的微服务都会将元数据传递给Sentinel控制台,架构图如下所示:

RestTemplate整合Sentinel
Spring Cloud Alibaba Sentinel 支持对 RestTemplate 的服务调用使用 Sentinel 进行保护,在构造 RestTemplate bean的时候需要加上 @SentinelRestTemplate 注解。
@SentinelRestTemplate 注解的属性支持限流(blockHandler, blockHandlerClass)和降级(fallback, fallbackClass)的处理。
引入依赖
<!--加入nocas-client-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
<!--加入ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
RestTemplate添加@SentinelRestTemplate注解
@Bean
@LoadBalanced
@SentinelRestTemplate(
blockHandler = "handleException",blockHandlerClass = GlobalExceptionHandler.class,
fallback = "fallback",fallbackClass = GlobalExceptionHandler.class
)
public RestTemplate restTemplate() {
return new RestTemplate();
}
异常处理类定义需要注意的是该方法的参数和返回值跟 org.springframework.http.client.ClientHttpRequestInterceptor#interceptor 方法一致,其中参数多出了一个 BlockException 参数用于获取 Sentinel 捕获的异常。
源码跟踪:
com.alibaba.cloud.sentinel.custom.SentinelBeanPostProcessor
com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor#intercept
// UserController.java
@RequestMapping(value = "/findOrderByUserId/{id}")
// @SentinelResource(value = "findOrderByUserId",
// fallback = "fallback",fallbackClass = ExceptionUtil.class,
// blockHandler = "handleException",blockHandlerClass = ExceptionUtil.class
// )
public R findOrderByUserId(@PathVariable("id") Integer id) {
//ribbon实现
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
return result;
}
public class GlobalExceptionUtil {
/**
* 注意: static修饰,参数类型不能出错
* @param request org.springframework.http.HttpRequest
* @param body
* @param execution
* @param ex
* @return
*/
public static SentinelClientHttpResponse handleException(HttpRequest request,byte[] body,
ClientHttpRequestExecution execution, BlockException ex) {
R r = R.error(-1, "===被限流啦===");
try {
return new SentinelClientHttpResponse(new ObjectMapper().writeValueAsString(r));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
public static SentinelClientHttpResponse fallback(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution, BlockException ex) {
R r = R.error(-2, "===被异常降级啦===");
try {
return new SentinelClientHttpResponse(new ObjectMapper().writeValueAsString(r));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
添加yml配置
server:
port: 8801
spring:
application:
name: mall-user-sentinel-ribbon-demo #微服务名称
#配置nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 添加sentinel的控制台地址
dashboard: 127.0.0.1:8080
# 指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
port: 8719
#暴露actuator端点 http://localhost:8800/actuator/sentinel
management:
endpoints:
web:
exposure:
include: '*'
#true开启sentinel对resttemplate的支持,false则关闭 默认true
resttemplate:
sentinel:
enabled: true
Sentinel RestTemplate 限流的资源规则提供两种粒度:
- httpmethod:schema://host:port/path:协议、主机、端口和路径
- httpmethod:schema://host:port:协议、主机和端口
测试限流

测试降级
修改服务提供者mall-order
@RequestMapping("/findOrderByUserId/{userId}")
public R findOrderByUserId(@PathVariable("userId") Integer userId) {
//模拟异常
if(userId==5){
throw new IllegalArgumentException("非法参数异常");
}
log.info("根据userId:"+userId+"查询订单信息");
List<OrderEntity> orderEntities = orderService.listByUserId(userId);
return R.ok().put("orders", orderEntities);
}


OpenFeign整合Sentinel
Sentinel 适配了 Feign 组件。如果想使用,除了引入 spring-cloud-starter-alibaba-sentinel 的依赖外还需要 2 个步骤:
配置文件打开 Sentinel 对 Feign 的支持:feign.sentinel.enabled=true

加入 spring-cloud-starter-openfeign 依赖使 Sentinel starter 中的自动化配置类生效:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在Feign的声明式接口上添加fallback属性
@FeignClient(value = "mall-order",path = "/order",fallback = FallbackOrderFeignService .class)
public interface OrderFeignService {
@RequestMapping("/findOrderByUserId/{userId}")
public R findOrderByUserId(@PathVariable("userId") Integer userId);
}
@Component //必须交给spring 管理
public class FallbackOrderFeignService implements OrderFeignService {
@Override
public R findOrderByUserId(Integer userId) {
return R.error(-1,"=======服务降级了========");
}
}
添加fallbackFactory属性
@Component
public class FallbackOrderFeignServiceFactory implements FallbackFactory<OrderFeignService> {
@Override
public OrderFeignService create(Throwable throwable) {
return new OrderFeignService() {
@Override
public R findOrderByUserId(Integer userId) {
return R.error(-1,"=======服务降级了========");
}
};
}
}
UserController
@Autowired
OrderFeignService orderFeignService;
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
//feign调用
R result = orderFeignService.findOrderByUserId(id);
return result;
}
注意:主启动类上加上@EnableFeignClients注解,开启Feign支持
测试
关闭mall-order服务,访问http://localhost:8801/user/findOrderByUserId/4自动降级了

Sentinel整合Dubbo实战
Sentinel 提供 Dubbo 的相关适配 Sentinel Dubbo Adapter,主要包括针对 Service Provider 和 Service Consumer 实现的 Filter。
相关模块:
sentinel-apache-dubbo-adapter(兼容 Apache Dubbo 2.7.x 及以上版本,自 Sentinel 1.5.1 开始支持)
sentinel-dubbo-adapter(兼容 Dubbo 2.6.x 版本)
引入此依赖后,Dubbo 的服务接口和方法(包括调用端和服务端)就会成为 Sentinel 中的资源,在配置了规则后就可以自动享受到Sentinel 的防护能力。
Sentinel Dubbo Adapter 还支持配置全局的 fallback 函数,可以在 Dubbo 服务被限流/降级/负载保护的时候进行相应的 fallback 处理。用户只需要实现自定义的 DubboFallback 接口,并通过 DubboAdapterGlobalConfig注册即可。默认情况会直接将 BlockException 包装后抛出。同时,我们还可以配合 Dubbo 的 fallback 机制 来为降级的服务提供替代的实现。
Provider端
对服务提供方的流量控制可分为服务提供方的自我保护能力和服务提供方对服务消费方的请求分配能力两个维度。
Provider 用于向外界提供服务,处理各个消费者的调用请求。为了保护 Provider 不被激增的流量拖垮影响稳定性,可以给 Provider 配置 QPS 模式的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。限流粒度可以是 服务接口 和 服务方法 两种粒度。若希望整个服务接口的 QPS 不超过一定数值,则可以为对应服务接口资源(resourceName 为接口全限定名)配置 QPS 阈值;若希望服务的某个方法的 QPS 不超过一定数值,则可以为对应服务方法资源(resourceName 为接口全限定名:方法签名)配置 QPS 阈值。
限流粒度可以是服务接口和服务方法两种粒度:
服务接口:resourceName 为 接口全限定名,如 com.tuling.mall.service.UserService
服务方法:resourceName 为 接口全限定名:方法签名,如 com.tuling.mall.service.UserService:getById(java.lang.Integer)
Consumer端
对服务提供方的流量控制可分为控制并发线程数和服务降级两个维度。
控制并发线程数
Service Consumer 作为客户端去调用远程服务。每一个服务都可能会依赖几个下游服务,若某个服务 A 依赖的下游服务 B 出现了不稳定的情况,服务 A 请求 服务 B 的响应时间变长,从而服务 A 调用服务 B 的线程就会产生堆积,最终可能耗尽服务 A 的线程数。我们通过用并发线程数来控制对下游服务 B 的访问,来保证下游服务不可靠的时候,不会拖垮服务自身。基于这种场景,推荐给 Consumer 配置线程数模式的限流,来保证自身不被不稳定服务所影响。采用基于线程数的限流模式后,我们不需要再显式地去进行线程池隔离,Sentinel 会控制资源的线程数,超出的请求直接拒绝,直到堆积的线程处理完成,可以达到信号量隔离的效果。
服务降级
当服务依赖于多个下游服务,而某个下游服务调用非常慢时,会严重影响当前服务的调用。这里我们可以利用 Sentinel 熔断降级的功能,为调用端配置基于平均 RT 的降级规则。这样当调用链路中某个服务调用的平均 RT 升高,在一定的次数内超过配置的 RT 阈值,Sentinel 就会对此调用资源进行降级操作,接下来的调用都会立刻拒绝,直到过了一段设定的时间后才恢复,从而保护服务不被调用端短板所影响。同时可以配合 fallback 功能使用,在被降级的时候提供相应的处理逻辑。
1.引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--Sentinel 对 Dubbo的适配 Apache Dubbo 2.7.x 及以上版本-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
2.接入sentinel dashboard,yml中增加配置
spring:
cloud:
sentinel:
transport:
# 添加sentinel的控制台地址
dashboard: 127.0.0.1:8080
#暴露actuator端点
management:
endpoints:
web:
exposure:
include: '*'
3.consumer端配置流控规则测试
@RequestMapping("/info/{id}")
public User info(@PathVariable("id") Integer id) {
User user = null;
try {
user = userService.getById(id);
} catch (Exception e) {
e.printStackTrace();
}
return user;
}
@PostConstruct
public void init() {
DubboAdapterGlobalConfig.setConsumerFallback(
(invoker, invocation, ex) -> AsyncRpcResult.newDefaultAsyncResult(
new User(0,"===fallback=="), invocation));
}
测试: http://localhost:8082/user/info/1

4.provider端配置流控规则测试

@RequestMapping("/getById/{id}")
@SentinelResource("getById")
public User getById(@PathVariable("id") Integer id) {
User user = null;
try {
user = userMapper.getById(id);
} catch (Exception e) {
e.printStackTrace();
}
return user;
}
@PostConstruct
public void init() {
DubboAdapterGlobalConfig.setProviderFallback(
(invoker, invocation, ex) -> AsyncRpcResult.newDefaultAsyncResult(
new User(0,"===provider fallback=="), invocation));
}

5.consumer中配置mock实现,关闭provider服务,测试mock降级
@DubboReference(mock = "com.tuling.mall.user.mock.UserServiceDubboMock")
private UserService userService;
public class UserServiceDubboMock implements UserService {
@Override
public List<User> list() {
return null;
}
@Override
public User getById(Integer id) {
return new User(0,"====mock===");
}
}

Sentinel限流、熔断降级源码架构图

常见限流算法精讲
计数器法
计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。

具体算法的伪代码:
/**
* 最简单的计数器限流算法
*/
public class Counter {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public int reqCount = 0; // 初始化计数器
public final int limit = 100; // 时间窗口内最大请求数
public final long interval = 1000 * 60; // 时间窗口ms
public boolean limit() {
long now = System.currentTimeMillis();
if (now < timeStamp + interval) {
// 在时间窗口内
reqCount++;
// 判断当前时间窗口内是否超过最大请求控制数
return reqCount <= limit;
} else {
timeStamp = now;
// 超时后重置
reqCount = 1;
return true;
}
}
}
滑动时间窗口算法
滑动时间窗口,又称rolling window。为了解决计数器法统计精度太低的问题,引入了滑动窗口算法。下面这张图,很好地解释了滑动窗口算法:
在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
具体算法的伪代码:
/**
* 滑动时间窗口限流实现
* 假设某个服务最多只能每秒钟处理100个请求,我们可以设置一个1秒钟的滑动时间窗口,
* 窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数
*/
public class SlidingTimeWindow {
//服务访问次数,可以放在Redis中,实现分布式系统的访问计数
Long counter = 0L;
//使用LinkedList来记录滑动窗口的10个格子。
LinkedList<Long> slots = new LinkedList<Long>();
public static void main(String[] args) throws InterruptedException {
SlidingTimeWindow timeWindow = new SlidingTimeWindow();
new Thread(new Runnable() {
@Override
public void run() {
try {
timeWindow.doCheck();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
while (true){
//TODO 判断限流标记
timeWindow.counter++;
Thread.sleep(new Random().nextInt(15));
}
}
private void doCheck() throws InterruptedException {
while (true) {
slots.addLast(counter);
if (slots.size() > 10) {
slots.removeFirst();
}
//比较最后一个和第一个,两者相差100以上就限流
if ((slots.peekLast() - slots.peekFirst()) > 100) {
System.out.println("限流了。。");
//TODO 修改限流标记为true
}else {
//TODO 修改限流标记为false
}
Thread.sleep(100);
}
}
}
漏桶算法
漏桶算法,又称leaky bucket。

从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题。
具体的伪代码如下:
/**
-
漏桶限流算法
*/
public class LeakyBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 水漏出的速度(每秒系统能处理的请求数)
public long water; // 当前水量(当前累积请求数)public boolean limit() { long now = System.currentTimeMillis(); water = Math.max(0, water - ((now - timeStamp)/1000) * rate); // 先执行漏水,计算剩余水量 timeStamp = now; if ((water + 1) < capacity) { // 尝试加水,并且水还未满 water += 1; return true; } else { // 水满,拒绝加水 return false; }}
}
令牌桶算法
令牌桶算法,又称token bucket。同样为了理解该算法,我们来看一下该算法的示意图:

从图中我们可以看到,令牌桶算法比漏桶算法稍显复杂。首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以 一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。
具体的伪代码如下:
/**
-
令牌桶限流算法
*/
public class TokenBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 令牌放入速度
public long tokens; // 当前令牌数量public boolean grant() {
long now = System.currentTimeMillis();
// 先添加令牌
tokens = Math.min(capacity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if (tokens < 1) {
// 若不到1个令牌,则拒绝
return false;
} else {
// 还有令牌,领取令牌
tokens -= 1;
return true;
}
}
}
限流算法小结
计数器 VS 滑动窗口:
计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。
滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。
也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。
漏桶算法 VS 令牌桶算法:
漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。
因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。
当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。
879

被折叠的 条评论
为什么被折叠?



