sentinel 简介
概述
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
支持功能:
- 流量控制
- 熔断降级
- 系统自适应保护
- 集群流量控制
- 网关流量控制
- 热点参数限流
- 来源访问控制
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于RXJava) |
规则配置 | 支持多数据源 | 支持多数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于QPS、支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC等 | Servlet、Spring Cloud Netflix |
Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | RocketMQ Version | Dubbo Version | Seata Version |
---|---|---|---|---|---|
2021.0.1.0 | 1.8.3 | 1.4.2 | 4.9.2 | ~ | 1.4.2 |
基于sentinel实现的功能
- 流量控制和限流 基于可视化控制台
- 实现熔断降级
1.实现流量控制
基于 QPS/并发数的流量控制
基于调用关系的流量控制
1.1 启动控制台服务及主要概述说明
1.1.1 配置
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
1.1.2 启动控制台
首先下载所需版本的sentinel可视化服务jar包,下载地址https://github.com/alibaba/Sentinel/releases
默认参数:
端口:8080
用户名:sentinel
密码:sentinel
1.1.3 基于可视化控制台设置QPS或并发线程数控制访问
资源名:流控针对的方法名;
针对来源:需要在代码中指定 在方法中添加ContextUtil.enter(resourceName,"填写来源名称")
下面代码中有示例;
阈值类型:QPS、并发线程数;
1). QPS:每秒请求查询数量
2). 并发线程数:瞬时的请求数量
单机阈值:限制的访问次数
支持集群,可设置集群单机访问也可设置集群总访问量
流控模式:直接、关联、链路;
1). 关联资源:如果流控模式为关联时,才有该参数;
2). 入口资源:如果流控模式为链路时,才有该参数;
流控效果:快速失败、Warm Up、排队等待;
1). 预热时长:如果流控效果为Warm Up才有该参数
流控模式:
直接:代表请求请求到该接口如果达到设置到阈值将直接进行流控,直接抛出异常
关联:代表请求到该接口,关联的资源达到设置的阈值,然后才触发流控,比如在同一个服务中会有两个接口,一个读资源操作的接口,一个是写资源操作的接口,如果读资源操作的接口QPS太大时,会影响写操作业务完整性,即可以在设定资源名为写操作的流控规则时流控模式设定为关联模式,关联的资源为读操作的接口,就是当读操作的QPS达到阈值时,将写操作进行流控。(当关联当资源达到阈值时,就限流自己)
链路:比如一个微服务中的两个接口都调用了该微服务中的同一个service方法,并且该方法用SentinelResource注解给标注了,然后对该注解标注的资源进行规则配置,则可以选择链路模式,规则中资源名,就是注解标注的名称,入口资源就是指定,对那个接口调用该service的方法进行流控。
流控效果:
快速失败:顾名思义,就是直接抛出异常
Warm Up :可以设置预热的时长,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
排队等待:顾名思义,就是请求一个一个排队等待,设定一个请求等待的超时时间,等待时间超过设定的值,然后抛出异常。
官网地址:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
1.1.4 可以通过注解形式控制方法的访问
通过在方法上添加@SentinelResource
注解对该方法进行流控,如果超过流控规则限制,则返回下面方法的错误提示
注:两个方法的返回值要一致;
value值要和方法名一致,blockHandler值要和下面方法名一致;
两个方法的参数一定要一致(除BlockException外);
下面的方法一定要有BlockException异常的参数
@GetMapping(value = "/get")
@SentinelResource(value = "getMethod",blockHandler = "fail")
public String getMethod(){
String resourceName = "get"; //一般即为请求路径
ContextUtil.enter(resourceName,"填写来源名称")
return "SUCCESS";
}
public String fail(BlockException e){
return "已被限制访问";
}
如果不使用可视化控制台添加流控规则,而是在系统中自定义开发流控规则配置功能,则需要通过spring AOP功能来完成sentinel源码改造工作相关重要属性如下表
具体改造工作可以参考sentinel原理https://blog.csdn.net/wanger5354/article/details/122493842
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 或线程数模式 | QPS 模式 |
limitApp | 流控针对的调用来源 | default,代表不区分调用来源 |
strategy | 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 | 根据资源本身 |
controlBehavior | 流控效果(直接拒绝 / 排队等待 / 慢启动模式) | 直接拒绝 |
2.实现熔断降级
2.1 降级策略
2.1.1 降级策略(sentinel 1.8.0及以上版本):
慢调用比例:需要设置慢调用的RT(即最大相应时间,单位毫秒),请求的相应时间大于该值,则统计为慢调用。
当单位统计时长内,请求数目大于设置的最小请求数目并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用RT,则结束熔断,若大于设置的慢调用RT,则会再次被熔断。
异常比例:当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
参数说明:
资源名:需要做熔断处理的接口名
熔断策略:慢比例调用、异常比例、异常数
最大RT(毫秒):设置接口响应时间比较标准值,如果接口响应时间超过最大RT,则判定该次接口调用为慢调用
比例阈值(取值[0.0~1.0]):设置 慢调用的次数占有的比例或异常次数占有的比例,如果超过设置的比例阈值,则满足熔断条件的50%
熔断时长:如果满足熔断条件,则熔断的时长
最小请求数:设置单位时间内接口请求次数标准值,如果超过设置的最小请求数,则满足熔断条件的50%
统计时长(毫秒):单位统计时长,设置多长时间作为一个单位统计时长,默认1000ms
异常数:设置单位时间内,调用接口异常次数比较标准值
注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。 示例:
Entry entry = null;
try {
entry = SphU.entry(key, EntryType.IN, key);
// Write your biz code here.
// <<BIZ CODE>>
} catch (Throwable t) {
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}
2.1.2 降级策略(sentinel 1.8.0以下版本):
RT(平均响应时间,秒级):平均响应时间超出阈值且在时间窗口内通过的请求>=5,两个条件同时满足后出发降级,窗口期过后关闭断路器;RT最大4900 (更大需要通过-Dcsp.sentinel.statistic.max.rt=XXX才能生效)
异常比例(秒级):QPS>=5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级):异常数超过阈值时,触发降级;时间窗口结束后,关闭降级
对比Hystrix熔断降级比较,Hystrix是有半开状态的,服务自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可使用
2.2 重要参数
如果通过apollo等配置中心进行配置时,重要参数非常重要;
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
2.3 熔断器事件监听
Sentinel 支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例:
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == State.OPEN) {
// 变换至 OPEN state 时会携带触发时的值
System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
TimeUtil.currentTimeMillis(), snapshotValue));
} else {
System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
TimeUtil.currentTimeMillis()));
}
});
监听demo类,官网都提供demo,也可以直接去官网找,这里我只是搬运工,嘻嘻!!!
慢调用比例 监听示例SlowRatioDemo.java
类
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker.State;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
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.degrade.circuitbreaker.EventObserverRegistry;
import com.alibaba.csp.sentinel.util.TimeUtil;
/**
* 慢调用比例 监听示例
*/
public class SlowRatioDemo {
private static final String KEY = "some_method";
private static volatile boolean stop = false;
private static int seconds = 120;
private static AtomicInteger total = new AtomicInteger();
private static AtomicInteger pass = new AtomicInteger();
private static AtomicInteger block = new AtomicInteger();
public static void main(String[] args) throws Exception {
// 初始化加载熔断规则
initDegradeRule();
// 熔断器事件监听
registerStateChangeObserver();
startTick();
int concurrency = 8;
for (int i = 0; i < concurrency; i++) {
Thread entryThread = new Thread(() -> {
while (true) {
Entry entry = null;
try {
entry = SphU.entry(KEY);
// get();
pass.incrementAndGet();
// RT: [40ms, 60ms) su
sleep(ThreadLocalRandom.current().nextInt(40, 60));
} catch (BlockException e) {
block.incrementAndGet();
sleep(ThreadLocalRandom.current().nextInt(5, 10));
} finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
}
});
entryThread.setName("sentinel-simulate-traffic-task-" + i);
entryThread.start();
}
}
// 熔断器事件监听
private static void registerStateChangeObserver() {
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == State.OPEN) {
System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
TimeUtil.currentTimeMillis(), snapshotValue));
} else {
System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
TimeUtil.currentTimeMillis()));
}
});
}
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule(KEY)
// 熔断策略
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
// Max allowed response time RT 慢调用标准值 接口相应时长超过RT则被判定为慢调用
.setCount(50)
// Retry timeout (in second) 窗口期 熔断时长
.setTimeWindow(10)
// Circuit breaker opens when slow request ratio > 60% 慢调用比例 判定是否熔断的条件之一
.setSlowRatioThreshold(0.3)
// 单位时长最小请求数 判定是否熔断的条件之一
.setMinRequestAmount(10)
// 统计时长也叫单位时长
.setStatIntervalMs(20000);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
System.out.println("Degrade rule loaded: " + rules);
}
private static void sleep(int timeMs) {
try {
TimeUnit.MILLISECONDS.sleep(timeMs);
} catch (InterruptedException e) {
// ignore
}
}
private static void startTick() {
Thread timer = new Thread(new TimerTask());
timer.setName("sentinel-timer-tick-task");
timer.start();
}
static class TimerTask implements Runnable {
@Override
public void run() {
long start = System.currentTimeMillis();
System.out.println("Begin to run! Go go go!");
System.out.println("See corresponding metrics.log for accurate statistic data");
long oldTotal = 0;
long oldPass = 0;
long oldBlock = 0;
while (!stop) {
sleep(1000);
long globalTotal = total.get();
long oneSecondTotal = globalTotal - oldTotal;
oldTotal = globalTotal;
long globalPass = pass.get();
long oneSecondPass = globalPass - oldPass;
oldPass = globalPass;
long globalBlock = block.get();
long oneSecondBlock = globalBlock - oldBlock;
oldBlock = globalBlock;
System.out.println(Thread.currentThread().getName()+"\t\t"+TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal
+ ", pass:" + oneSecondPass + ", block:" + oneSecondBlock);
if (seconds-- <= 0) {
stop = true;
}
}
long cost = System.currentTimeMillis() - start;
System.out.println("time cost: " + cost + " ms");
System.out.println("total: " + total.get() + ", pass:" + pass.get()
+ ", block:" + block.get());
System.exit(0);
}
}
public static String get() {
System.out.println("===========SUCCESS==============");
// RT: [40ms, 60ms)
sleep(ThreadLocalRandom.current().nextInt(40, 60));
return "Success";
}
}
系统自适应保护 demoSystemGuardDemo.java
类
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.alibaba.csp.sentinel.util.TimeUtil;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
/**
* 系统自适应保护 demo
*
* 系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
*
* 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
*
* 系统规则支持以下的阈值类型:
*
* Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
* CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
* RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
* 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
* 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
*
*/
public class SystemGuardDemo {
private static AtomicInteger pass = new AtomicInteger();
private static AtomicInteger block = new AtomicInteger();
private static AtomicInteger total = new AtomicInteger();
private static volatile boolean stop = false;
private static final int threadCount = 100;
private static int seconds = 60 + 40;
public static void main(String[] args) throws Exception {
tick();
initSystemRule();
for (int i = 0; i < threadCount; i++) {
Thread entryThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Entry entry = null;
try {
entry = SphU.entry("methodA", EntryType.IN);
pass.incrementAndGet();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
// ignore
}
} catch (BlockException e1) {
block.incrementAndGet();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
// ignore
}
} catch (Exception e2) {
// biz exception
} finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
}
}
});
entryThread.setName("working-thread");
entryThread.start();
}
}
private static void initSystemRule() {
List<SystemRule> rules = new ArrayList<SystemRule>();
SystemRule rule = new SystemRule();
// max load is 3
rule.setHighestSystemLoad(3.0);
// max cpu usage is 60%
rule.setHighestCpuUsage(0.6);
// max avg rt of all request is 10 ms
rule.setAvgRt(10);
// max total qps is 20
rule.setQps(20);
// max parallel working thread is 10
rule.setMaxThread(10);
rules.add(rule);
SystemRuleManager.loadRules(Collections.singletonList(rule));
}
private static void tick() {
Thread timer = new Thread(new TimerTask());
timer.setName("sentinel-timer-task");
timer.start();
}
static class TimerTask implements Runnable {
@Override
public void run() {
System.out.println("begin to statistic!!!");
long oldTotal = 0;
long oldPass = 0;
long oldBlock = 0;
while (!stop) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
long globalTotal = total.get();
long oneSecondTotal = globalTotal - oldTotal;
oldTotal = globalTotal;
long globalPass = pass.get();
long oneSecondPass = globalPass - oldPass;
oldPass = globalPass;
long globalBlock = block.get();
long oneSecondBlock = globalBlock - oldBlock;
oldBlock = globalBlock;
System.out.println(seconds + ", " + TimeUtil.currentTimeMillis() + ", total:"
+ oneSecondTotal + ", pass:"
+ oneSecondPass + ", block:" + oneSecondBlock);
if (seconds-- <= 0) {
stop = true;
}
}
System.exit(0);
}
}
}