当前位置: 首页>后端>正文

Sentinel整合Dubbo限流实战(二)

限流的基本认识

场景分析

一个互联网产品,打算搞一次大促来增加销量以及曝光。公司的架构师基于往期的流量情况做了一个活动流量的预估,然后整个公司的各个技术团队开始按照这个目标进行设计和优化,最终在大家不懈的努力之下,达到了链路压测的目标流量峰值。到了活动开始那天,大家都在盯着监控面板,看着流量像洪水一样涌进来。由于前期的宣传工作做得很好,使得这个流量远远超过预估的峰值,后端服务开始不稳定,CPU、内存各种爆表。部分服务开始出现无响应的情况。最后,整个系统开始崩溃,用户无法正常访问服务。

引入限流

在10.1黄金周,各大旅游景点都是人满为患。所有有些景点为了避免出现踩踏事故,会采取限流措施。
那在架构场景中,是不是也能这么做呢?针对这个场景,能不能够设置一个最大的流量限制,如果超过这个流量,我们就拒绝提供服务,从而使得我们的服务不会挂掉。当然,限流虽然能够保护系统不被压垮,但是对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。但是相比于全部不可用,有损服务是最好的一种解决办法。

限流的作用

除了前面说的限流使用场景之外,限流的设计还能防止恶意请求流量、恶意攻击。
所以,限流的基本原理是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或者告知资源没有了)、排队或等待(秒杀、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。
一般互联网企业常见的限流有:限制总并发数(如数据库连接池、线程池)、限制瞬时并发数(nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他的还有限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
有了限流,就意味着在处理高并发的时候多了一种保护机制,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务。

常见的限流算法

滑动窗口
发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口。目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。如下图所示:

Sentinel整合Dubbo限流实战(二),第1张
微信图片_20200512190054.png

图中的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项目

Sentinel整合Dubbo限流实战(二),第2张
微信图片_20200512193542.png

添加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 控制台实时进行推送。如下图所示:


Sentinel整合Dubbo限流实战(二),第3张
微信图片_20200512202412.png

搭建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。同时运行两个程序。


https://www.xamrdz.com/backend/3r91924313.html

相关文章: