Sentinel中的流量控制(十九)

Sentinel中的流量控制

上篇的几个案例中,了解了如何使用sentinel去实现限流,在Sentinel中,其实提供了两种的流量控制方式。
  • 并发线程数
  • QPS
不管是什么方式,流控的核心原理是,监控应用流量的QPS或者并发线程数这些指标,去判断指标的阈值来实现对流量的控制,防止瞬时流高峰流量导致系统被压垮,从而保证系统的高可用性。
在Sentinel中,限流的直接表现形式是,在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。 FlowException 是 BlockException 的子类,您可以捕捉BlockException 来自定义被限流之后的处理逻辑。
同一个资源可以创建多条限流规则。 FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
  • resource :资源名,即限流规则的作用对象
  • count : 限流阈值
  • grade : 限流阈值类型(QPS 或并发线程数)
  • limitApp : 流控针对的调用来源,若为 default 则不区分调用来源
  • strategy : 调用关系限流策略
  • controlBehavior : 流量控制效果(直接拒绝、Warm Up、匀速排队)

如果想要查看实时的统计信息,可以运行上篇中的代码然后访问http://localhost:8719/cnode?id=doTest 就可以查看

  • thread: 代表当前处理该资源的并发数;
  • pass: 代表一秒内到来到的请求;
  • blocked: 代表一秒内被流量控制的请求数量;
  • success: 代表一秒内成功处理完的请求;
  • total: 代表到一秒内到来的请求以及被阻止的请求总和;
  • RT: 代表一秒内该资源的平均响应时间;
  • 1m-pass: 则是一分钟内到来的请求;
  • 1m-block: 则是一分钟内被阻止的请求;
  • 1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;
  • exception: 则是一秒内业务本身异常的总和。

并发线程数控制

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

QPS流量控制

QPS其实就是经常说的最大能支撑的每秒查询数量,当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的策略包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。

直接拒绝

直接拒绝( RuleConstant.CONTROL_BEHAVIOR_DEFAULT )方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowException 。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。改下上篇的SentinelDemo代码,加上直接拒绝的策略

//单机限流
public class SentinelDemo {
    public static void main(String[] arg) {
        initFlowRule(); //初始化限流规则
        while(true){
            //ResourceName表示资源,控制访问流量的点
            try(Entry entry= SphU.entry("helloWorld")){
                System.out.println("hello world");
            }catch (BlockException e){
                System.out.println("被拒绝");
            }
//            if (SphO.entry("helloWorld")) {
//                System.out.println("Hello World");
//                SphO.exit();
//            }
        }
    }
    private static void initFlowRule(){
        List<FlowRule> rules=new ArrayList<> ();
        FlowRule flowRule=new FlowRule();
        flowRule.setResource("helloWorld"); //针对那个资源设置规则
        flowRule.setGrade( RuleConstant.FLOW_GRADE_QPS);//QPS或者并发数
        flowRule.setCount(5); //QPS=5
        flowRule.setControlBehavior ( RuleConstant.CONTROL_BEHAVIOR_DEFAULT );//直接拒绝
        rules.add(flowRule);
        FlowRuleManager.loadRules(rules);
    }
}

Warm Up

Warm Up( RuleConstant.CONTROL_BEHAVIOR_WARM_UP )方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮;如下图所示,当前系统所能够处理的最大并发数是480,首先在最下面的标记位置,系统一直处于空闲状态,接着请求量突然直线升高,这个时候系统并不是直接将QPS拉到最大值,而是在一定时间内逐步增加阈值,而中间这段时间就是一个系统逐步预热的过程中。
 
 

匀速排队

匀速排队( RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER )方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
 

基于调用关系的流量控制

调用关系包含调用放和被调用放,一个方法有可能会调用其他方法,形成一个调用链,所谓的调用关系限流,就是根据不同的调用纬度来触发流量控制。
  • 根据调用方限流
  • 根据调用链路入口限流
  • 具有关系的资源流量控制

根据调用方限流

