限流的基本认识
场景分析
一个互联网产品,打算搞一次大促来增加销量以及曝光。公司的架构师基于往期的流量情况做了一个活动流量的预估,然后整个公司的各个技术团队开始按照这个目标进行设计和优化,最终在大家不懈的努力之下,达到了链路压测的目标流量峰值。到了活动开始那天,大家都在盯着监控面板,看着流量像洪水一样涌进来。由于前期的宣传工作做得很好,使得这个流量远远超过预估的峰值,后端服务开始不稳定,CPU、内存各种爆表。部分服务开始出现无响应的情况。最后,整个系统开始崩溃,用户无法正常访问服务。
引入限流
在10.1黄金周,各大旅游景点都是人满为患。所有有些景点为了避免出现踩踏事故,会采取限流措施。
那在架构场景中,是不是也能这么做呢?针对这个场景,能不能够设置一个最大的流量限制,如果超过这个流量,我们就拒绝提供服务,从而使得我们的服务不会挂掉。当然,限流虽然能够保护系统不被压垮,但是对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。但是相比于全部不可用,有损服务是最好的一种解决办法。
限流的作用
除了前面说的限流使用场景之外,限流的设计还能防止恶意请求流量、恶意攻击。
所以,限流的基本原理是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或者告知资源没有了)、排队或等待(秒杀、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。
一般互联网企业常见的限流有:限制总并发数(如数据库连接池、线程池)、限制瞬时并发数(nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他的还有限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
有了限流,就意味着在处理高并发的时候多了一种保护机制,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务。
常见的限流算法
滑动窗口
发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口。目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。如下图所示:
图中的4,5,6号数据帧已经被发送出去,但是未收到关联的ACK,7,8,9帧则是等待发送。可以看出发送端的窗口大小为6,这是由接受端告知的。此时如果发送端收到4号ACK,则窗口的左边缘向右收缩,窗口的右边缘则向右扩展,此时窗口就向前“滑动了”,即数据帧10也可以被发送。演示地址
漏桶(控制传输速率Leaky bucket)
漏桶算法思路是,不断的往桶里面注水,无论注水的速度是大还是小,水都是按固定的速率往外漏水;如果桶满了,水会溢出;
桶本身具有一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水可以加入。一旦水满,上方的水就无法加入。桶满正是算法中的一个关键的触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方流下来的水,有两种方式,在桶满水之后,常见的两种处理方式为:
1)暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。
2)溢出的上方水直接抛弃。
特点:
- 1、漏水的速率是固定的。
- 2、即使存在突然注水量变大的情况,楼岁的速率也是固定的。
*令牌桶(能够处理突发的流量)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
- 令牌流和令牌桶
系统会以一定的速度生成令牌,并将其放置到令牌桶中,可以将令牌桶想象成一个缓冲区(可以用队列这种数据结构来实现),当缓冲区填满的时候,新生成的令牌会被扔掉。这里有两个变量很重要:
第一个是生成令牌的速度,一般称为 rate 。比如,我们设定 rate = 2 ,即每秒钟生成 2 个令牌,也就是每 1/2 秒生成一个令牌;
第二个是令牌桶的大小,一般称为 burst 。比如,我们设定 burst = 10 ,即令牌桶最大只能容纳 10 个令牌。
有三种情况可能发生:
- 数据流的速率=令牌流点的速率:这种情况下,每个到来的数据包或者请求都能对应一个令牌,然后无延迟地通过队列;
- 数据流的速率<令牌流的速率:通过队列的数据包或者请求只消耗了一部分令牌,剩下的令牌会在令牌桶里积累下来,直到桶被装满。剩下的令牌可以在突发请求的时候消耗掉。
- 数据流的速率>令牌流的速率:桶里的令牌很快就会被耗尽。导致服务中断一段时间,如果数据包或者请求持续到来,将发生丢包或者拒绝响应。
Sentinel整合Dubbo
限流的方式:
Semaphore,RateLimiter ->令牌桶/漏桶;
Redisson(RRateLimiter);
Alibaba Sentinel
限流只是一个最基本的服务治理/服务质量体系要求
- 流量的切换
- 能不能够针对不同的渠道设置不同的限流策略
- 流量的监控
- 熔断
- 动态限流
- 集群限流
本文主要讲讲Sentinel怎么用
- 初始化限流规则
- 根据限流规则进行限流
Sentinel限流的思考
- 限流用了什么算法来实现(滑动窗口)
- 他是怎么实现的(责任链)
- SPI的扩展
创建provider项目
添加jar依赖
<dependency>
<groupId>com.tc.sentinel</groupId>
<version>1.1-SNAPSHOT</version>
<artifactId>sentinel-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
SentinelService api
public interface SentinelService { String sayHello(String name); }
SentinelServiceImpl
/**把当前服务发布成dubbo服务*/
@Service
public class SentinelServiceImpl implements SentinelService {
@Override
public String sayHelloWorld(String txt) {
System.out.println("begin execute sayHello:" + txt);
return "Hello World :" + LocalDateTime.now();
}
}
DubboConfig
@Configuration
@DubboComponentScan("com.tc.sentinel")
public class DubboConfig {
@Bean
public ApplicationConfig applicationConfig(){
ApplicationConfig applicationConfig=new ApplicationConfig();
applicationConfig.setName("sentinel-provider");
applicationConfig.setOwner("Tc");
return applicationConfig;
}
@Bean
public RegistryConfig registryConfig(){
RegistryConfig registryConfig=new RegistryConfig();
registryConfig.setAddress("zookeeper://120.**.**.***:2181");
return registryConfig;
}
@Bean
public ProtocolConfig protocolConfig(){
ProtocolConfig protocolConfig=new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
return protocolConfig;
}
}
创建SpringBoot的Consumer项目
添加jar依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>com.tc.sentinel</groupId>
<artifactId>sentinel-api</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
SentinelDubboController
@RestController
public class SentinelController {
@Reference
SentinelService sentinelService;
@GetMapping("/say")
public String sayHello(){
RpcContext.getContext().setAttachment("dubboApplication","sentinel-web");
return sentinelService.sayHelloWorld("test-sentinel");
}
@GetMapping("/say2")
public String sayHello2(){
return sentinelService.sayHelloWorld("test-default");
}
}
application.properties
dubbo.registry.address=zookeeper://120.**.**.***:2181
dubbo.scan.base-packages=com.tc.sentinel.sentinelweb
dubbo.application.name=sentinel-web
server.port=8090
在provider添加sentinel限流支持
添加jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-dubbo-adapter</artifactId>
<version>1.6.3</version>
</dependency>
设置限流的基准
Service Provider 用于向外界提供服务,处理各个消费者的调用请求。为了保护 Provider 不被激增的流量拖垮影响稳定性,可以给 Provider 配置 QPS 模式的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。
限流粒度可以是服务接口和服务方法两种粒度。若希望整个服务接口的 QPS 不超过一定数值,则可以为对应服务接口资源(resourceName 为接口全限定名)配置 QPS 阈值;若希望服务的某个方法的 QPS 不超过一定数值,则可以为对应服务方法资源(resourceName 为接口全限定名:方法签名)配置 QPS 阈值
@SpringBootApplication
public class SentinelProviderApplication {
public static void main(String[] args) throws IOException {
//表示当前的节点是集群客户端
// ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
//初始化限流规则
initFlowRole();
SpringApplication.run(SentinelProviderApplication.class, args);
System.in.read();
}
//限流规则
private static void initFlowRole(){
FlowRule flowRule = new FlowRule();
//针对具体方法限流
flowRule.setResource("com.tc.sentinel.SentinelService:sayHelloWorld(java.lang.String)");
//限流阈值 qps=10
flowRule.setCount(5);
//限流阈值类型(QPS或者并发线程数)
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//流控针对的调用来源,若为 default 则不区分调 用来源
flowRule.setLimitApp("sentinel-web");
//流量控制手段(直接拒绝、Warm Up、匀速排队)
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(Collections.singletonList(flowRule));
}
}
启动时加入 JVM 参数 -Dcsp.sentinel.dashboard.server=localhost:8080 指定控制台地址和端口。
试用jemeter进行压测
参数解释
LimitApp*
很多场景下,根据调用方来限流也是非常重要的。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流,则可以设置限流规则的 limitApp 为服务 B 的名称。Sentinel Dubbo Adapter 会自动解析 Dubbo 消费者(调用方)的 application name 作为调用方名称(origin),在进行资源保护的时候都会带上调用方名称。若限流规则未配置调用方(default),则该限流规则对所有调用方生效。若限流规则配置了调用方则限流规则将仅对指定调用方生效。
注:Dubbo 默认通信不携带对端 application name 信息,因此需要开发者在调用端手动将 application name 置入 attachment 中,provider 端进行相应的解析。Sentinel Dubbo Adapter 实现了一个 Filter 用 于自动从 consumer 端向 provider 端透传 application name。若调用端未引入 Sentinel Dubbo Adapter,又希望根据调用端限流,可以在调用端手动将 application name 置入 attachment 中,key 为dubboApplication。
演示流程
1、修改provider中限流规则:flowRule.setLimitApp("sentinel-web");
2、在consumer工程中,做如下处理。其中一个通过attachment传递了一个消费者的application.name,另一个没有传,通过jemeter工具进行测试。
@GetMapping("/say")
public String sayHello(){
RpcContext.getContext().setAttachment("dubboApplication","sentinel-web");
return sentinelService.sayHelloWorld("test-sentinel");
}
@GetMapping("/say2")
public String sayHello2(){
return sentinelService.sayHelloWorld("test-default");
}
ControlBehavior
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。
直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位。
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式,当系统长期处于低并发的情况下,流量突然增加到qps的最高峰值,可能会造成系统的瞬间流量过大把系统压垮。所以warmup,相当于处理请求的数量是缓慢增加,经过一段时间以后,到达系统处理请求个数的最大值。
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
它的原理是,以固定的间隔时间让请求通过。当请求过来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。
可以设置一个最长排队等待时间:
flowRule.setMaxQueueingTimeMs(5 * 1000); // 最长排队等待时间:5s
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
如何是实现分布式限流
在前面的所有案例中,我们只是基于Sentinel的基本使用和单机限流的使用,假如有这样一个场景,我们现在把provider部署了10个集群,希望调用这个服务的api的总的qps是100,意味着每一台机器的qps是10,理想情况下总的qps就是100.但是实际上由于负载均衡策略的流量分发并不是非常均匀的,就会导致总的qps不足100时,就被限了。在这个场景中,仅仅依靠单机来实现总体流量的控制是有问
题的。所以最好是能实现集群限流。
架构图
要想使用集群流控功能,我们需要在应用端配置动态规则源,并通过 Sentinel 控制台实时进行推送。如下图所示:
搭建token-server
jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.6.3</version>
</dependency>
ClusterServer
public class ClusterServer {
public static void main(String[] args) throws Exception {
ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
ClusterServerConfigManager.loadGlobalTransportConfig(
new ServerTransportConfig().setIdleSeconds(600).setPort(9999));
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("App-Tc"));
tokenServer.start();
}
}
/**
* 从nacos上去获得动态的限流规则
*/
public class NacosDataSourceInitFunc implements InitFunc {
/**nacos 配置中心的服务host*/
private final String remoteAddress="192.168.12.101";
/**nacos groupId*/
private final String groupId="SENTINEL_GROUP";
/**dataid(names + postfix) namespace不同,限流规则也不同*/
private final String FLOW_POSTFIX="-flow-rules";
/**意味着当前的token-server会从nacos上获得限流的规则*/
@Override
public void init() throws Exception {
ClusterFlowRuleManager.setPropertySupplier(namespeces->{
ReadableDataSource<String, List<FlowRule>> rds =
new NacosDataSource<>(remoteAddress, groupId, namespeces + FLOW_POSTFIX,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
return rds.getProperty();
});
}
}
在resource目录添加扩展点
/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc = 自定义扩展点
启动Sentinel dashboard
启动nacos以及增加配置
配置内容:
[
{
"resource":"com.gupao.sentinel.sentinelService:sayHello(java.lang.String)",
"grade":1, 限流模式:1-qps
"count":10, 限流阈值
"clusterMode":true, 是否集群模式
"clusterConfig":{
"flowId":111222, 全局唯一id
"thresholdType":1, 阈值模式:1-全局
"fallbackToLocalWhenFail":true client通信失败 是否使用本地
}
}
]
配置JVM参数:
配置如下jvm启动参数,连接到sentinel dashboard
-Dproject.name=App-Tc -Dcsp.sentinel.dashboard.server=192.168.12.101:8080 -Dcsp.sentinel.log.use.pid=true
Dubbo接入分布式限流
jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-client-default</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.6.3</version>
</dependency>
增加扩展点
扩展点需要在resources/META-INF/services/增加扩展的配置
com.alibaba.csp.sentinel.init.initFunc=自定义扩展点
/**
* 从nacos上去获得动态的限流规则
*/
public class NacosDataSourceInitFunc implements InitFunc {
//token-server的地址
private static final String CLUSTER_SERVER_HOST="localhost";
private static final int CLUSTER_SERVER_PORT=9999;
//请求超时时间
private static final int REQUEST_TIME_OUT=200000;
//namespace
private static final String APP_NAME = "App-Tc";
/**nacos配置中心的服务host*/
private static final String REMOTE_ADDRESS = "192.168.12.101";
/**groupId*/
private static final String GROUP_ID = "SENTINEL_GROUP";
/**dataid(names+postfix)*/
private static final String FLOW_POSTFIX = "-flow-rules";
/**意味着当前的token-server会从nacos上获得限流的规则*/
@Override
public void init() throws Exception {
//加载集群-信息
loadClusterClientConfig();
registryClusterFlowRuleProperty();
}
/**通过硬编码的方式,配置连接到token-server的服务地址(实际使用过程中不建议,
*可以基于动态配置改造 )*/
private static void loadClusterClientConfig() {
ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
assignConfig.setServerHost(CLUSTER_SERVER_HOST);
assignConfig.setServerPort(CLUSTER_SERVER_PORT);
ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
ClusterClientConfig clientConfig = new ClusterClientConfig();
//token-client请求token-server获取令牌的超时时间
clientConfig.setRequestTimeout(REQUEST_TIME_OUT);
ClusterClientConfigManager.applyNewConfig(clientConfig);
}
/**注册动态数据源*/
/**
* [
* {
* "resource":"com.gupao.sentinel.sentinelService:sayHello(java.lang.String)",
* "grade":1, 限流模式:1-qps
* "count":10, 限流阈值
* "clusterMode":true, 是否集群模式
* "clusterConfig":{
* "flowId":111222, 全局唯一id
* "thresholdType":1, 阈值模式:1-全局
* "fallbackToLocalWhenFail":true client通信失败 是否使用本地
* }
* }
* ]
*/
/*** 注册动态规则Property
* 当client与Server连接中断,退化为本地限流时需要用到的该规则
* 该配置为必选项,客户端会从nacos上加载限流规则,请求tokenserver时,会戴上要check的规 则id
* {这里的动态数据源,我们稍后会专门讲到}
*/
private static void registryClusterFlowRuleProperty(){
// 使用 Nacos 数据源作为配置中心,需要在 REMOTE_ADDRESS 上启动一个 Nacos 的服务
ReadableDataSource<String, List<FlowRule>> ds =
new NacosDataSource<>(REMOTE_ADDRESS, GROUP_ID, APP_NAME + FLOW_POSTFIX,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
// 为集群客户端注册动态规则源
FlowRuleManager.register2Property(ds.getProperty());
}
}
配置jvm参数
这里的project-name要包含在token-server中配置的namespace中,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值。
-Dproject.name=App-Tc
-Dcsp.sentinel.dashboard.server=192.168.12.101:8080
-Dcsp.sentinel.log.use.pid=true
演示集群限流
所谓集群限流,就是多个服务节点使用同一个限流规则。从而对多个节点的总流量进行限制,添加一个sentinel-server。同时运行两个程序。