最近公司里面在进行微服务开发,因为有使用到限流降级,所以去调研学习了一下Sentinel,在这里做一个记录。
Sentinel官方文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
一、Sentinel的作用
1、Sentinel是什么
Sentinel---分布式系统的流量卫兵。
主要面向分布式架构的流量控制产品。以流量为切入点,从流量控制、熔断降级、系统负载保护等各个维度对服务提供保护。官方称其为轻量级产品。
2、基础概念
2.1、资源:Sentinel的核心概念,一切需要Sentinel保护的东西都称为资源。它可以是一段代码、由应用程序提供的服务、或由应用程序调用的其它应用程序提供的服务。在程序中最直观的呈现就是由Sentinel Api包围起来的一段代码。
2.2、规则:对资源的保护规则。如流量控制规则、熔断降级规则、系统保护规则等。所有规则都是可以实时动态调整的。
2.3、埋点:定义资源的过程。
3、基本用途
3.1、流量控制
因为流量是不规则的,在流量高峰期,大批量流量瞬间涌入系统会冲垮服务,这个时候我们需要限制流量对系统进行保护;同时在高峰期,如果直接丢弃掉超过服务承载能力的所有流量,而在流量低谷期又只有很少的流量或者无流量,那么此时服务又是一种资源浪费,并且也无无法给用户一个很好的体验感,因为我们需要将流量高峰期的部分流量分流到低谷期,而不是在高峰期全数丢弃超过服务负载能力的流量。
其原理就是监控应用流量的QPS或并发线程数,当达到指定的阈值时对流量进行控制。
流控规则在Sentinel控制台的直观体现。
流控模式:
直接:当接口达到限流条件时,开启限流;
关联:当关联的资源达到限流条件时,开启限流;适合做应用让步;比如一个查询的接口添加关联限流,关联限流资源为一个更新的接口,当更新的接口达到阈值时,开启查询接口的限流,为更新接口让步服务器资源。
比如说对于订单,现在有两种请求,一种是使用更新订单量请求【QUERY_ORDER_NUM】,另一种是查询请求【UPDATE_ORDER_NUM】。
当正处于订单使用高峰期,更新请求达到了设定阈值,这时可以暂时降低订单查询服务的可用性,优先保障订单扣减逻辑的执行。
链路:当从某个接口过来的资源达到限流条件时,开启限流。这是一种更精细化的资源管理方式。
比如说定义一个资源 【SAY_HELLO】:
且同时有两个接口 【/friendly】【/haughty】都调用该资源:
这时我们配置SAY_HELLO的入口资源为 【/friendly】,那么当【/friendly】调用超过阈值时就会触发限流开启,而【/haughty】则不会触发。
流控效果
快速失败:当请求达到限流阈值的时候,后续请求会立即拒绝,拒绝方式就是抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
Warm Up(预热/冷启动):当系统长期处于低水位的情况下,而流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",在指定的预热时间内,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。主要用于启动需要额外开销的场景;
排队等待:该种模式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,从而达到一种流量整形的效果。
3.2、熔断降级
一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。
例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
因此,我们需要对不稳定的依赖服务调用进行熔断降级。
熔断策略说明:
慢调用比例:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
异常比例:当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
二、Sentinel的使用
1、基础使用
1.1、引入jar包
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>
1.2、定义资源
用 Sentinel API SphU.entry("HelloWorld") 和 entry.exit()将需要进行流量控制的代码包围起来。被包围起来的代码就作为资源,用API包围起来即是埋点。
public static void fun() {
Entry entry = null;
try {
entry = SphU.entry(SOURCE_KEY);
pass.incrementAndGet();
// todo 业务逻辑
int temp = 10 / 0;
} catch (BlockException e1) {
block.incrementAndGet();
// todo 流控处理
} catch (Throwable e) {
Tracer.traceEntry(e, entry);
// todo 业务异常处理
} finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
}
1.3、定义规则
private static void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(SOURCE_KEY);
// 采用qps策略,每秒允许通过1个请求
rule1.setCount(1);
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
1.4、完整代码
/**
* 流量控制
*/
public class FlowQpsDemo { private static final String SOURCE_KEY = "CESHI_KEY"; private static AtomicInteger pass = new AtomicInteger();
private static AtomicInteger block = new AtomicInteger();
private static AtomicInteger total = new AtomicInteger(); public static void main(String[] args) throws InterruptedException {
initFlowQpsRule();
for (int i = 0;i < 10;i++) {
fun();
}
System.out.println("total=" + total.get() + " pass=" + pass.get() + " block=" + block.get());
} public static void fun() {
Entry entry = null;
try {
entry = SphU.entry(SOURCE_KEY);
// todo 业务逻辑
pass.incrementAndGet();
} catch (BlockException e1) {
// todo 流控处理
block.incrementAndGet();
} finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
} private static void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(SOURCE_KEY);
// 采用qps策略,每秒允许通过1个请求
rule1.setCount(1);
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
} private static void sleep(int sleep) {
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
}
}
}
2、生产使用
上面的示例代码演示了基础的使用方式,但是实际生产环境不会如此使用。一是直接将Sentinel Api代码写入到业务代码中,对业务代码造成了侵入;二是将规则写死在代码里面,不能动态调整。所以针对实际生产使用,我们使用Sentinel注解埋点,且集成Nacos,将配置数据同步到Nacos配置中心进行存储;
2.1、Sentinel推模式
在介绍生产环境使用Sentinel的时候,首先介绍一下Sentinel控制台、Sentinel客户端、配置中心的关系:
首先在Sentinel Dashboard中维护各种限流、降级规则,Sentinel Dashboard将规则推送到Nacos进行持久化保存;同时Sentinel客户端注册Nacos数据源,订阅Nacos数据中心的流控规则,当发生变化时更新客户端本地内存规则。
2.2、引入jar包
<!-- spring cloud 与 sentinel 集成包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- SpringCloud ailibaba sentinel-datasource-nacos 配置nacos动态配置中心 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.0</version>
</dependency>
2.3、配置Sentinel
2.3、注册动态配置中心
import com.alibaba.cloud.sentinel.SentinelProperties;
import com.alibaba.cloud.sentinel.datasource.config.NacosDataSourceProperties;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct;
import java.util.List; @Configuration
public class SentinelConfig { @Autowired
private SentinelProperties sentinelProperties; @PostConstruct
public void run() throws Exception {
// sentinel客户端注册流控nacos动态数据源
NacosDataSourceProperties flowConfiguration = sentinelProperties.getDatasource().get("flow").getNacos();
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(
flowConfiguration.getServerAddr(), flowConfiguration.getGroupId(), flowConfiguration.getDataId(),
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); // sentinel客户端注册降级nacos动态数据源
NacosDataSourceProperties degradeConfiguration = sentinelProperties.getDatasource().get("degrade").getNacos();
ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new NacosDataSource<>(
degradeConfiguration.getServerAddr(), degradeConfiguration.getGroupId(),
degradeConfiguration.getDataId(),
source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
}));
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
}
}
2.4、使用注解埋点:@SentinelResource
@GetMapping("/ceshi/flow")
@SentinelResource(value = "CESHI_FLOW", blockHandler = "ceshiFlow")
public String ceshiFlow(@RequestParam String message) {
return "hello " + message;
} public String ceshiFlow(String message, BlockException exception) {
return "当前服务被限流,暂不可用! message=" + message;
} @GetMapping("/ceshi/degrade")
@SentinelResource(value = "CESHI_DEGRADE", blockHandler = "ceshiDegradeBlock", fallback = "ceshiDegradeFallback")
public String ceshiDegrade(@RequestParam int age) throws InterruptedException {
if (age < 0) {
throw new ApplicationBaseException("年龄非法");
} else if (age == 0) {
Thread.sleep(500);
return "***刚出生***";
}
return "您的年龄为:" + age;
} public String ceshiDegradeBlock(int age, BlockException e) {
return "当前请求被阻塞,age=" + age + " exMessage=" + e.getMessage();
} public String ceshiDegradeFallback(int age, Throwable t) {
return "当前请求执行失败,age=" + age + " exMessage=" + t.getMessage();
}
2.6、在Sentinel控制台维护各项流控规则
3、注解埋点详解-@SentinelResource
官方文档地址:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
摘抄几个属性如下:
1、value:资源名称,必需项(不能为空)。
2、entryType:entry 类型,可选项(默认为 EntryType.OUT)。简单来说从外面发起的请求进到系统,这类流量就属于EntryType.IN,如果从内部发起的内部资源调用或者内部发起的对外部资源调用,这类流量就为EntryType.OUT。其中系统保护规则只针对EntryType.IN生效。
3、blockHandler / blockHandlerClass、fallback / fallbackClass:blockHandler是处理Sentinel本身限流降级的异常;而fallback处理的则是业务异常;如果两者都没有配置,那么直接抛出异常,若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException;如果只有blockHandler,那么只会处理sentinel的阻塞异常,不会处理业务异常;若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。