sentinel-单机流量控制

1 简介

        Sentinel被称为分布式系统的流量防卫兵,是阿里开源流控框架,从服务限流、降级、熔断等多个维度保护服务。

2 重要概念

        Sentinel的核心功能就是通过根据可配置的资源保护规则来保护指定的资源。这里有两个核心概念:资源、规则和数据源。

2.1 资源

        资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

2.2 规则

        围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

2.3 数据源

      数据源相关就不多说了,本文是以nacos作为持久化例子的,其他的可参考这个类:com.alibaba.cloud.sentinel.SentinelProperties#setDatasource  

public void setDatasource(Map datasource) {
		this.datasource = datasource;
	}
/**
 * 从以下代码可以看出,sentinel的持久化有很多途径,例如文件、nacos、Zookeeper、redis等
 */
public class DataSourcePropertiesConfiguration {

	private FileDataSourceProperties file;

	private NacosDataSourceProperties nacos;

	private ZookeeperDataSourceProperties zk;

	private ApolloDataSourceProperties apollo;

	private RedisDataSourceProperties redis;

	private ConsulDataSourceProperties consul;

	public DataSourcePropertiesConfiguration() {
	}


…………………………………………………………

2.4 名词解释

  • QPS:每秒的请求数
  • RT:表示该资源1秒内处理请求的平均响应时间

3 下载和运行

        首先到http://github.com/alibaba/Sentinel/releases页面下载dashboard(仪表盘)的jar包,因为是springboot工程,可以直接使用java命令启动,命令如下:

java -jar -Dserver.port = 7070 sentinel-dashoard-1.7.1.jar

默认地址为:http://127.0.0.1:7070,默认账号密码:sentine/sentinel

Sentinel是采用懒加载的方式,注册到仪表盘的服务被调用时,才会显示功能菜单。

4 项目集成

maven增加依赖如下,版本取决于项目集成的alibabaCloud版本。



    com.alibaba.cloud
    spring-cloud-starter-alibaba-sentinel




    com.alibaba.csp
    sentinel-datasource-nacos

4.1 sentinel框架的配置

修改bootstrap.yml文件,配置如下:

sentinel-单机流量控制_第1张图片

配置的路径为:spring.cloud.sentinel。

  • 配置中的flow、degrade、system都是自定义的,在源码中,这些都是Map的key,但建议还是有意义的名称最好,以下为sentinel设置数据源的部分源码。
/**
 * yml配置中的flow、degrade、system等,均是Map的key
 */
public void setDatasource(Map datasource) {
   this.datasource = datasource;
}
  • rule-type的值,参考:com.alibaba.cloud.sentinel.datasource.RuleType
4.1.1 nacos中的限流配置(flow)。

以下为配置文件的基本信息,一定要和bootstrap.yml中spring.cloud.sentinel.datasource.flow.nacos下的配置一致。配置格式选择json

sentinel-单机流量控制_第2张图片

配置内容(具体的限流规则)如下:

* 此处json的注释切记在使用时删掉,否则会出现json的转换异常

[
    {
        "resource":"updateNum",   //资源名称
        "limitApp":"default",  //表示要限制哪些来源的调用,default表示全部限制
        "grade":1,             //表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
        "count":1,             //表示限流阈值
        "strategy":0,          //表示流控模式(直接、关联、链路)
        "controlBehavior":0,   //表示流控效果(快速失败、Warm Up、排队等待)
        "clusterMode":false    //是否集群
    }
]
  • resource:为资源名称
  • limitApp:表示要限制哪些来源的调用(其实也是资源名称,资源之间有互相调用的可能)
  • grade:表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
  • count:表示限流阈值
  • strategy:表示流控模式(直接、关联、链路),可选值为:0、1、 2。0为直接(Direct);1为关联(Relate);2为链路(Chain)
  • controlBehavior:表示流控效果(快速失败、Warm Up、排队等待),可选值为:0、1、2、3。0为默认,即快速失败(直接拒绝);1为预热(Warm Up);2为排队等待(速率限制器),3为预热+排队等待
  • clusterMode:是否集群(布尔值),true为集群;false为非集群

以上配置的可选项的常量类:com.alibaba.csp.sentinel.slots.block.RuleConstant

4.1.1.1 strategy属性-“关联”

