本文属于sentinel学习笔记系列。网上看到吴就业老师的专栏,原文地址如下:
https://blog.youkuaiyun.com/baidu_28523317/category_10400605.html
基本概念
basic-api-resource-rule | Sentinel 官网的基本概念介绍
资源 是 Sentinel 中的核心概念之一。最常用的资源是我们代码中的 Java 方法,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
先把可能需要保护的资源定义好,之后再配置规则。sentinel把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了。官网上给出了5种不同的方式。
规则的种类
Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。
Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则 和 热点参数规则。
资源指标数据统计相关的类
首推官网:Sentinel 核心类解析 · alibaba/Sentinel Wiki · GitHub
Sentinel 中指标数据统计以资源为维度。资源使用 ResourceWrapper 对象表示,我们把 ResourceWrapper 对象称为资源 ID。接口通常是接口对应的URL。
public abstract class ResourceWrapper {
//资源名称
protected final String name;
//流量类型:枚举(流入IN还是流出OUT)
protected final EntryType entryType;
//资源类型:rpc\mvc\api
protected final int resourceType;
可以把流量类型 IN 和 OUT 简单理解为接收处理请求与发送请求。
Sentinel 目前支持的资源类型有以下几种:
public final class ResourceTypeConstants {
//默认
public static final int COMMON = 0;
//web
public static final int COMMON_WEB = 1;
//dubbo rpc
public static final int COMMON_RPC = 2;
//api gateway
public static final int COMMON_API_GATEWAY = 3;
//数据库 sql
public static final int COMMON_DB_SQL = 4;
private ResourceTypeConstants() {}
}
ResourceWrapper 有两个实现类,分别是:StringResourceWrapper(字符串进行包装) 和 MethodResourceWrapper(对方法调用的包装)。
public interface Node extends OccupySupport, DebugSupport {
/**
* Get incoming request per minute ({@code pass + block}).
* 获取总的请求数
* @return total request count per minute
*/
long totalRequest();
/**
* Get pass count per minute.
* 获取通过的请求总数
* @return total passed request count per minute
* @since 1.5.0
*/
long totalPass();
/**
* Get {@link Entry#exit()} count per minute.
* 获取成功的请求总数
* @return total completed request count per minute
*/
long totalSuccess();
/**
* Get blocked request count per minute (totalBlockRequest).
* 被拒绝的请求总数
* @return total blocked request count per minute
*/
long blockRequest();
/**
* Get exception count per minute.
* 异常总数
* @return total business exception count per minute
*/
long totalException();
/**
* Get pass request per second.
* 通过 QPS
* @return QPS of passed requests
*/
double passQps();
/**
* Get block request per second.
* 拒绝 QPS
* @return QPS of blocked requests
*/
double blockQps();
/**
* Get {@link #passQps()} + {@link #blockQps()} request per second.
* 总 qps
* @return QPS of passed and blocked requests
*/
double totalQps();
/**
* Get {@link Entry#exit()} request per second.
* 成功 qps
* @return QPS of completed requests
*/
double successQps();
/**
* Get estimated max success QPS till now.
* 最大成功总数 QPS
* @return max completed QPS
*/
double maxSuccessQps();
/**
* Get exception count per second.
* 异常 QPS
* @return QPS of exception occurs
*/
double exceptionQps();
/**
* Get average rt per second.
* 平均耗时
* @return average response time per second
*/
double avgRt();
/**
* Get minimal response time.
* 最小耗时
* @return recorded minimal response time
*/
double minRt();
/**
* Get current active thread count.
* 当前并发占用的线程数
* @return current active thread count
*/
int curThreadNum();
/**
* Get last second block QPS.
* 前一个时间窗口的被拒绝 qps
*/
double previousBlockQps();
/**
* Last window QPS.
* 前一个时间窗口的通过 qps
*/
double previousPassQps();
/**
* Fetch all valid metric nodes of resources.
*
* @return valid metric nodes of resources
*/
Map<Long, MetricNode> metrics();
/**
* Fetch all raw metric items that satisfies the time predicate.
*
* @param timePredicate time predicate
* @return raw metric items that satisfies the time predicate
* @since 1.7.0
*/
List<MetricNode> rawMetricsInMin(Predicate<Long> timePredicate);
/**
* Add pass count.
* 添加通过请求数
* @param count count to add pass
*/
void addPassRequest(int count);
/**
* Add rt and success count.
* 添加成功请求数
* @param rt response time
* @param success success count to add
*/
void addRtAndSuccess(long rt, int success);
/**
* Increase the block count.
* 添加被拒绝的请求数
* @param count count to add
*/
void increaseBlockQps(int count);
/**
* Add the biz exception count.
* 添加异常请求数
* @param count count to add
*/
void increaseExceptionQps(int count);
/**
* Increase current thread count.
* 自增占用线程
*/
void increaseThreadNum();
/**
* Decrease current thread count.
* 自减占用线程
*/
void decreaseThreadNum();
/**
* Reset the internal counter. Reset is needed when {@link IntervalProperty#INTERVAL} or
* {@link SampleCountProperty#SAMPLE_COUNT} is changed.
* 重置滑动窗口
*/
void reset();
}
它的几个实现类:DefaultNode、ClusterNode、EntranceNode、StatisticNode 的关系如下图所示
分成了两类node:
DefaultNode:代表链路树中的每一个资源,一个资源出现在不同链路中时,会创建不同的DefaultNode节点。而树的入口节点叫EntranceNode,用来实现基于链路模式的限流规则。
ClusterNode:代表资源,一个资源不管出现在多少链路中,只会有一个ClusterNode。记录的是当前资源被访问的所有统计数据之和,实现默认模式、关联模式的限流规则。
StatisticNode
StatisticNode实现了Node接口,其他的Node都继承了该接口。StatisticNode中保存了资源的实时统计数据(基于滑动时间窗口机制),通过这些统计数据,sentinel才能进行限流、降级等一系列操作。
public class StatisticNode implements Node {
/**
* Holds statistics of the recent {@code INTERVAL} milliseconds. The {@code INTERVAL} is divided into time spans
* by given {@code sampleCount}. 级的滑动时间窗口(时间窗口单位500ms)
*/
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
/**
* Holds statistics of the recent 60 seconds. The windowLengthInMs is deliberately set to 1000 milliseconds,
* meaning each bucket per second, in this way we can get accurate statistics of each second.
* 分钟级的滑动时间窗口(时间窗口单位1s)
*/
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
/**
* The counter for thread count.
* 统计并发使用的线程数
*/
private LongAdder curThreadNum = new LongAdder();
/**
* The last timestamp when metrics were fetched.
*/
private long lastFetchTime = -1;
可以看到StatisticNode 包含一个秒级和一个分钟级的滑动窗口,以及并行线程数计数器。秒级滑动窗口用于统计实时的 QPS,分钟级的滑动窗口用于保存最近一分钟内的历史指标数据,并行线程计数器用于统计当前并行占用的线程数。例如,记录请求成功和请求执行耗时的方法中调用了两个滑动窗口的对应指标项的记录方法.
public void addRtAndSuccess(long rt, int successCount) {
rollingCounterInSecond.addSuccess(successCount);//秒级
rollingCounterInSecond.addRT(rt);
rollingCounterInMinute.addSuccess(successCount);//分钟级
rollingCounterInMinute.addRT(rt);
}
StatisticNode 还负责统计并行占用的线程数,用于实现信号量隔离,按资源所能并发占用的最大线程数实现限流。当接收到一个请求就将 curThreadNum 自增 1,当处理完请求时就将 curThreadNum 自减一,如果同时处理 10 个请求,那么 curThreadNum 的值就为 10。通过控制并发线程数实现信号量隔离的好处就是不让一个接口同时使用完tomcat 处理请求的线程池大小,避免因为一个接口响应慢将所有线程都阻塞导致应用无法处理其他请求的问题,这也是实现信号量隔离的目的。
DefaultNode
DefaultNode 是实现以资源为维度的指标数据统计的 Node,是将资源 ID 和 StatisticNode 映射到一起的 Node。
public class DefaultNode extends StatisticNode {
/**
* The resource associated with the node. 资源 ID
*/
private ResourceWrapper id;
/**
* The list of all child nodes.子节点list
*/
private volatile Set<Node> childList = new HashSet<>();
/**
* Associated cluster node.
*/
private ClusterNode clusterNode;
public DefaultNode(ResourceWrapper id, ClusterNode clusterNode) {
this.id = id;
this.clusterNode = clusterNode;
}
DefaultNode 是 StatisticNode 的子类,构造方法要求传入资源 ID,表示该 Node 用于统计哪个资源的实时指标数据,指标数据统计则由父类 StatisticNode 完成。
DefaultNode 创建是在NodeSelectorSlot.entry完成的,在NodeSelectorSlot中是一个context有且仅有一个DefaultNode。代码com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot#entry
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = new DefaultNode(resourceWrapper, null);
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
// Build invocation tree
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
EntranceNode
EntranceNode 是一个特殊的 Node,它继承 DefaultNode,用于维护一颗树,从根节点到每个叶子节点都是不同请求的调用链路,所经过的每个节点都对应着调用链路上被 Sentinel 保护的资源,一个请求调用链路上的节点顺序正是资源被访问的顺序。EntranceNode代表调用链的入口节点,持有某个Context中调用的信息,同一个Context共享一个EntranceNode。
public class EntranceNode extends DefaultNode {
public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
super(id, clusterNode);
}
如果想统计一个应用的所有接口总的 QPS,只需要调用 EntranceNode 的 totalQps 就能获取到
public double totalQps() {
double r = 0;
for (Node node : getChildList()) {
r += node.totalQps();
}
return r;
}
ClusterNode
Sentinel 使用 ClusterNode 统计每个资源全局的指标数据,以及统计该资源按调用来源区分的指标数据。全局数据指的是不区分调用链路,一个资源 ID 只对应一个 ClusterNode。
public class ClusterNode extends StatisticNode {
// 资源名称
private final String name;
// 资源类型
private final int resourceType;
// 资源类型
public ClusterNode(String name) {
this(name, ResourceTypeConstants.COMMON);
}
public ClusterNode(String name, int resourceType) {
AssertUtil.notEmpty(name, "name cannot be empty");
this.name = name;
this.resourceType = resourceType;
}
/**
* <p>The origin map holds the pair: (origin, originNode) for one specific resource.</p>
* <p>
* The longer the application runs, the more stable this mapping will become.
* So we didn't use concurrent map here, but a lock, as this lock only happens
* at the very beginning while concurrent map will hold the lock all the time.
* </p>
* 来源指标数据统计
*/
private Map<String, StatisticNode> originCountMap = new HashMap<>();
//控制并发修改 originCountMap 用的锁
private final ReentrantLock lock = new ReentrantLock();
网上找了图来加深理解:Sentinel 实现原理——概述-阿里云开发者社区
Entry
每一次资源调用都会创建一个
Entry
。Entry
包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
CtEntry
为普通的Entry
,在调用SphU.entry(xxx)
的时候创建。特性:Linked entry within current context(内部维护着parent
和child
)需要注意的一点:CtEntry 构造函数中会做调用链的变换,即将当前 Entry 接到传入 Context 的调用链路上(
setUpEntryFor
)。资源调用结束时需要
entry.exit()
。exit 操作会过一遍 slot chain exit,恢复调用栈,exit context 然后清空 entry 中的 context 防止重复调用。entry类CtEntry结构如下:
public abstract class Entry implements AutoCloseable {
private static final Object[] OBJECTS0 = new Object[0];
private final long createTimestamp;
private long completeTimestamp;
//当前节点
private Node curNode;
/**
* {@link Node} of the specific origin, Usually the origin is the Service Consumer.
* 来源节点
*/
private Node originNode;
//是否出现异常
private Throwable error;
private BlockException blockError;
// 资源
protected final ResourceWrapper resourceWrapper;
...
}
class CtEntry extends Entry {
protected Entry parent = null;
protected Entry child = null;
protected ProcessorSlot<Object> chain;
protected Context context;
protected LinkedList<BiConsumer<Context, Entry>> exitHandlers;
Context
Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。Context 是保存调用链路元数据的容器,这里所说的元数据主要包括:
- 当前调用树的根节点,通过这个根节点,我们可以用来区分不同的调用链路,
ContextUtil.enter
接口的第一个参数描述了该调用链路的根节点的名称,如果没有显式地调用ContextUtil.enter
接口,那么 Sentinel 会以sentinel_default_context
作为默认根节点,Sentinel 会保证每个名称的根节点实例是唯一的. - 调用源 origin,通过
ContextUtil.enter
接口的第二个参数指定,一般会将 Consumer name 或者 consumer IP 地址设定为调用源 - 当前的执行点 Entry,因为 Context 是保存在 ThreadLocal 中的,所以当前线程每到一个执行点,就会将其记录在 Context 中
小结:
StatisticNode
:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构。DefaultNode
:链路节点,用于统计调用链路上某个资源的数据,维持树状结构。ClusterNode
:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为StatisticNode
)。特别地,Constants.ENTRY_NODE
节点用于统计全局的入口资源数据。EntranceNode
:入口节点,特殊的链路节点,对应某个 Context 入口的所有调用数据。Constants.ROOT
节点也是入口节点。