常用的限流策略:
- Niginx添加限流模块限制平均访问速度
- 通过数据库连接池,线程池大小限制
- 通过Guava包Ratelimiter限制接口访问速度
- Tcp通信协议中流量整形
常见限流算法:
1.计数器算法
在指定周期内限制访问次数,进入下一个时间周期次数清0.
这种算法可以用在短信发送频次限制上,比如限制一个用户一分钟之内触发短信发送的次数。
可以借助 redis incr 命令实现
缺点:
- 临界问题:单位周期内,某一个时间点达到峰值,导致后续单位时间内无法访问。
2.滑动窗口法
在固定窗口中分割出多个小窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可
解决了固定窗口中的临界问题
Sentinel就是才用的滑动窗口算法
3.令牌桶限流算法
令牌通是网络流量整形和速率限制中最常使用的一种算法,每一个请求都需要从桶中获得一个令牌,如果没有获得令牌,则触发限流策略。
- 请求速度大于令牌生成速度:令牌被用完后,后续请求被限流。
- 请求速度等于令牌生成速度:流量处于平稳状态。
- 请求速度小于令牌生成速度:并发数不高,正常被处理。
令牌通能够处理突发流量,也就是在短时间内新增的流量系统能够正常处理,这是令牌通的特性。
利用 Guava concurrent 包下 的 RateLimiter 限流工具类可以实现令牌桶算法
4.漏桶限流算法
主要作用是控制数据注入网络的速度,平滑网络上的突发流量。
维护一个固定容量的桶,这个桶会按照指定的速度漏水。
请求到达系统就类似于向桶中加水,如果这个桶满了,就忽略后面来的请求,直到这个桶可以存放多余的水。
漏桶算法可以将系统处理能力维持在一个比较平稳的水平,缺点就是瞬时流量过来后,会拒绝后续的请求,类似队列满了以后拒绝。
Sentinel
分为两部分:
- 核心库(Java客户端):不依赖任何框架/库。能够运行与java运行时环境,同时对Dubbo,SpringCloud等框架有较好支持、
- 控制台(Dashboard):基于Spring boot开发,打包后直接运行,不需要额外的容器
Sentinel实现限流
使用方式:
- 抛出异常的方式定义资源
- 返回布尔值方式定义资源
- 注解方式
- 异步调用
规则种类:
- 流量控制规则
- 熔断降级规则
- 系统保护规则
- 访问控制规则
- 热点规则
- 查询更改规则
- 定制自己的持久化规则
基于并发数和QPS的流量控制
Sentinel流量控制统计有2中,通过grade属性控制
- 并发线程数(FLOW_GRADE_THREAD)
- QPS(FLOW_GRADE_QPS)
并发线程数:
如果A服务调用B服务,B不稳当或者延迟,那么A的吞吐量会下降,占用更多的线程,且线程阻塞一致未释放,会导致线程池耗尽。
解决方式:通过不同业务逻辑使用不同线程池来隔离业务自身资源争抢问题,但是会造成线程数量过多带来的上下文切换问题。
Sentinel并发线程数限流,就是统计当前请求的上下文线程数量,如果超出阈值,则后续请求会被拒绝。
Qps:
一台服务器每秒能够响应的查询次数,当QPS达到限流的阈值时,就会触发限流策略
当Qps超过阈值时,会触发限流行为,通过controlBehavior来设置,包含:
- 直接拒绝 (RuleConstant.CONTROL_BEHAVIOR_DEFAULT)
- Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)
- 匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)
- 冷启动+匀速排队(RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER)
调用关系流量策略
调用关系包括调用方和被调用方,一个方法又可能会调用其他方法,形成一个调用链。
调用关系流量策略,就是根据不同的调用维度来出发流量控制。
- 根据调用方限流
- 根据调用链路入口限流
- 据有关系的资源流量控制
调用方限流:
根据请求来源进行流量控制,需要设置limitApp
- default:表示不区分调用者,任何调用者都会进行限流统计。
- {some_origin_name}:设置特定的调用者,只有来自这个调用者的请求才会进行流量统计和控制。
- other:标识针对除{some_origin_name}外的其他调用者进行流量控制
根据调用链路入口限流:
一个被限流保护的方法,可能来自不同的调用链,如资源nodeA,入口Entrance1 和 Entrance2 都调用了资源nodeA,如果志云与某个入口来进行流量统计,那么设置Entrance1入口,的调用才会统计请求次数,一定程度有点像 调用方限流。
关联流量控制:
两个资源之间存在争抢时,就说明两个资源存在关联。造成相互影响执行效率问题,所以关联流量控制(流控)就是限制其中一个资源的执行流量。
Sentinel实现服务熔断
与限流类似,限流才用的是FlowRule,而熔断才用的是DegradeRule
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule(KEY)
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
// Max allowed response time
.setCount(50)
// Retry timeout (in second)
.setTimeWindow(10)
// Circuit breaker opens when slow request ratio > 60%
.setSlowRatioThreshold(0.6)
.setMinRequestAmount(100)
.setStatIntervalMs(20000);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
System.out.println("Degrade rule loaded: " + rules);
}
Sentinel三种熔断策略
- 平均响应时间(RuleConstant.DEGRADE_GRADE_RT):如果1s内持续进入5个请求,对应的平均响应时间都超过了阈值(count,单位 ms) ,那么在接下来的时间窗口(timeWindow , 单位为s)内,对这个方法的调用都会自动熔断,抛出 DegradeException。
Sentinel 默认统计的RT上限是4900ms,如果超出此阈值都会算4900ms,可以通过启动参数来配置。
- 异常比例(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO):如果每秒资源数>=minRequestAmount(默认值为5),并且每秒的异常总数占总通过量的比例超过阈值count (count 的取值范围是 【0.0 ,1.0】 代表 0 ~ 100%,则资源进入降级状态,在接下来的timeWindow之内,对这个方法的调用都会触发熔断。
- 异常数(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT):当资源最近一分钟的异常数目超过阈值之后,会触发熔断。需要注意的是,如果timeWindow小于60s,则结束熔断状态后仍然可能再进入熔断状态。
Spring Cloud 集成 Sentinel
pom文件引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
通过@SentinelResource 注解配置限流保护资源
@RestController
public class HelloController {
@SentinelResource(value = "hello",blockHandler = "blockHandlerHello")
@GetMapping("/say")
public String hello(){
return "hello ,Mic";
}
public String blockHandlerHello(BlockException e){
return "被限流了";
}
}
- 在引入Sentinel starter pom 依赖以后,默认情况下,会对所有的HTTP请求限流埋点。
- 如果想对特定方法进行限流或者降级,加@SentinalResource注解来实现即可
- 可以通过Sphu.entry() 方法进行配置
手动配置流控规则,可以利用Sentinel 的 InitFunc SPI 扩展接口实现
public class FlowRuleInitFunc implements InitFunc{
@Override
public void init() throws Exception {
List<FlowRule> rules=new ArrayList<>();
FlowRule rule=new FlowRule();
rule.setCount(1);
rule.setResource("hello");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
SPI是扩展点机制,如果需要被Sentinel加载,需要在resource目录下创建META-INF/service/com.alibaba.csp.sentinel.init.InitFunc
在此文件中指定 FlowRuleInitFunc 类的路径。
最终效果如下:
基于Dashboard来实现流控配置
自定义URL限流异常
在默认情况下 URL 出发限流后会直接返回。
如果希望出发限流后返回结果形式,可以通过自定义限流异常来处理,实现CustomUrlBlockHandler 并且重写 blocked方法:
public class CustomUrlBlockHandler implements UrlBlockHandler{
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
httpServletResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
String message = "{\"code\":999,\"msg\":\"访问人数过多\"}";
httpServletResponse.getWriter().write(message);
}
}
当触发限流之后,希望直接跳转到一个限流页面:
spring.cloud.sentinel.servlet.block-page={url}
URL资源清洗
Sentinel 中Http 服务限流 默认是有 Sentinel-Web-Servlet 中的 CommonFilter 实现的。
假设只针对 clean 请求做流控处理,而不是针对每一个不同的{id} 进行流控。
@RestController
public class UrlCleanController {
@GetMapping("/clean/{id}")
public String dash(@PathVariable("id")int id){
return "Hello clean";
}
}
可以通过UrlCleaner接口来实现资源清洗,将clean/id 归集到 clean/*
@Service
public class CustomerUrlCleaner implements UrlCleaner{
@Override
public String clean(String originUrl) {
if(StringUtils.isEmpty(originUrl)){
return originUrl;
}
if(originUrl.startsWith("/clean/")){
return "/clean/*";
}
return originUrl;
}
}
Sentinel 集成 Nacos 实现流控
Sentinel Dashboard 所配置的流控规则,都是保存在内存中的,一旦应用重启,这些规则都会被清除。
Sentinel提供了动态数据源支持:Consul,ZooKeepr,Redis,Nacos,Apollo,etcd 等数据源的扩展。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.7.0</version>
</dependency>
spring:
application:
name: spring-cloud-sentinel-dynamic
cloud:
sentinel:
transport:
dashboard: 192.168.216.128:7777
datasource:
- nacos:
server-addr: 192.168.216.128:8848
data-id: ${spring.application.name}-sentinel-flow
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
server:
port: 8888
datasource 目前支持redis,apollo ,zk,file,nacos
data-type
配置项表示 Converter
类型,Spring Cloud Alibaba Sentinel 默认提供两种内置的值,分别是 json
和 xml
(不填默认是json)。 如果不想使用内置的 json
或 xml
这两种 Converter
,可以填写 custom
表示自定义 Converter
,然后再配置 converter-class
配置项,该配置项需要写类的全路径名(比如 spring.cloud.sentinel.datasource.ds1.file.converter-class=com.alibaba.cloud.examples.JsonFlowRuleListConverter
)。
rule-type
配置表示该数据源中的规则属于哪种类型的规则(flow
,degrade
,authority
,system
, param-flow
, gw-flow
, gw-api-group
)。
@RestController
public class DynamicController {
@GetMapping("/dynamic")
public String dynamic(){
return "Hello Dynamic Rule";
}
}
修改接口流控规则目前就有两个入口:
- Dashboard
- Nacos
在Dashboard上修改规则,不能同步到Nacos上,目前Sentinel Dashboard上不支持此功能。
但是再Nacos上修改规则,危险系数很高,没有专门的UI管理,以修改参数的形式容易引发问题。
Sentinel Dashboard 集成 Nacos 实现规则同步
com.alibaba.csp.sentinel.dashboard.controller.v2包下有一个 FlowControllerV2 类,提供流控规则的CRUD,和V1版本不同的是,他可以实现指定数据源的规则拉取和发布。
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
- DynamicRuleProvider 动态规则拉取
- DynamicRulePublisher 动态规则发布
修改源码步骤:
1. 注释 <scope>
!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!-- <scope>test</scope>-->
</dependency>
2.修改sentinel-dashboard\src\main\webapp\resources\app\scripts\directives\sidebar\sidebar.html
修改之后会调用 FlowControllerV2 中的接口
<li ui-sref-active="active" ng-if="!entry.isGateway">
<!-- <a ui-sref="dashboard.flowV1({app: entry.app})">-->
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>
3.在com.alibaba.csp.sentinel.dashboard.rule 包中创建一个 nacos 包,用来加载外部化配置
@ConfigurationProperties(prefix="sentinel.nacos",ignoreUnknownFields = true)
public class NacosPropertiesConfiguration {
private String serverAddr;
private String dataId;
private String groupId = "DEFAULT_GROUP";
private String namespace;
4.创建一个Nacos配置类
@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@Configuration
public class NacosConfiguration {
@Bean
public Converter<List<FlowRuleEntity>,String> flowRuleEntityEncoder(){
return JSON::toJSONString;
}
@Bean
public Converter<String,List<FlowRuleEntity>> flowRuleEntityDecoder(){
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws NacosException {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());
properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
return ConfigFactory.createConfigService(properties);
}
}
5.创建一个常量类 NacosConstans,分别表示 GROUP_ID 和 DATA_ID 的后缀
public class NacosConstants {
public static final String DATA_ID_POSTFIX = "-sentinel-flow";
public static final String GROUP_ID = "DEFAULT_GROUP";
}
6.实现动态从Nacos 配置中心获取流控规则
@Service
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>>{
private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosProvider.class);
@Autowired
private NacosPropertiesConfiguration nacosConfigProperties;
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String dataID=new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
String rules = configService.getConfig(dataID, nacosConfigProperties.getGroupId(), 3000);
logger.info("pull FlowRule from Nacos Config:{}",rules);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
7.创建一个流控规则发布类,在Dashboard修改完后,推送到Nacos中
@Service
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private NacosPropertiesConfiguration nacosPropertiesConfiguration;
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
@Override
public void publish(String appName, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(appName, "appName cannot be empty");
if (rules == null) {
return;
}
String dataID=new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
configService.publishConfig(dataID, nacosPropertiesConfiguration.getGroupId(), converter.convert(rules));
}
}
8.修改FlowControllerV2类,将新配置的两个类注入进来
@Autowired······
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
9.application.properties 中添加 Nacos 服务端配置信息。
sentinel.nacos.serverAddr=localhost:8848
sentinel.nacos.namespace=
sentinel.nacos.group-id=DEFAULT_GROUP
10.重新打包,启动服务
Sentinel 热点限流
针对访问频次非常高的数据进行限流,比如针对一段时间内频繁访问的用户IP地址进行限流,或者针对频繁访问的某个用户进行限流。
热点限流在一下场景中使用较多:
- 服务网关层:防止网络爬虫和恶意攻击,一种方法是限制爬虫的IP地址,客户端IP地址就是一种热点参数。
- 写数据的服务:业务系统提供写数据服务,数据会写入数据库之类的存储。存储系统的底层会加锁写磁盘上的文件,部分存储系统会将某一类数据写入同一个文件。如果底层写同一文件,会出现抢占锁的情况,导致出现大量的超时和失败。出现这种情况时一般有两种解决方法:修改存储设计,对热点参数限流
Sentinel 通过 LRU 策略结合滑动窗口机制来实现热点参数的统计,LRU 策略可以统计单位时间内最常访问的热点数据,滑动窗口机制可以协助统计每个参数的QPS。
限流依赖包:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>${project.version}</version>
</dependency>
热点参数限流使用
设置热点参数限流埋点 通过 ParamFlowRuleManager.loadRules 方法加载热点参数规则。
热点参数是通过ParamFlowRule来配置的,大部分属性和FlowRule类似。
@PostConstruct
public void initParamRule(){
ParamFlowRule rule=new ParamFlowRule(resourceName);
rule.setParamIdx(0);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1);
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}
@GetMapping("/hello")
public String sayHello(@PathParam("id")String id,@PathParam("name")String name){
Entry entry=null;
try {
entry=SphU.entry(resourceName, EntryType.IN,1,id);
return "access success";
} catch (BlockException e) {
e.printStackTrace();
return "block";
}finally {
if(entry!=null) {
entry.exit();
}
}
}
http://localhost:8888/hello?id=1
当传入id时,进行流控
http://localhost:8888/hello 不传id时,不进行流控
@SentinelResource 热点参数限流
@SentinelResource
@GetMapping("/hello")
public String sayHello(@PathParam("id")String id){
return "access success";
}
当注解所配置的方法上有参数是,Sentinel会把这些参数传入 Sphu.enty(res,args) 。会把id这个参数作为热点参数进行限流。
默认情况下,当用户访问这个接口时就会触发热点限流规则的验证。
硬编码限流
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
public class Test {
public static void main(String[] args) {
initFlowRules();
while (true) {
doSomething();
}
}
private static void doSomething(){
try( Entry entry = SphU.entry("doSomething")) {
/*您的业务逻辑 - 开始*/
System.out.println("hello world doSomething" + System.currentTimeMillis());
/*您的业务逻辑 - 结束*/
} catch (BlockException e1) {
/*流控逻辑处理 - 开始*/
// System.out.println("block!");
/*流控逻辑处理 - 结束*/
} /*finally {
if (entry != null) {
entry.exit();
}
}*/
}
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("doSomething");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
${USER_HOME}\logs\csp\{包名-类名}-metrics.log
- passQps:代表通过的请求数
- blockQps:代表被阻止的请求数
- successQps:代表成功执行完成的请求个数
- rt:代表平均响应市场
- occupiedPassQps:代表优先通过的请求
- concurrency:代表并发量
- classsification:代表组员类型