        strategy属性-“直接“就不多做介绍了,非常好理解,就是指定的资源请求数达到阈值,该资源就被限流了,此处主要说明下“关联”,关联的意思是:如果关联资源达到限流条件,就限流自身。

举例:订单生成接口完成后,需要调用支付接口,如果支付接口达到限流条件,那关联支付接口的订单生成接口会限流。

配置如下:

[
    {
        "resource":"pay",      //资源名称
        "limitApp":"default",  //表示要限制哪些来源的调用,default表示全部限制
        "grade":1,             //表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
        "count":1,             //表示限流阈值
        "strategy":0,          //表示流控模式(直接、关联、链路)
        "controlBehavior":0,   //表示流控效果(快速失败、Warm Up、排队等待)
        "clusterMode":false    //是否集群
    },
    {
        "resource":"addOrder", //资源名称
        "limitApp":"default",  //表示要限制哪些来源的调用,default表示全部限制
        "grade":1,             //表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
        "count":10,             //表示限流阈值
        "strategy":1,          //表示流控模式(直接、关联、链路)
        "refResource":"pay",   //关联资源
        "controlBehavior":0,   //表示流控效果(快速失败、Warm Up、排队等待)
        "clusterMode":false    //是否集群
    }
]
4.1.1.2 strategy属性-“链路”

        只针对从指定链路访问到本资源的请求做统计,判断是否达到限流条件时,开启限流。假如有两个链路:分别为/query访问/queryUsers,/save访问/queryUsers,我们只限制从/query访问的请求。

/query ---> /queryUsers

/save ---> /queryUsers

[
    {
        "resource":"queryUsers",      //资源名称
        "limitApp":"default",  //表示要限制哪些来源的调用,default表示全部限制
        "grade":1,             //表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
        "count":1,             //表示限流阈值
        "strategy":2,          //表示流控模式(直接、关联、链路)
        "refResource":"query",   //入口资源(字段名称与关联资源一致)
        "controlBehavior":0,   //表示流控效果(快速失败、Warm Up、排队等待)
        "clusterMode":false    //是否集群
    }
]
4.1.1.3 流控效果(controlBehavior)
  1. 快速失败:某些不重要的服务接口,例如订单的物流服务,并不需要数据的强一致性,事后再补也可以,为了不让物流服务影响到订单生成、支付等服务,可进行快速失败,无需重试机制等,保证下订单的高可用。
  2. Warm Up(预热):主要用于服务启动初期或者长时间无请求后突然有大量请求到来的情况。在这种模式下,Sentinel不会立即对所有的请求进行严格的流量控制,而是允许一定的请求通过以逐步增加系统的负载,直到达到预设的稳定阈值。预热模式会根据一个冷加载因子(coldFactor,默认为3)来计算初始的阈值。初始阈值会被设定为流控阈值除以冷加载因子。随着请求的处理和时间的推移,阈值会逐渐增加,直到达到设定的流控阈值。这个过程就是所谓的“预热”。预热模式有助于避免在服务刚启动或从空闲状态恢复时,由于瞬间流量过大而导致的服务不稳定或者拒绝服务的情况。通过平滑地增加处理请求的能力,系统可以更从容地应对流量的增长,提供更好的服务体验。同时,预热模式也支持设置预热的时间周期(warmUpPeriodSec),在这个时间内,阈值会按照预设的策略逐渐增加到设定的流控阈值
  3. 排队等待:将超过阈值之外的请求,放入队列中等待。
4.1.2 nacos中降级配置(degrade)

        nacos配置的基本信息与限流的配置类似,以下仅展示具体的降级策略

* 此处json的注释切记在使用时删掉,否则会出现json的转换异常

[
    {
        "resource":"getGoods",  // 表示资源名称
        "count":1,      // 表示阈值  
        "grade":2,      // 表示降级策略,取值参考RuleConstant类(0-RT、1-异常比例、2-异常数)
        "timeWindow":5  // 表示时间窗口
    }
]
  • grade:0为RT(资源1秒内处理请求的平均响应时间)、1为异常比例(默认间隔时间为1秒)、2为异常数(在最后60秒内按业务异常计数降级。)
  • timeWindow:降级的持续时间。

       例如grade为RT,count为20,timeWindow为10,则资源1秒内平均响应时间超过20ms,则该服务在接下来的10秒中降级。

4.1.3 在nacos中配置系统规则(system)

        nacos基础信息配置同上,以下仅展示系统规则的配置。

* 此处json的注释切记在使用时删掉,否则会出现json的转换异常

[
    {
        "qps":1,                 
        "avgRt":100,              
        "highestCpuUsage":0.2,    
        "highestSystemLoad":80,    
        "maxThread":1000          
    }
]  
  • highestSystemLoad:仅对Linux/Unix系统有效,当系统负载超过阈值,且并发线程数超过系统容量时触发。同时应设置avgRT和qps进行协调(设置-1为不限制)
  • highestCpuUsage: 当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • avgRT: 当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • maxThread: 当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • qps: 当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
4.1.4 在nacos中配置授权规则(authority)