很多场景下,根据调用方来限流也是非常重要的。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流,则可以设置限流规则的 limitApp 为服务 B 的名称。Sentinel Dubbo Adapter 会自动解析 Dubbo 消费者(调用方)的 application name 作为调用方名称(origin),在进行资源保护的时候都会带上调用方名称。若限流规则未配置调用方(default),则该限流规则对所有调用方生效。若限流规则配置了调用方则限流规则将仅对指定调用方生效。所谓调用方限流,就是根据请求来源进行流量控制,我们可以设置limitApp属性来配置来源信息,它有三个选项:
  • default :表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
  • {some_origin_name} :表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者 caller1 的规则,那么当且仅当来自 caller1 对NodeA 的请求才会触发流量控制。
  • other :表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源 NodeA 配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。
对于同一个资源,可以配置多条规则,规则的生效顺序为: {some_origin_name} > other >default

根据调用链路入口限流

一个被限流的保护方法,可能来自于不同的调用链路,比如针对资源NodeA,入口 Entrance1 和Entrance2 的请求都调用到了资源 NodeA ,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 strategy 为 RuleConstant.STRATEGY_CHAIN ,同时设置 refResource 为Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经Entrance2 到来的调用

具有关系的资源流量控制

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

Sentinel控制台

sentine他有自己的监控平台,可以去官网下载然后安装就行,启动后他可以监控平台的运行情况,这个很简单,提供下网址自已进去按流程做就可以了

https://github.com/alibaba/Sentinel/releases

动态限流规则

 

 

 通过控制表可以知道可以配置很多流程规则,这些是存在内存中的,如果服务器重启了这些规则就会丢失,这些是肯定不能接受的,那么接下来要做的是如果持久化存储,把他做成动态规则;动态规则保存的地方有很多;例如zookeeper / apollo / etcd / redis / Consul / Eureka;下图是从官网上拉的图,下图说的流程很简单,步骤1说的就是有一个控制台这个控制台能够更改规则,然后可以将规则发送到Nacos/Zookeeper上,这样第一步保存就完成了;接下来就是应用端的动态更新规则了,说的就是2的配置的动态更新变化

动态数据源演示

导入pom

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
            <version>1.8.0</version>
        </dependency>

如下图所示,在上篇中我是把规则写在如下位置,如果配置规则多了放在这里维护也不方便,下面把这部分写死的规则给隐藏,通过sentinel的SPI的扩展进行优化

 

 通过前面篇幅可以知道竟然是spi的扩展就要创建如下东西

 

public class FlowRuleInitFunc  implements InitFunc {

  

    @Override
    public void init() throws Exception {
        initFlowRule();
        //loadClusterConfig();
       // registerFlowRule();
    }
    private static void initFlowRule(){
        List<FlowRule> rules=new ArrayList<> ();
        FlowRule flowRule=new FlowRule();
        flowRule.setResource("doTest"); //针对那个资源设置规则
        flowRule.setGrade( RuleConstant.FLOW_GRADE_QPS);//QPS或者并发数
        flowRule.setCount(5); //QPS=5
        rules.add(flowRule);
        FlowRuleManager.loadRules(rules);
    }

然后启动项目访问,通过JMeter进行压测可以看到如下结果

 

 

写到这里只是把配置放到了FlowRuleInitFunc  类中,还是没达到动态配置规则,接下来就是从远程服务器加载配置规则,我选择用nacos;在nacos上配置动态数据源

修改FlowRuleInitFunc类,从nacos上拉取配置
public class FlowRuleInitFunc  implements InitFunc {

    private final String nacosAddress="localhost:8848";
    private final String groupId="SENTINEL_GROUP";//nacos分组
    private final String dataId="springboot-sentinel";//nacos的data Id配置

    @Override
    public void init() throws Exception {
       // initFlowRule();
        //loadClusterConfig();
        registerFlowRule();
    }
    private static void initFlowRule(){
        List<FlowRule> rules=new ArrayList<> ();
        FlowRule flowRule=new FlowRule();
        flowRule.setResource("doTest"); //针对那个资源设置规则
        flowRule.setGrade( RuleConstant.FLOW_GRADE_QPS);//QPS或者并发数
        flowRule.setCount(5); //QPS=5
        rules.add(flowRule);
        FlowRuleManager.loadRules(rules);
    }

