Sentinel被称为分布式系统的流量防卫兵,是阿里开源流控框架,从服务限流、降级、熔断等多个维度保护服务。
Sentinel的核心功能就是通过根据可配置的资源保护规则来保护指定的资源。这里有两个核心概念:资源、规则和数据源。
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
数据源相关就不多说了,本文是以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() {
}
…………………………………………………………
首先到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是采用懒加载的方式,注册到仪表盘的服务被调用时,才会显示功能菜单。
maven增加依赖如下,版本取决于项目集成的alibabaCloud版本。
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.csp
sentinel-datasource-nacos
修改bootstrap.yml文件,配置如下:
配置的路径为:spring.cloud.sentinel。
/**
* yml配置中的flow、degrade、system等,均是Map的key
*/
public void setDatasource(Map datasource) {
this.datasource = datasource;
}
以下为配置文件的基本信息,一定要和bootstrap.yml中spring.cloud.sentinel.datasource.flow.nacos下的配置一致。配置格式选择json
配置内容(具体的限流规则)如下:
* 此处json的注释切记在使用时删掉,否则会出现json的转换异常
[
{
"resource":"updateNum", //资源名称
"limitApp":"default", //表示要限制哪些来源的调用,default表示全部限制
"grade":1, //表示阈值类型,取值参考RuleConstant类(0-线程数限流、1-QPS限流)
"count":1, //表示限流阈值
"strategy":0, //表示流控模式(直接、关联、链路)
"controlBehavior":0, //表示流控效果(快速失败、Warm Up、排队等待)
"clusterMode":false //是否集群
}
]
以上配置的可选项的常量类:com.alibaba.csp.sentinel.slots.block.RuleConstant
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 //是否集群
}
]
只针对从指定链路访问到本资源的请求做统计,判断是否达到限流条件时,开启限流。假如有两个链路:分别为/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 //是否集群
}
]
nacos配置的基本信息与限流的配置类似,以下仅展示具体的降级策略
* 此处json的注释切记在使用时删掉,否则会出现json的转换异常
[
{
"resource":"getGoods", // 表示资源名称
"count":1, // 表示阈值
"grade":2, // 表示降级策略,取值参考RuleConstant类(0-RT、1-异常比例、2-异常数)
"timeWindow":5 // 表示时间窗口
}
]
例如grade为RT,count为20,timeWindow为10,则资源1秒内平均响应时间超过20ms,则该服务在接下来的10秒中降级。
nacos基础信息配置同上,以下仅展示系统规则的配置。
* 此处json的注释切记在使用时删掉,否则会出现json的转换异常
[
{
"qps":1,
"avgRt":100,
"highestCpuUsage":0.2,
"highestSystemLoad":80,
"maxThread":1000
}
]
nacos基础信息配置同上,以下仅展示授权规则的配置。授权规则是根据调用方判断调用资源的请求是否应该被允许。Sentinel控制台提供了黑白名单的授权类型,如果配置了白名单,表示只允许白名单的应用调用该资源时通过;如果配置了黑名单,表示黑名单的应用调用该资源时不通过,其余的通过。
bootstrap.yml配置如下:
* 此处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,请求被限制。
此功能的应用场景,可在服务被第三方应用订阅时,分配来源标识,第三方应用需持标识访问,否则会被拦之门外
bootstrap.yml的配置如下:
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.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注解的属性如下:
以下为我的兜底代码:
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);
}
}
以上为单机流控的内容,如何做到集群流控,请看我的其他博文