        nacos基础信息配置同上,以下仅展示授权规则的配置。授权规则是根据调用方判断调用资源的请求是否应该被允许。Sentinel控制台提供了黑白名单的授权类型,如果配置了白名单,表示只允许白名单的应用调用该资源时通过;如果配置了黑名单,表示黑名单的应用调用该资源时不通过,其余的通过。

bootstrap.yml配置如下:

sentinel-单机流量控制_第3张图片

* 此处json的注释切记在使用时删掉,否则会出现json的转换异常

[
    {
    "resource": "getUserInfo", //资源名称
    "limitApp": "order",         //流控应用,多个应用以","隔开
    "strategy": 0                //0为白名单,1为黑名单
    }
]

以上配置完成之后,需创建一个类,实现RequestOriginParser接口,用于获取参数,其后将返回结果值交给Sentinel流控匹配处理,具体代码如下:

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.jc.core.exception.ServiceException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class CustomRequestOriginParser implements RequestOriginParser {


    @Override
    public String parseOrigin(HttpServletRequest request) {
        String origin = request.getHeader("origin");//区分来源:本质通过request域获取来源标识
        if(StringUtils.isEmpty(origin)){
            throw new ServiceException("请求头中的「origin」不能为空!");
        }
        return origin;//返回结果交给sentinel流控匹配处理
    }
}

以上配置示例:

请求地址:http://localhost/getUserInfo,请求头添加origin=order,返回结果通过,返回正确数据。

请求地址:http://localhost/getUserInfo,请求头添加origin=dept,请求被限制。

此功能的应用场景,可在服务被第三方应用订阅时,分配来源标识,第三方应用需持标识访问,否则会被拦之门外

4.1.5 热点规则

bootstrap.yml的配置如下:

sentinel-单机流量控制_第4张图片

nacos配置的基本信息同上,以下仅展示授权规则的配置,此处json的注释切记在使用时删掉,否则会出现json的转换异常。热点规则主要是为了防止某些参数被大量访问,例如秒杀商品华为mete 60 Pro比较火爆,该商品会查询的并发量比较高。可用如下配置进行限流。

[
    {
        "resource":"getGoods"   //资源名称
        "paramIdx":0,           //热点参数的索引
        "count":"50",           //阈值
        "grade":"qps",          //默认为qps
        "durationInSec":5,      //统计窗口时间长度(例如,5秒内的QPS超过阈值1)
        "controlBehavior":0,    //默认为0(快速失败),可选值同4.1.1
        "maxQueueingTimeMs":1000//排队最大等待时常,单位毫秒
        "paramFlowItemList":[      //参数例外配置,例如个别商品比较热门,可以单独在此配置
            {                      //索引为0的参数值为10时,阈值为10
               "object":1,         //参数值为1
               "count":10          //例外阈值:10
               "classType":"long"  //参数类型                           
            }        
        ],
        "clusterMode":false     //为true,需配置集训相关配置,详情见后续章节
    }
]

4.2 资源的定义

        上述4.1章节里的配置,均有个字段resource(资源名称),那这个名称又是如何和我们系统里的资源结合起来的呢?看下面代码:

package com.jc.shop.warehouse.web;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson2.JSONObject;
import com.jc.core.domain.AjaxResult;
import com.jc.shop.biz.vo.PreBuyGoods;
import com.jc.shop.biz.vo.ShoppingCart;
import com.jc.shop.warehouse.domain.Goods;
import com.jc.shop.warehouse.service.IGoodsService;
import com.jc.shop.warehouse.web.handler.GoodsBlockHandler;
import com.jc.shop.warehouse.web.handler.GoodsFallbackHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 商品库存控制层
 */
@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private IGoodsService service;

    @PostMapping("/add")
    public AjaxResult addGoods(@RequestBody JSONObject goods){
        int flag = service.insert(goods);
        if(flag > 0){
            return AjaxResult.success("添加成功!");
        }else{
            return AjaxResult.error("商品添加失败,请联系管理员!");
        }
    }

    @PostMapping("/getAll")
    public AjaxResult getGoodsList(@RequestBody JSONObject jsonObject){

        return AjaxResult.success(service.findList(jsonObject));
    }