    private void registerFlowRule(){
        //从远程服务器上加载规则(nacos)
        ReadableDataSource<String, List<FlowRule>> flowRuleDs=
                new NacosDataSource<List<FlowRule>>(nacosAddress,groupId,dataId,
                        source-> JSON.parseObject(source,new TypeReference<List<FlowRule>> (){}));
        FlowRuleManager.register2Property(flowRuleDs.getProperty());
    }
}

启动项目验证,可以手动访问接口也可以用JMeter进行压测,可以看到监控面板上有出现配置的两个限流策略结果,说明动态配置成功

 

 

虽然上面的步骤已经实现了动态的配置,但是还是要在类中写代码,其实这个步骤在springcloud集成中也是不用的,下面写下在springcloud中通过配置文件实现动态过程;实现前先把前面的代码和services文件给隐藏掉

在和springcloud集成中只用一个配置信息就解决所有代码,配置如下:

spring.cloud.sentinel.datasource.ds1.nacos.server-addr=localhost:8848
spring.cloud.sentinel.datasource.ds1.nacos.data-id=springboot-sentinel
spring.cloud.sentinel.datasource.ds1.nacos.group-id=SENTINEL_GROUP
spring.cloud.sentinel.datasource.ds1.nacos.data-type=json
spring.cloud.sentinel.datasource.ds1.nacos.rule-type=flow
spring.cloud.sentinel.datasource.ds1.nacos.username=nacos
spring.cloud.sentinel.datasource.ds1.nacos.password=nacos

集成的内容搞完了,接下来要搞的就是另一件事,上面的一系列步骤完成了动态配置的持久化,但是又引来了一个新的问题,那就是现在sentinel的流控规则可以从nacos上读取没问题了,但新问题是如果我在sentinel上修改了流控规则后,nacos上怎么同步更新。这个问题如果要解决需要去改sentinel源码触发调用nacos的监听,由于今天想把这个专题结束,后面有空我补充下。

集群限流

在前面的所有案例中,只是基于Sentinel的基本使用和单机限流的使用,假如有这样一个场景,现在把provider部署了10个集群,希望调用这个服务的api的总的qps是100,意味着每一台机器的qps是10,理想情况下总的qps就是100.但是实际上由于负载均衡策略的流量分发并不是非常均匀的,就会导致总的qps不足100时,就被限了。在这个场景中,仅仅依靠单机来实现总体流量的控制是有问题的。所以最好是能实现集群限流。
集群流控中共有两种身份: 
  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

 下面就来写个demo来实现集群限流,新建一个maven工程sentinel-token-server

导入pom

    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-cluster-server-default</artifactId>
      <version>1.8.0</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-datasource-nacos</artifactId>
      <version>1.8.0</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-transport-simple-http</artifactId>
      <version>1.8.0</version>
    </dependency>

 下面是服务端作为一个token-server的一个核心服务包,这里面提供了一些API给我们,我们可以通过这个API进行调用,下面就是抄代码了

 

 

 

public class ClusterServer {
    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer=new SentinelDefaultTokenServer ();
        //手动载入namespace和serverTransportConfig的配置到ClusterServerConfigManager
        //集群限流服务端通信相关配置
        ClusterServerConfigManager.loadGlobalTransportConfig(
                new ServerTransportConfig ().setIdleSeconds(600).setPort(9999));
        //加载namespace集合列表() , namespace也可以放在配置中心
        ClusterServerConfigManager.loadServerNamespaceSet( Collections.singleton("App-Test"));
        tokenServer.start();
        //Token-client会上报自己的project.name到token-server。Token-server会根据namespace来统计连接数
    }
}

 

