Sentinel——流控规则

流控规则

流控规则是用于完成服务流控的。服务流控即对访问流量的控制,也称为服务限流。Sentine实现流控的原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的值时对再到来的请求进行进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

流量控制有两种阈值统计类型,QPS和并发线程数

QPS

设置流控规则

代码设置流控调用的方法,@SentinelResource(value = "get", fallback = "getFallBack", blockHandler = "getFlowFallBack")

package com.zjw.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.zjw.domain.Depart;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * <p>
 * 部门表 前端控制器
 * </p>
 *
 * @author zjw
 * @since 2023-11-20
 */
@RestController
@RequestMapping("/depart")
@Slf4j
public class DepartController {

    @Resource
    private RestTemplate restTemplate;

    private static final String PROVIDER_URL = "http://depart-provider/depart";

    /**
     * 根据id查询部门
     */
    // 发生异常,跳到500空白页
//    @SentinelResource(value = "get")
    // 发生异常会降级,调用getFallBack方法
//    @SentinelResource(value = "get", fallback = "getFallBack")
    //发生异常会降级,调用getFallBack方法, 触发流控,会调用流控的getFlowFallBack方法
    @SentinelResource(value = "get", fallback = "getFallBack", blockHandler = "getFlowFallBack")
    // 发生异常会降级,调用getFallBack方法,
    @GetMapping("/get/{id}")
    public Depart get(@PathVariable Long id) {
        return restTemplate.getForObject(PROVIDER_URL + "/get/" + id, Depart.class);
    }

    /**
     * 服务流控使用的方法.
     * 需要指定BlockException参数,否则调用降级方法
     */
    public Depart getFlowFallBack(Long id, BlockException e) {
        log.info("id = " + id);
        log.info("exception = " + e.getMessage());
        Depart depart = new Depart();
        depart.setId(id);
        depart.setName("flow fall back");
        return  depart;
    }

    /**
     * 服务降级使用的方法
     */
    public Depart getFallBack(Long id, Throwable t) {
        log.info("id = " + id);
        log.info("throwable = " + t.getMessage());
        Depart depart = new Depart();
        depart.setId(id);
        depart.setName("no this depart");
        return  depart;
    }

控制台设置流控的规则:

如果调用的服务停止了,会触发降级,调用getFallBack方法。

当请求QPS超过3时会触发流控规则,调用getFlowFallBack方法。

api设置流控规则

通过控制台设置的流控规则会在服务重启后失效,可以通过sentinel的api在代码中指定规则

package com.zjw;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.ArrayList;
import java.util.List;

@SpringBootApplication
public class ConsumerFlowRule8080Application {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerFlowRule8080Application.class, args);
        //初始化流控规则
        initFlowRule();
    }

    private static void initFlowRule() {
        FlowRuleManager.loadRules(configFlowRule());
    }

    private static List<FlowRule> configFlowRule() {
        List<FlowRule> flowRuleList = new ArrayList<>();
        FlowRule rule = new FlowRule();
        // 资源名
        rule.setResource("get");
        // 针对来源
        rule.setLimitApp("default");
        // 阈值类型 (并发线程数 0: thread count, 1: QPS).
        // RuleConstant.FLOW_GRADE_THREAD 0
        // RuleConstant.FLOW_GRADE_QPS 1
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 单机阈值
        rule.setCount(3);
        flowRuleList.add(rule);
        return flowRuleList;
    }
}

资源实体指定流控规则

package com.zjw.controller;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.zjw.domain.Depart;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * <p>
 * 部门表 前端控制器
 * </p>
 *
 * @author zjw
 * @since 2023-11-20
 */
@RestController
@RequestMapping("/depart")
@Slf4j
public class DepartController {

    @Resource
    private RestTemplate restTemplate;

    private static final String PROVIDER_URL = "http://depart-provider/depart";

    @GetMapping("/get2/{id}")
    public Depart get2(@PathVariable Long id) {
        Entry entry = null;
        try {
            entry = SphU.entry("xxx");
            return restTemplate.getForObject(PROVIDER_URL + "/get/" + id, Depart.class);
        } catch (BlockException e) {
            log.info("id = " + id);
            log.info("exception = " + e.getMessage());
            Depart depart = new Depart();
            depart.setId(id);
            depart.setName("entry flow fall back");
            return  depart;
        } finally {
            if (entry != null)
                entry.exit();
        }
    }
}

控制台设置

触发流控

并发线程数

隔离方案:

A 线程池隔离

系统为不同的提供者资源设置不同的线程池来隔离业务自身之间的资源争抢。该方案隔离性较好,但需要创建的线程池及线程数量太多,系统消耗较大。当请求线程到达后,会从线程池中获取到一个新的执行线程去完成提供者的调用。由请求线程到执行线程的上下文切换时间开销较大。特别是对低延时的调用有比较大的影响。