    @SentinelResource(value = "getGoods",blockHandlerClass = GoodsBlockHandler.class,
            blockHandler = "blockHandle",fallbackClass = GoodsFallbackHandler.class,fallback = "fallbackHandle")
    @GetMapping("/get/{id}")
    public AjaxResult getGoods(@PathVariable("id")Long id){
        return AjaxResult.success(service.findById(id));
    }

    @SentinelResource(value = "getInventory",blockHandlerClass = GoodsBlockHandler.class,
            blockHandler = "blockHandle",fallbackClass = GoodsFallbackHandler.class,fallback = "fallbackHandle")
    @GetMapping("/inventory/{id}")
    public AjaxResult getInventory(@PathVariable("id")Long id){
        int num = service.getGoodsNum(id);
        JSONObject result = new JSONObject();
        result.put("goodsNum",num);
        return  AjaxResult.success("查询成功!",result);
    }

    @SentinelResource(value = "updateNum",blockHandlerClass = GoodsBlockHandler.class,
            blockHandler = "blockHandle",fallbackClass = GoodsFallbackHandler.class,fallback = "fallbackHandle")
    @PostMapping("/updateNum")
    public AjaxResult updateNum(@RequestBody ShoppingCart cart){

        List goodsList = service.updateNum(cart);
        return AjaxResult.success(goodsList);
    }

}

        代码中的有用到注解@SentinelResource,这个注解就是用来定义资源的,在controller层的方法上添加该注解,就可以被sentinel认定为一个资源。当然,按照官方的说法,应用程序的任何内容都可以被认定为资源。

@SentinelResource注解的属性如下:

  • value:资源名称
  • blockHandlerClass:该属性与blockHandler属性搭配使用,用于请求被sentinel限流、降级、等措施后的兜底方法
  • blockHandler:兜底方法,若被sentinel限流(即抛出的异常为BlockException的子类),则进入该方法。
  • fallbackClass和fallback属性和上面两个属性的用法和功能类似,不过该属性主要是作为出现异常后,进行兜底的和统一异常处理的注解@RestControllerAdvice的效果一样。

以下为我的兜底代码:

GoodsBlockHandler.java
package com.jc.shop.warehouse.web.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson2.JSONObject;
import com.jc.core.domain.AjaxResult;
import org.springframework.stereotype.Component;

/**
 * 若超过流量限制,则会使用以下方法,该类的方法必须为静态方法
 */
@Component
public class GoodsBlockHandler {

    public static AjaxResult blockHandle(JSONObject jsonObject,BlockException e){

        System.out.println(jsonObject.toJSONString());
        return AjaxResult.error(429,"已超过服务器最大承受能力,限流规则:"+e.getRuleLimitApp());
    }
}
GoodsFallbackHandler.java
package com.jc.shop.warehouse.web.handler;

import com.alibaba.fastjson2.JSONObject;
import com.jc.core.domain.AjaxResult;
import org.springframework.stereotype.Component;

/**
 * 若发生异常,则调用以下方法,该类的方法需为静态方法。
 */
@Component
public class GoodsFallbackHandler {

    public static AjaxResult fallbackHandle(JSONObject jsonObject,Exception e){

        System.out.println(jsonObject.toJSONString());
        return AjaxResult.error(500,e.getMessage());
    }
}

        当然,也可以使用全局异常处理,不过这两处代码只会使用一个,除非异常处理完之后,再抛出去,让外面再处理一遍。

全局异常处理的代码如下:

* 重点看方法:handleBlockException(BlockException,HttpServletRquest);

package com.jc.core.exception;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.jc.core.domain.AjaxResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice
public class GlobalExceptionHandle {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandle.class);

    /**
     * 请求方式不支持
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
                                                          HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request)
    {
        log.error(e.getMessage(), e);
        Integer code = e.getCode();
        return ObjectUtils.isEmpty(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生未知异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 拦截Sentinel的异常
     */
    @ExceptionHandler(BlockException.class)
    public AjaxResult handleBlockException(BlockException e, HttpServletRequest request)
    {
        String msg = null;
        if (e instanceof FlowException) {
            msg = "因流量过大,已进入限流中,请稍后重试!";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
        }else if(e instanceof SystemBlockException){
            msg = "超过系统负载的阈值";
        }
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生未知异常.", requestURI, e);
        return AjaxResult.error(msg);
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生系统异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 自定义验证异常
     */
    @ExceptionHandler(BindException.class)
    public AjaxResult handleBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return AjaxResult.error(message);
    }

    /**
     * 自定义验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return AjaxResult.error(message);
    }
}

以上为单机流控的内容,如何做到集群流控,请看我的其他博文

你可能感兴趣的:(微服务,sentinel,java,开发语言)