public class FlowRuleInitFunc implements InitFunc {

private final String nacosAddress="localhost:8848";
private final String groupId="SENTINEL_GROUP";//nacos分组
private final String dataId="-flow-rules";//nacos的data Id配置


@Override
public void init() throws Exception {
registerFlowRule();
}
private static void initFlowRule(){
List<FlowRule> rules=new ArrayList<> ();
FlowRule flowRule=new FlowRule ();
flowRule.setResource("doTest"); //针对那个资源设置规则
flowRule.setGrade( RuleConstant.FLOW_GRADE_QPS);//QPS或者并发数
flowRule.setCount(5); //QPS=5
rules.add(flowRule);
FlowRuleManager.loadRules(rules);
}

private void registerFlowRule(){
ClusterFlowRuleManager.setPropertySupplier( namespace->{
ReadableDataSource<String,List<FlowRule>> flowRuleDs=
new NacosDataSource<List<FlowRule>>(nacosAddress,groupId,namespace+dataId,
source-> JSON.parseObject(source,new TypeReference<List<FlowRule>>(){}));
return flowRuleDs.getProperty();
});
}
}

然后 启动maven项目在本地日志中可以发现端口已监听

 

 

 

 然后在nacos上根据FlowRuleInitFunc类中的dataId配置规则

 

然后在VM中加入下面话加入监控

 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-token-server

 

 然后启动maven项目运行,就可以发现控制面板上数据上来了

 

 

 然后来改springboot-sentinel项目,红框内配置删除,因为加载规则改成了要先拿tocker-server信息,所以修改FlowRuleInitFunc类;

public class FlowRuleInitFunc  implements InitFunc {

    private final String nacosAddress="localhost:8848";
    private final String groupId="SENTINEL_GROUP";//nacos分组
    private final String dataId="-flow-rules";//nacos的data Id配置

    private final String clusterServerHost="localhost";
    private final int clusterServerPort=9999;
    private final int requestTimeOut=20000;
    private final String appName="App-Test";

    @Override
    public void init() throws Exception {
       // initFlowRule();
        loadClusterConfig();
        registerFlowRule();
    }

    private static void initFlowRule(){
        List<FlowRule> rules=new ArrayList<> ();
        FlowRule flowRule=new FlowRule();
        flowRule.setResource("doTest"); //针对那个资源设置规则
        flowRule.setGrade( RuleConstant.FLOW_GRADE_QPS);//QPS或者并发数
        flowRule.setCount(5); //QPS=5
        rules.add(flowRule);
        FlowRuleManager.loadRules(rules);
    }
    //加载集群限流配置
    private void loadClusterConfig(){
        ClusterClientAssignConfig assignConfig=new ClusterClientAssignConfig();
        assignConfig.setServerHost(clusterServerHost); //放到配置中心
        assignConfig.setServerPort(clusterServerPort);
        ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
        ClusterClientConfig clientConfig=new ClusterClientConfig();
        clientConfig.setRequestTimeout(requestTimeOut);  //放到配置中心
        ClusterClientConfigManager.applyNewConfig(clientConfig);
    }
    //回退
    private void registerFlowRule(){
        //从远程服务器上加载规则(nacos)
        ReadableDataSource<String, List<FlowRule>> flowRuleDs=
                new NacosDataSource<List<FlowRule>>(nacosAddress,groupId,appName+dataId,
                        source-> JSON.parseObject(source,new TypeReference<List<FlowRule>> (){}));
        FlowRuleManager.register2Property(flowRuleDs.getProperty());
    }
}

因为是集群是根据应用名称统计的,所以修改VM开启两个

 

 修改启动类,声明状态

@SpringBootApplication
public class SpringbootSentinelApplication {

    public static void main(String[] args) {
        //设置应用状态,表示是客户端
        ClusterStateManager.applyState ( ClusterStateManager.CLUSTER_CLIENT );
//        initFlowRule();
        SpringApplication.run ( SpringbootSentinelApplication.class ,args );
    }

然后启动项目,就完成了集成

 

git代码:https://gitee.com/TongHuaShuShuoWoDeJieJu/sentinel-token-server.git

 

posted @ 2022-01-03 13:24  童话述说我的结局  阅读(642)  评论(0编辑  收藏  举报