B 信号量隔离

系统为不同的提供者资源设置不同的计数器。每增加一个该资源的调用请求,计数器就变化一次。当达到该计数器阙值时,再来的请求将被限流。该方式的执行线程与请求线程是同一个线程,不存在线程上下文切换的问题,更不存在很多的线程池创建与线程创建问题。“也正因为请求线程与执行线程没有分离,所以,其对于提供者的调用无法实现异步,执行效率降低,且对于依赖资源的执行超时控制不方便。

Sentinel 隔离方案

Sentinel 并发线程数控制也属于隔离方案,但不同于以上两种隔离方式,是对以上两种方案的综合与改进,或者说更像是线程池隔离。其也将请求线程与执行线程进行了分离,但 Sentinel 不负责创建和管理线程池,而仅仅是简单统计当前资源请求占用的线程数目。如果对该资源的请求占用的线程数量超出了阙值,则可以立即拒绝再新进入的请求。

流控模式-关联


当关联的资源触(主业务)发流控,如list资源QPS大于3,会将get资源也进行流控。

流控模式-链路

当对一个资源有多种访问路径时,可以对某一路劲的访问进行限流,而其他访问路径不限流。

application.yml配置

server:
  port: 8081

spring:
  datasource:
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/sc?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf-8&rewriteBatchedStatements=true
  application:
    name: depart-provider # 微服务名称
  cloud:
    nacos:
      discovery:
        server-addr: nacos-local:8848 # nacos注册中心地址
        username: nacos # 用户名密码
        password: nacos
    sentinel:   # sentinel配置
      eager: true # 饥饿加载,默认false
      # 默认true,会收敛所有URL入口,即对于同一资源的请求URL到Sentinel后会
      # 统一为一个URL。即Sentinel只关心被访问资源,不需要区分请求的来源
      # 设置为false后,就可以区分请求来源了
      web-context-unify: false
      transport:
        port: 8719  # sentinel内部启动的http服务
        dashboard: localhost:8888 # sentinel服务
mybatis-plus:
  # 别名包扫描路径
  type-aliases-package: com.zjw.domain
  global-config:
    db-config:
      # 设置id字段为自增长
      id-type: auto
      logic-delete-field: deleted #逻辑删除的字段
      logic-delete-value: 1 # 已经逻辑删除的记录该字段值
      logic-not-delete-value: 0 # 未被逻辑删除的记录该字段值
  configuration:
    # mybatis二级缓存,默认为true,开启
    cache-enabled: false
    # 是否开启自动驼峰命名规则,默认为true,开启
    map-underscore-to-camel-case: true
    # 设置控制台日志打印,默认不显示SQL语句
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 枚举字段加上了@EnumValue注解后默认使用的是MybatisEnumTypeHandler,否则默认为mybatis EnumTypeHandler
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  # Mapper 所对应的 XML 文件位置,默认为“classpath*:/mapper/**/*.xml”
  mapper-locations:
    - classpath*:/mapper/**/*.xml

现在service有一个查询所有的服务list,我们加上流控@SentinelResource

package com.zjw.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zjw.domain.Depart;
import com.zjw.mapper.DepartMapper;
import com.zjw.service.IDepartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 部门表 服务实现类
 * </p>
 * @since 2023-11-20
 */
@Service
@Slf4j
public class DepartServiceImpl extends ServiceImpl<DepartMapper, Depart> implements IDepartService {

    @SentinelResource(value = "list", blockHandler = "listFlowFallBack")
    @Override
    public List<Depart> list() {
        return super.list();
    }

    public List<Depart> listFlowFallBack(BlockException e) {
        log.info("流控调用");
        List<Depart> list = new ArrayList<>();
        Depart depart = new Depart();
        depart.setName("listFallBack");
        list.add(depart);
        return  list;
    }
}

在controller层有两个方法调用list

package com.zjw.controller;


import com.zjw.domain.Depart;
import com.zjw.service.IDepartService;
import jakarta.annotation.Resource;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 部门表 前端控制器
 * </p>
 *
 * @since 2023-11-20
 */
@RestController
@RequestMapping("/depart")
public class DepartController {

    @Resource
    private IDepartService departService;

    /**
     * 查询所有部门
     */
    @GetMapping("/list")
    public List<Depart> list() {
        return departService.list();
    }

    @GetMapping("/all")
    public List<Depart> all() {
        return departService.list();
    }

如果我们需要对通过/depart/list的方法进行流控,可以在sentinel控制台进行如下设置:

测试:

访问http://localhost:8081/depart/list会被流控,访问http://localhost:8081/depart/all并不会被流控。

控制效果

当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝Warm Up匀速排队(排队等待)。对应 FlowRule 中的 controlBehavior 字段。[1]

注意:若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。

快速失败

直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。具体的例子参见 FlowQpsDemo[2]

Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

排队等待

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo

该方式的作用如下图所示:

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

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

三种控制效果API实现


    private static void initFlowRule() {
        FlowRuleManager.loadRules(configQpsFlowRule());
    }

    // QPS 控制效果
    private static List<FlowRule> configQpsFlowRule() {
        List<FlowRule> flowRuleList = new ArrayList<>();
        FlowRule rule = new FlowRule();
        // 资源名
        rule.setResource("get");
        // 针对来源
        rule.setLimitApp("default");
        // 阈值类型 (并发线程数 0: thread count, 1: QPS).
        // RuleConstant.FLOW_GRADE_THREAD 0
        // RuleConstant.FLOW_GRADE_QPS 1
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 单机阈值
        rule.setCount(3);
        flowRuleList.add(rule);
        return flowRuleList;
    }

    // warm up 控制效果
    private static List<FlowRule> configWarmUpFlowRule() {
        List<FlowRule> flowRuleList = new ArrayList<>();
        FlowRule rule = new FlowRule();
        // 资源名
        rule.setResource("get");
        // 针对来源
        rule.setLimitApp("default");
        // 阈值类型 (并发线程数 0: thread count, 1: QPS).
        // RuleConstant.FLOW_GRADE_THREAD 0
        // RuleConstant.FLOW_GRADE_QPS 1
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 单机阈值
        rule.setCount(3);
        // 流控效果
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        // 预热时长 s
        rule.setWarmUpPeriodSec(10);
        flowRuleList.add(rule);
        return flowRuleList;
    }

    // 排队等待 控制效果
    private static List<FlowRule> configPaceFlowRule() {
        List<FlowRule> flowRuleList = new ArrayList<>();
        FlowRule rule = new FlowRule();
        // 资源名
        rule.setResource("get");
        // 针对来源
        rule.setLimitApp("default");
        // 阈值类型 (并发线程数 0: thread count, 1: QPS).
        // RuleConstant.FLOW_GRADE_THREAD 0
        // RuleConstant.FLOW_GRADE_QPS 1
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 单机阈值
        rule.setCount(3);
        // 流控效果
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        // 预热时长 20s
        rule.setMaxQueueingTimeMs(20 * 1000);
        flowRuleList.add(rule);
        return flowRuleList;
    }

来源流控

来源名称可以在请求参数、请求头或Cookie 中通过 key-value 形式指定,而sentinel 提供了一个RequestOriginParser 的请求解析器,sentinel可以从该解析器中获取到请求中携带的来源,然后将流控规则应用于获取到的请求来源之上。

package com.zjw.parser;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * @since 2023/12/04 18:33
 */
@Component
@Slf4j
public class DepartRequestOriginParser implements RequestOriginParser {

    @Override
    public String parseOrigin(HttpServletRequest request) {
        String source = request.getParameter("source");
        if(!StringUtils.hasText(source)){
            //设置为默认来源
            source = "guest";
        }
        log.info("source = " + source);
        return source;
    }
}

controller

package com.zjw.controller;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.zjw.domain.Depart;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * <p>
 * 部门表 前端控制器
 * </p>
 *
 * @since 2023-11-20
 */
@RestController
@RequestMapping("/depart")
@Slf4j
public class DepartController {

    @Resource
    private RestTemplate restTemplate;

    private static final String PROVIDER_URL = "http://depart-provider/depart";

    /**
     * 根据id查询部门
     */
    //发生异常会降级,调用getFallBack方法, 触发流控,会调用流控的getFlowFallBack方法
    @SentinelResource(value = "get", fallback = "getFallBack", blockHandler = "getFlowFallBack")
    @GetMapping("/get/{id}")
    public Depart get(@PathVariable Long id) {
        return restTemplate.getForObject(PROVIDER_URL + "/get/" + id, Depart.class);
    }

    /**
     * 服务流控使用的方法.
     * 需要指定BlockException参数,否则调用降级方法
     */
    public Depart getFlowFallBack(Long id, BlockException e) {
        log.info("id = " + id);
        log.info("exception = " + e.getMessage());
        Depart depart = new Depart();
        depart.setId(id);
        depart.setName("flow fall back");
        return  depart;
    }

    /**
     * 服务降级使用的方法
     */
    public Depart getFallBack(Long id, Throwable t) {
        log.info("id = " + id);
        log.info("throwable = " + t.getMessage());
        Depart depart = new Depart();
        depart.setId(id);
        depart.setName("no this depart");
        return  depart;
    }
}

sentinel控制台配置

对于来源为guest的进行流控限制。

参考:


  1. https://github.com/alibaba/Sentinel/wiki/流量控制 ↩︎

  2. https://github.com/alibaba/Sentinel/blob/master/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowQpsDemo.java ↩︎

posted @ 2023-12-04 14:24  雨中遐想  阅读(61)  评论(0编辑  收藏  举报