1. SPI
SPI 全称为 Service Provider Interface,是一种服务发现机制。它通过在 classPath 路径下的 META-INF/services
文件夹查找文件,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。我们先通过一个很简单的例子来看下它是怎么用的。
1.1 demo
a. 测试代码
首先,我们需要定义一个接口:SPIService
package com.viewscenes.netsupervisor.spi;
public interface SPIService {
void execute();
}
然后,定义两个实现类,没别的意思,只输入一句话。
package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
public void execute() {
System.out.println("SpiImpl1.execute()");
}
}
---------------------- 分割线 ----------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
public void execute() {
System.out.println("SpiImpl2.execute()");
}
}
最后呢,要在 classPath 路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
文件路径如下:
内容就是实现类的全限定类名:
com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
然后我们就可以通过 ServiceLoader.load
或者 Service.providers
方法拿到实现类的实例。其中,Service.providers
包位于 sun.misc.Service
,而 ServiceLoader.load
包位于 java.util.ServiceLoader
。
public class SPITest {
public static void main(String[] args) {
Iterator<SPIService> providers = Service.providers(SPIService.class);
ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
while(providers.hasNext()) {
SPIService ser = providers.next();
ser.execute();
}
System.out.println("--------------------------------");
Iterator<SPIService> iterator = load.iterator();
while(iterator.hasNext()) {
SPIService ser = iterator.next();
ser.execute();
}
}
}
两种方式的输出结果是一致的:
SpiImpl1.execute()
SpiImpl2.execute()
--------------------------------
SpiImpl1.execute()
SpiImpl2.execute()
b. 源码分析
我们看到一个位于 sun.misc
包,一个位于 java.util
包,sun 包下的源码看不到。我们就以 ServiceLoader.load
为例,通过源码看看它里面到底怎么做的。
(1)ServiceLoader 类结构
public final class ServiceLoader<S> implements Iterable<S>
// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务类或接口
private final Class<S> service;
// 已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 类加载器
private final ClassLoader loader;
// 内部类,真正加载服务类
private LazyIterator lookupIterator;
}
(2)load 方法
public final class ServiceLoader<S> implements Iterable<S>
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 要加载的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 先清空
providers.clear();
// 实例化内部类
LazyIterator lookupIterator = new LazyIterator(service, loader);
}
}
(3)查找实现类
查找实现类和创建实现类的过程,都在 LazyIterator 完成。当我们调用 iterator.hasNext
和 iterator.next
方法的时候,实际上调用的都是 LazyIterator 的相应方法。
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
.......
};
}
所以,我们重点关注 lookupIterator.hasNext()
方法,它最终会调用到 hasNextService
。
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
// 第 2 次调用的时候,已经解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
// META-INF/services/<接口的全限定类名>
// META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
// 将文件路径转成 URL 对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
// 解析 URL 文件对象、读取内容、最后返回
pending = parse(service, configs.nextElement());
}
// 拿到第一个实现类的类名
nextName = pending.next();
return true;
}
}
(4)创建实例
当然,调用 next 方法的时候,实际调用到的是 lookupIterator.nextService
。它通过反射的方式,创建实现类的实例并返回。
private class LazyIterator implements Iterator<S>{
private S nextService() {
// 全限定类名
String cn = nextName;
nextName = null;
// 创建类的 Class 对象
Class<?> c = Class.forName(cn, false, loader);
// 通过 newInstance 实例化
S p = service.cast(c.newInstance());
// 放入集合,返回实例
providers.put(cn, p);
return p;
}
}
1.2 JDBC
我们开头说,SPI 机制为很多框架的扩展提供了可能,其实 JDBC 就应用到了这一机制。回忆一下 JDBC 获取数据库连接的过程。在早期版本中,需要先设置数据库驱动的连接,再通过 DriverManager.getConnection
获取一个 Connection。
String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
它是怎么分辨是哪种数据库的呢?答案就在 SPI。
a. 加载
我们把目光回到 DriverManager 类,它在静态代码块里面做了一件比较重要的事。很明显,它已经通过 SPI 机制, 把数据库驱动连接初始化了。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
具体过程还得看 loadInitialDrivers
,它在里面查找的是 Driver 接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver
。
public class DriverManager {
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 很明显,它要加载 Driver 接口的服务类,Driver接口的包为 java.sql.Driver
// 所以它要找的就是 META-INF/services/java.sql.Driver 文件
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 查到之后创建对象
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// do nothing
}
return null;
}
});
}
}
那么,这个文件哪里有呢?我们来看 MySQL 的 jar 包,就是如下这个文件,文件内容为:com.mysql.cj.jdbc.Driver
。
b. 创建实例
上一步已经找到了 MySQL 中的 com.mysql.cj.jdbc.Driver
全限定类名,当调用 next 方法时,就会创建这个类的实例。它就完成了一件事,向 DriverManager 注册自身的实例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
// 调用 DriverManager 类的注册方法,向 registeredDrivers 集合中加入实例
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
c. 创建 Connection
在 DriverManager.getConnection()
方法就是创建连接的地方,它通过循环已注册的数据库驱动程序,调用其 connect 方法,获取连接并返回。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
// registeredDrivers 中就包含 com.mysql.cj.jdbc.Driver实例
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
// 调用 connect 方法创建连接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}
d. 再扩展
既然我们知道 JDBC 是这样创建数据库连接的,我们能不能再扩展一下呢?如果我们自己也创建一个 java.sql.Driver
文件,自定义实现类 MyDriver,那么,在获取连接的前后就可以动态修改一些信息。
还是先在项目 classPath 下创建文件,文件内容为自定义驱动类 com.viewscenes.netsupervisor.spi.MyDriver
。
我们的 MyDriver 实现类,继承自 MySQL 中的 NonRegisteringDriver,还要实现 java.sql.Driver
接口。这样,在调用 connect 方法的时候,就会调用到此类,但实际创建的过程还靠 MySQL 完成。
package com.viewscenes.netsupervisor.spi
public class MyDriver extends NonRegisteringDriver implements Driver {
static {
try {
java.sql.DriverManager.registerDriver(new MyDriver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public MyDriver() throws SQLException {}
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("准备创建数据库连接#url:" + url);
System.out.println("JDBC配置信息:" + info);
info.setProperty("user", "root");
Connection connection = super.connect(url, info);
System.out.println("数据库连接创建完成!" + connection.toString());
return connection;
}
}
--------------------输出结果---------------------
准备创建数据库连接#url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f
2. 基本原理
a. Slot
https://sentinelguard.io/zh-cn/docs/basic-implementation.html
在 Sentinel 里面,所有的资源都对应一个资源名称以及一个 Entry。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建;每一个 Entry 创建的时候,同时也会创建一系列功能插槽(slot chain)。这些插槽有不同的职责,例如:
NodeSelectorSlot
负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
ClusterBuilderSlot
则用于存储资源的统计信息以及调用者信息,如该资源的 RT/QPS/threadCount/exceptionCount 等,
这些信息将用作为多维度限流、降级的依据;
StatisticSlot
则用于记录、统计不同纬度的 runtime 指标监控信息;
························································································
ParamFlowSlot
用于【热点参数限流】;
FlowSlot
则用于根据预设的限流规则以及前面 slot 统计的状态,来进行【流量控制】;
AuthoritySlot
则根据配置的黑白名单和调用来源信息,来做【黑白名单控制】;
DegradeSlot
则通过统计信息以及预设的规则,来做【熔断降级】;
SystemSlot
则通过系统的状态,例如 load1 等,来控制【总的入口流量】;
Sentinel 的核心骨架是 ProcessorSlotChain。其将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能组合在一起(限流、降级、系统保护)。系统会为每个资源创建一套 SlotChain。
Sentinel 将 ProcessorSlot 作为 SPI 接口进行扩展(1.7.2 版本以前 SlotChainBuilder 作为 SPI),使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。
b. Context
Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。
Context 维持的方式:通过 ThreadLocal 传递,只有在入口 enter
的时候生效。由于 Context 是通过 ThreadLocal 传递的,因此对于异步调用链路,线程切换时会丢掉 Context,因此需要手动通过 contextUtil.runOnContext(context, f)
来变换 context。
Context 是对资源操作的上下文,每个资源操作必须属于一个 Context。如果代码中没有指定 Context,则会创建一个 namesentinel_default_context
的默认 Context。一个 Context 生命周期中可以包含多个资源操作。Context 生命周期中的最后一个资源在 exit()
时会清理该 Conetxt,这也就意味着这个 Context 生命周期结束了。
/**
* The fundamental Sentinel API for recording statistics and performing rule
* checking for resources.
*
* Conceptually, physical or logical resource that need protection should be
* surrounded by an entry. The requests to this resource will be blocked if any
* criteria is met, eg. when any {@link Rule}'s threshold is exceeded. Once blocked,
* a {@link BlockException} will be thrown.
*
* To configure the criteria, we can use `XxxRuleManager.loadRules()` to load rules.
*
* Following code is an example, {@code "abc"} represent a unique name for the
* protected resource:
*
* public void foo() {
* Entry entry = null;
* try {
* entry = SphU.entry("abc");
* // resource that need protection
* } catch (BlockException blockException) {
* // when goes there, it is blocked
* // add blocked handle logic here
* } catch (Throwable bizException) {
* // business exception
* Tracer.trace(bizException);
* } finally {
* // ensure finally be executed
* if (entry != null){
* entry.exit();
* }
* }
* }
*
* Make sure {@code SphU.entry()} and {@link Entry#exit()} be paired in the same thread,
* otherwise {@link ErrorEntryFreeException} will be thrown.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 上面这段注释来自 com.alibaba.csp.sentinel.SphU(都是基本单词能读懂)
*/
// 创建一个来自于 appA 访问的 Context, entranceOne 是 Context 的 name
ContextUtil.enter("entranceOne", "appA");
// Entry 是用来表示是资源是否可以继续执行一个凭证,可以理解为 token。
Entry resource1 = null;
Entry resource2 = null;
try {
// 获取资源 resource1 的 entry
// 使用 SphU.entry() 会返回一个 entry,如果抛出 BlockException,说明资源被保护了。
resource1 = SphU.entry("resource1");
// => 代码能走到这里,说明当前对资源 resource1 的请求通过了流控
// ===== 对资源resource1的相关业务处理 =====
// 获取资源 resource2 的 entry
// 使用 SphU.entry() 会返回一个 entry,如果抛出 BlockException,说明资源被保护了。
resource2 = SphU.entry("resource2");
// => 代码能走到这里,说明当前对资源 resource2 的请求通过了流控
// ===== 对资源 resource2 的相关业务处理 =====
} catch (BlockException e) {
// 代码能走到这里,说明请求被限流,这里执行降级处理...
} finally {
if (resource1 != null) {
resource1.exit();
}
if (resource2 != null) {
resource2.exit();
}
}
// 释放 Context
ContextUtil.exit();
// ===== 使用 SphO.entry() 时,资源被保护了会返回 false,反之 true。=====
// 创建另一个来自于 appA 访问的 Context,entranceTwo 是 Context 的 name
ContextUtil.enter("entranceTwo", "appA");
// Entry 是用来表示是资源是否可以继续执行一个凭证,可以理解为 token。
Entry resource3 = null;
try {
// 获取资源 resource2 的 entry
// 使用 SphU.entry() 会返回一个 entry,如果抛出 BlockException,说明资源被保护了。
resource2 = SphU.entry("resource2");
// => 代码能走到这里,说明当前对资源 resource2 的请求通过了流控
// ===== 对资源 resource2 的相关业务处理 =====
// 获取资源 resource3 的 entry
// 使用 SphU.entry() 会返回一个 entry,如果抛出 BlockException,说明资源被保护了。
resource3 = SphU.entry("resource3");
// => 代码能走到这里,说明当前对资源 resource3 的请求通过了流控
// ===== 对资源 resource3 的相关业务处理 =====
} catch (BlockException e) {
// 代码能走到这里,说明请求被限流,这里执行降级处理...
} finally {
if (resource2 != null) {
resource2.exit();
}
if (resource3 != null) {
resource3.exit();
}
}
// 释放Context
ContextUtil.exit();
c. Node
结合上述示例代码观察 Node 间的关系:
- Node:用于完成数据统计的接口;
- StatisticNode:统计节点,是 Node 接口的实现类,用于完成数据统计;
- EntranceNode:入口节点,一个 Context 会有一个入口节点,用于统计当前 Context 的总体流量数据;
- DefaultNode:默认节点,用于统计一个资源在当前 Context 中的流量数据;
- ClusterNode:集群节点,用于统计一个资源在所有 Context 中的总体流量数据。
类的结构图:
3. 源码解析 basic
SentinelAutoConfiguration
@Bean
@ConditionalOnMissingBean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
SentinelResourceAspect
/**
* Aspect for methods with {@link SentinelResource} annotation.
*/
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
/**
* 指定切入点为 @SentinelResource 注解
*/
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {}
/**
* 指定此为环绕通知 around advice
* @param pjp
* @return
* @throws Throwable
*/
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = resolveMethod(pjp);
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
// Should not go through here.
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
// =====> [1] 要织入的、增强的功能
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
// 调用目标方法
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
// The ignore list will be checked first.
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
throw ex;
}
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
traceException(ex);
return handleFallback(pjp, annotation, ex);
}
// No fallback function can handle the exception, so throw it out.
throw ex;
} finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
3.1 SphU
/**
* Record statistics and perform rule checking for the given resource.
*
* @param name the unique name for the protected resource
* @param trafficType the traffic type (inbound, outbound or internal). This is used
* to mark whether it can be blocked when the system is unstable,
* only inbound traffic could be blocked by {@link SystemRule}
* @param resourceType classification of the resource (e.g. Web or RPC)
* @param args args for parameter flow control or customized slots
* @return the {@link Entry} of this invocation (used for mark the invocation complete
* and get context data)
* @throws BlockException if the block criteria is met
* (e.g. metric exceeded the threshold of any rules)
* @since 1.7.0
*/
public static Entry entry(String name, int resourceType, EntryType trafficType,
Object[] args) throws BlockException {
// =====> [2] 注意 param4 = 1!
return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}
3.2 CtSph*
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType,
int count, Object[] args) throws BlockException {
// =====> [3] count: 表示当前请求可以增加多少个计数
// Param5#prioritized = false
return entryWithType(name, resourceType, entryType, count, false, args);
}
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType,
int count, boolean prioritized, Object[] args) throws BlockException {
// 将信息封装为一个资源对象
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
// =====> [4] 返回一个资源操作对象 entry
// prioritized=true 则表示当前访问必须等待“根据其优先级计算出的时间”后才可通过
// prioritized=false 则当前请求无需等待
return entryWithPriority(resource, count, prioritized, args);
}
/**
* 核心方法!
* @param resourceWrapper 资源实例
* @param count 默认值为 1
* @param prioritized 默认值为 false
* @param args
* @return
* @throws BlockException
*/
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count,
boolean prioritized, Object... args) throws BlockException {
// =====> [5] 从 ThreadLocal 中获取 context
// 即一个请求会占用一个线程,一个线程会绑定一个 context
Context context = ContextUtil.getContext();
// 若 context 是 NullContext 类型,则表示当前系统中的 context 数量已经超出的阈值,
// 即访问请求的数量已经超出了阈值。此时直接返回一个无需做规则检测的资源操作对象。
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded
// the threshold, so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}
// =====> [6] 若当前线程中没有绑定 context,则创建 default context 并将其放入到 Thread 中
// String CONTEXT_DEFAULT_NAME = "sentinel_default_context";
if (context == null) {
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// 若全局开关是关闭的,则直接返回一个无需做规则检测的资源操作对象
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
// =====> [7] 查找 SlotChain
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
/*
* Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
* so no rule checking will be done.
*/
// 若没有找到 chain,则意味着 chain 数量超出了阈值,则直接返回一个无需做规则检测的资源操作对象
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 创建一个资源操作对象
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// =====> [8] 对资源进行操作
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
/**
* =====> [7] 延伸
*
* Get {@link ProcessorSlotChain} of the resource. new {@link ProcessorSlotChain} will
* be created if the resource doesn't relate one.
*
* <p>Same resource({@link ResourceWrapper#equals(Object)}) will share the same
* {@link ProcessorSlotChain} globally, no matter in which {@link Context}.<p/>
*
* <p>
* Note that total {@link ProcessorSlot} count must not exceed
* {@link Constants#MAX_SLOT_CHAIN_SIZE}, otherwise null will return.
* </p>
*
* @param resourceWrapper target resource
* @return {@link ProcessorSlotChain} of the resource
*/
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 从缓存 map 中获取当前资源的 SlotChain
// key 为 resource,value 为其相关的 SlotChain
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
// [DCL] 若缓存中没有相关的 SlotChain,则创建一个并放入到缓存
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
// 缓存 map 的 size >= chain 数量阈值,则直接返回 null,不再创建新的 chain
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// =====> [7] 延伸:创建新的 chain
chain = SlotChainProvider.newSlotChain();
// 防止迭代稳定性问题
Map<ResourceWrapper, ProcessorSlotChain> newMap =
new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
/**
* =====> [6] 延伸
* This class is used for skip context name checking.
*/
private final static class InternalContextUtil extends ContextUtil {
static Context internalEnter(String name) {
// =====> origin = ""
return trueEnter(name, "");
}
static Context internalEnter(String name, String origin) {
return trueEnter(name, origin);
}
}
3.3 ContextUtil
/**
* Store the context in ThreadLocal for easy access.
*/
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
/**
* =====> [5] Get {@link Context} of current thread.
*
* @return context of current thread. Null value will be return if current
* thread does't have context.
*/
public static Context getContext() {
return contextHolder.get();
}
// =====> [6] 延伸
protected static Context trueEnter(String name, String origin) {
// 尝试着从 ThreadLocal 中获取 Context
Context context = contextHolder.get();
// 若 ThreadLocal 中没有 context,则尝试着从缓存 map 中获取
if (context == null) {
// key 为 context 名称,value 为 EntranceNode (DefaultNode extends EntranceNode)
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 获取 EntranceNode —— 双重检测锁DCL —— 为了防止并发创建
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 若缓存 map 的 size 大于 context 数量的最大阈值,则直接返回 NULL_CONTEXT
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 创建一个 EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 将其添加到 ROOT
Constants.ROOT.addChild(node);
// 将新建 node 写入到缓存 map
// 为了防止“迭代稳定性问题” —— iterate stable —— 对于共享集合的写操作
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size()+1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 将 context#name 与 entranceNode 封装为 context
context = new Context(node, name);
// 初始化 context 的来源
context.setOrigin(origin);
// 将 context 写入到 ThreadLocal
contextHolder.set(context);
}
return context;
}
3.4 SlotChainProvider
// =====> [7] 延伸
public final class SlotChainProvider {
private SlotChainProvider() {}
private static volatile SlotChainBuilder slotChainBuilder = null;
/**
* The load and pick process is not thread-safe, but it's okay
* since the method should be only invoked.
* via {@code lookProcessChain} in {@link com.alibaba.csp.sentinel.CtSph} under lock.
*
* @return new created slot chain
*/
public static ProcessorSlotChain newSlotChain() {
// 若 builder 不为 null,则直接使用 builder 构建一个 chain,否则先创建一个 builder
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// =====> [7.1] 通过 SPI 方式创建一个 Builder
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
// 若通过 SPI 方式未能创建 Builder,则手工 new 一个 DefaultSlotChainBuilder
if (slotChainBuilder == null) {
// Should not go through here.
RecordLog.warn(
"[SlotChainProvider] Wrong state when resolving slot chain builder, using default"
);
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
// =====> [7.2] 通过 Builder 构建一个 chain
return slotChainBuilder.build();
}
}
3.5 DefaultSlotChainBuilder
/**
* Builder for a default {@link ProcessorSlotChain}.
*/
@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 通过 SPI 方式构建 Slot
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName()
+ ") is not an instance of AbstractLinkedProcessorSlot, "
+ "can't be added intoProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
[7.2] 全局查找名为 com.alibaba.csp.sentinel.slotchain.ProcessorSlot
的文件:
- sentinel-core
# Sentinel default ProcessorSlots com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot com.alibaba.csp.sentinel.slots.logger.LogSlot com.alibaba.csp.sentinel.slots.statistic.StatisticSlot com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot com.alibaba.csp.sentinel.slots.system.SystemSlot com.alibaba.csp.sentinel.slots.block.flow.FlowSlot com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
- sentinel-parameter-flow-control
com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot
- sentinel-api-gateway-adapter-common
com.alibaba.csp.sentinel.adapter.gateway.common.slot.GatewayFlowSlot
Constants:
public final class Constants {
public static final String SENTINEL_VERSION = VersionUtil.getVersion("1.8.1");
public final static int MAX_CONTEXT_NAME_SIZE = 2000;
public final static int MAX_SLOT_CHAIN_SIZE = 6000;
public final static String ROOT_ID = "machine-root";
public final static String CONTEXT_DEFAULT_NAME = "sentinel_default_context";
/**
* A virtual resource identifier for total inbound statistics (since 1.5.0).
*/
public final static String TOTAL_IN_RESOURCE_NAME = "__total_inbound_traffic__";
/**
* A virtual resource identifier for cpu usage statistics (since 1.6.1).
*/
public final static String CPU_USAGE_RESOURCE_NAME = "__cpu_usage__";
/**
* A virtual resource identifier for system load statistics (since 1.6.1).
*/
public final static String SYSTEM_LOAD_RESOURCE_NAME = "__system_load__";
/**
* Global ROOT statistic node that represents the universal parent node.
*/
public final static DefaultNode ROOT = new EntranceNode(
new StringResourceWrapper(ROOT_ID, EntryType.IN),
new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));
/**
* Global statistic node for inbound traffic. Usually used for {@code SystemRule} checking.
*/
public final static ClusterNode ENTRY_NODE = new ClusterNode(
TOTAL_IN_RESOURCE_NAME, ResourceTypeConstants.COMMON);
/**
* The global switch for Sentinel.
*/
public static volatile boolean ON = true;
/**
* Order of default processor slots (越小优先级越高)
*/
public static final int ORDER_NODE_SELECTOR_SLOT = -10000;
public static final int ORDER_CLUSTER_BUILDER_SLOT = -9000;
public static final int ORDER_LOG_SLOT = -8000;
public static final int ORDER_STATISTIC_SLOT = -7000;
public static final int ORDER_AUTHORITY_SLOT = -6000;
public static final int ORDER_SYSTEM_SLOT = -5000;
public static final int ORDER_FLOW_SLOT = -2000;
public static final int ORDER_DEGRADE_SLOT = -1000;
private Constants() {}
}
3.6 DefaultProcessorSlotChain
/**
* 这是一个单向链表,默认包含 1 个节点,且有两个指针 first 与 end 同时指向这个节点
*/
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper,
Object t, int count, boolean prioritized, Object... args) throws Throwable {
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
super.fireExit(context, resourceWrapper, count, args);
}
};
AbstractLinkedProcessorSlot<?> end = first;
@Override
public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
protocolProcessor.setNext(first.getNext());
first.setNext(protocolProcessor);
if (end == first) {
end = protocolProcessor;
}
}
@Override
public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
end.setNext(protocolProcessor);
end = protocolProcessor;
}
/**
* Same as {@link #addLast(AbstractLinkedProcessorSlot)}.
*
* @param next processor to be added.
*/
@Override
public void setNext(AbstractLinkedProcessorSlot<?> next) {
addLast(next);
}
@Override
public AbstractLinkedProcessorSlot<?> getNext() {
return first.getNext();
}
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t,
int count, boolean prioritized, Object... args) throws Throwable {
// =====> [8] 转向下一个节点
first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
first.exit(context, resourceWrapper, count, args);
}
}
3.7 AbstractLinkedProcessorSlot
// =====> [8] 延伸
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
/**
* 声明了一个同类型的变量,其可以指向下一个 Slot 节点
*/
private AbstractLinkedProcessorSlot<?> next = null;
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj,
int count, boolean prioritized, Object... args) throws Throwable {
if (next != null) {
// 切换到下个节点
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
@SuppressWarnings("unchecked")
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o,
int count, boolean prioritized, Object... args) throws Throwable {
T t = (T)o;
// 进入下个节点
entry(context, resourceWrapper, t, count, prioritized, args);
}
@Override
public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
if (next != null) {
next.exit(context, resourceWrapper, count, args);
}
}
public AbstractLinkedProcessorSlot<?> getNext() {
return next;
}
public void setNext(AbstractLinkedProcessorSlot<?> next) {
this.next = next;
}
}
4. 源码解析 slotChain
4.1 XxxSlot
a. NodeSelectorSlot
/**
* This class will try to build the calling traces via
* - adding a new {@link DefaultNode} if needed as the last child in the context.
* The context's last node is the current node or the parent node of the context.
* - setting itself to the context current node.
*
* It works as follow:
*
* ContextUtil.enter("entrance1", "appA");
* Entry nodeA = SphU.entry("nodeA");
* if (nodeA != null) {
* nodeA.exit();
* }
* ContextUtil.exit();
*
*
* Above code will generate the following invocation structure in memory:
*
*
* machine-root
* /
* /
* EntranceNode1
* /
* /
* DefaultNode(nodeA)- - - - - -> ClusterNode(nodeA);
*
*
* Here the {@link EntranceNode} represents "entrance1" given by
* {@code ContextUtil.enter("entrance1", "appA")}.
*
* Both DefaultNode(nodeA) and ClusterNode(nodeA) holds statistics of "nodeA",
* which is given by {@code SphU.entry("nodeA")}
*
* The {@link ClusterNode} is uniquely identified by the ResourceId; the {@link DefaultNode}
* is identified by both the resource id and {@link Context}. In other words, one resource
* id will generate multiple {@link DefaultNode} for each distinct context, but only one
* {@link ClusterNode}.
*
* the following code shows one resource id in two different context:
*
* ContextUtil.enter("entrance1", "appA");
* Entry nodeA = SphU.entry("nodeA");
* if (nodeA != null) {
* nodeA.exit();
* }
* ContextUtil.exit();
*
* ContextUtil.enter("entrance2", "appA");
* nodeA = SphU.entry("nodeA");
* if (nodeA != null) {
* nodeA.exit();
* }
* ContextUtil.exit();
*
*
* Above code will generate the following invocation structure in memory:
*
*
* machine-root
* / \
* / \
* EntranceNode1 EntranceNode2
* / \
* / \
* DefaultNode(nodeA) DefaultNode(nodeA)
* | |
* +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
*
*
* As we can see, two {@link DefaultNode} are created for "nodeA" in two context,
* but only one {@link ClusterNode} is created.
*
* We can also check this structure by calling: <br/>
* {@code curl http://localhost:8719/tree?type=root}
*
*/
@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
/**
* {@link DefaultNode}s of the same resource in different context.
*/
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj,
int count, boolean prioritized, Object... args) throws Throwable {
/*
* It's interesting that we use context name rather resource name as the map key.
*
* Remember that same resource({@link ResourceWrapper#equals(Object)}) will share
* the same {@link ProcessorSlotChain} globally, no matter in which context. So if
* code goes into {@link #entry(Context, ResourceWrapper, DefaultNode, int, Object...)},
* the resource name must be same but context name may not.
*
* If we use {@link com.alibaba.csp.sentinel.SphU#entry(String resource)} to enter
* same resource in different context, using context name as map key can distinguish
* the same resource. In this case, multiple {@link DefaultNode}s will be created of
* the same resource name, for every distinct context (different context name) each.
*
* Consider another question. One resource may have multiple {@link DefaultNode},
* so what is the fastest way to get total statistics of the same resource?
* The answer is all {@link DefaultNode}s with same resource name share one
* {@link ClusterNode}. See {@link ClusterBuilderSlot} for detail.
*/
// 从缓存中获取 DefaultNode
DefaultNode node = map.get(context.getName());
// DCL
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
// 创建一个 DefaultNode,并放入缓存 map
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
// 将新建 node 添加到调用树中
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
context.setCurNode(node);
// 触发下一个节点~
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
b. ClusterBuilderSlot
/**
* This slot maintains resource running statistics (response time, qps, thread
* count, exception), and a list of callers as well which is marked by
* {@link ContextUtil#enter(String origin)}
*
* One resource has only one cluster node, while one resource can have multiple default nodes.
*/
@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
/**
*
* Remember that same resource({@link ResourceWrapper#equals(Object)}) will share the same
* {@link ProcessorSlotChain} globally, no matter in which context. So if code goes into
* {@link #entry(Context, ResourceWrapper, DefaultNode, int, boolean, Object...)},
* the resource name must be same but context name may not.
*
* To get total statistics of the same resource in different context, same resource
* shares the same {@link ClusterNode} globally. All {@link ClusterNode}s are cached
* in this map.
*
* The longer the application runs, the more stable this mapping will
* become. so we don't concurrent map but a lock. as this lock only happens
* at the very beginning while concurrent map will hold the lock all the time.
*
*/
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
private volatile ClusterNode clusterNode = null;
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
int count, boolean prioritized, Object... args) throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
node.setClusterNode(clusterNode);
/*
* if context origin is set, we should get or create a new {@link Node}
* of the specific origin.
*/
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
/**
* Get {@link ClusterNode} of the resource of the specific type.
*
* @param id resource name.
* @param type invoke type.
* @return the {@link ClusterNode}
*/
public static ClusterNode getClusterNode(String id, EntryType type) {
return clusterNodeMap.get(new StringResourceWrapper(id, type));
}
/**
* Get {@link ClusterNode} of the resource name.
*
* @param id resource name.
* @return the {@link ClusterNode}.
*/
public static ClusterNode getClusterNode(String id) {
if (id == null) {
return null;
}
ClusterNode clusterNode = null;
for (EntryType nodeType : EntryType.values()) {
clusterNode = clusterNodeMap.get(new StringResourceWrapper(id, nodeType));
if (clusterNode != null) {
break;
}
}
return clusterNode;
}
/**
* Get {@link ClusterNode}s map, this map holds all {@link ClusterNode}s,
* it's key is resource name, value is the related {@link ClusterNode}.
* DO NOT MODIFY the map returned.
*
* @return all {@link ClusterNode}s
*/
public static Map<ResourceWrapper, ClusterNode> getClusterNodeMap() {
return clusterNodeMap;
}
/**
* Reset all {@link ClusterNode}s. Reset is needed when {@link IntervalProperty#INTERVAL}
* or {@link SampleCountProperty#SAMPLE_COUNT} is changed.
*/
public static void resetClusterNodes() {
for (ClusterNode node : clusterNodeMap.values()) {
node.reset();
}
}
}
c. StatisticSlot
/**
* A processor slot that dedicates to real time statistics.
*
* When entering this slot, we need to separately count the following information:
* - {@link ClusterNode}: total statistics of a cluster node of the resource ID.
* - Origin node: statistics of a cluster node from different callers/origins.
* - {@link DefaultNode}: statistics for specific resource name in the specific context.
* - Finally, the sum statistics of all entrances.
*/
@Spi(order = Constants.ORDER_STATISTIC_SLOT)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
int count, boolean prioritized, Object... args) throws Throwable {
try {
// Do some checking.
// 调用 SlotChain 中后续的所有 Slot,完成所有规则检测
// 其在执行过程中可能会抛出异常,例如,规则检测未通过,抛出 BlockException
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// ============ Request passed, add thread count and pass count. ============
// 代码能走到这里,说明前面所有规则检测全部通过,此时就可以将该请求统计到相应数据中了
// ~> 增加线程数据
node.increaseThreadNum();
// ~> 增加通过的请求数量
node.addPassRequest(count);
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
// Handle pass event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException ex) {
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseThreadNum();
}
// Handle pass event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
// Blocked, set block exception to current entry.
context.getCurEntry().setBlockError(e);
// Add block count.
node.increaseBlockQps(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseBlockQps(count);
}
// Handle block event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
} catch (Throwable e) {
// Unexpected internal error, set error to current entry.
context.getCurEntry().setError(e);
throw e;
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
Node node = context.getCurNode();
if (context.getCurEntry().getBlockError() == null) {
// Calculate response time (use completeStatTime as the time of completion).
long completeStatTime = TimeUtil.currentTimeMillis();
context.getCurEntry().setCompleteTimestamp(completeStatTime);
long rt = completeStatTime - context.getCurEntry().getCreateTimestamp();
Throwable error = context.getCurEntry().getError();
// Record response time and success count.
recordCompleteFor(node, count, rt, error);
recordCompleteFor(context.getCurEntry().getOriginNode(), count, rt, error);
if (resourceWrapper.getEntryType() == EntryType.IN) {
recordCompleteFor(Constants.ENTRY_NODE, count, rt, error);
}
}
// Handle exit event with registered exit callback handlers.
Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
for (ProcessorSlotExitCallback handler : exitCallbacks) {
handler.onExit(context, resourceWrapper, count, args);
}
fireExit(context, resourceWrapper, count);
}
private void recordCompleteFor(Node node, int batchCount, long rt, Throwable error) {
if (node == null) {
return;
}
node.addRtAndSuccess(rt, batchCount);
node.decreaseThreadNum();
if (error != null && !(error instanceof BlockException)) {
node.increaseExceptionQps(batchCount);
}
}
}
d. FlowSlot
/**
*
* Combined the runtime statistics collected from the previous
* slots (NodeSelectorSlot, ClusterNodeBuilderSlot, and StatisticSlot), FlowSlot
* will use pre-set rules to decide whether the incoming requests should be
* blocked.
*
*
*
* {@code SphU.entry(resourceName)} will throw {@code FlowException} if any rule is
* triggered. Users can customize their own logic by catching {@code FlowException}.
*
*
*
* One resource can have multiple flow rules. FlowSlot traverses these rules
* until one of them is triggered or all rules have been traversed.
*
*
*
* Each {@link FlowRule} is mainly composed of these factors: grade, strategy, path. We
* can combine these factors to achieve different effects.
*
*
*
* The grade is defined by the {@code grade} field in {@link FlowRule}. Here, 0 for thread
* isolation and 1 for request count shaping (QPS). Both thread count and request
* count are collected in real runtime, and we can view these statistics by
* following command:
*
* curl http://localhost:8719/tree
*
* idx id thread pass blocked success total aRt 1m-pass 1m-block 1m-all exception
* 2 abc647 0 460 46 46 1 27 630 276 897 0
*
*
*
* {@code thread} for the count of threads that is currently processing the resource
* {@code pass} for the count of incoming request within one second
* {@code blocked} for the count of requests blocked within one second
* {@code success} for the count of the requests successfully handled by Sentinel within one second
* {@code RT} for the average response time of the requests within a second
* {@code total} for the sum of incoming requests and blocked requests within one second
* {@code 1m-pass} is for the count of incoming requests within one minute
* {@code 1m-block} is for the count of a request blocked within one minute
* {@code 1m-all} is the total of incoming and blocked requests within one minute
* {@code exception} is for the count of business (customized) exceptions in one second
*
*
* This stage is usually used to protect resources from occupying. If a resource
* takes long time to finish, threads will begin to occupy. The longer the
* response takes, the more threads occupy.
*
* Besides counter, thread pool or semaphore can also be used to achieve this.
*
* - Thread pool: Allocate a thread pool to handle these resource. When there is
* no more idle thread in the pool, the request is rejected without affecting
* other resources.
*
* - Semaphore: Use semaphore to control the concurrent count of the threads in
* this resource.
*
* The benefit of using thread pool is that, it can walk away gracefully when
* time out. But it also bring us the cost of context switch and additional
* threads. If the incoming requests is already served in a separated thread,
* for instance, a Servlet HTTP request, it will almost double the threads count if
* using thread pool.
*
* Traffic Shaping
*
* When QPS exceeds the threshold, Sentinel will take actions to control the incoming request,
* and is configured by {@code controlBehavior} field in flow rules.
*
*
* Immediately reject ({@code RuleConstant.CONTROL_BEHAVIOR_DEFAULT})
*
* This is the default behavior. The exceeded request is rejected immediately
* and the FlowException is thrown
*
*
* Warmup ({@code RuleConstant.CONTROL_BEHAVIOR_WARM_UP})
*
* If the load of system has been low for a while, and a large amount of
* requests comes, the system might not be able to handle all these requests at
* once. However if we steady increase the incoming request, the system can warm
* up and finally be able to handle all the requests.
* This warmup period can be configured by setting the field {@code warmUpPeriodSec}
* in flow rules.
*
*
* Uniform Rate Limiting ({@code RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER})
*
* This strategy strictly controls the interval between requests.
* In other words, it allows requests to pass at a stable, uniform rate.
*
* https://raw.githubusercontent.com/wiki/alibaba/Sentinel/image/uniform-speed-queue.png
*
* This strategy is an implement of <a href="https://en.wikipedia.org/wiki/Leaky_bucket">
* leaky bucket.
* It is used to handle the request at a stable rate and is often used in burst traffic
* (e.g. message handling).
*
* When a large number of requests beyond the system’s capacity arrive
* at the same time, the system using this strategy will handle requests and its
* fixed rate until all the requests have been processed or time out.
*/
@Spi(order = Constants.ORDER_FLOW_SLOT)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
private final FlowRuleChecker checker;
public FlowSlot() {
this(new FlowRuleChecker());
}
/**
* Package-private for test.
*
* @param checker flow rule checker
* @since 1.6.1
*/
FlowSlot(FlowRuleChecker checker) {
AssertUtil.notNull(checker, "flow checker should not be null");
this.checker = checker;
}
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
int count, boolean prioritized, Object... args) throws Throwable {
// =====> 检测并应用流控规则
checkFlow(resourceWrapper, context, node, count, prioritized);
// 触发下一个 Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
@Override
public Collection<FlowRule> apply(String resource) {
// Flow rule map should not be null.
// 获取到所有资源的流控规则 map 的 key 为资源名称,value 为该资源上加载的所有流控规则
Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
// 获取指定资源的所有流控规则
return flowRules.get(resource);
}
};
}
e. DegradeSlot
/**
* A {@link ProcessorSlot} dedicates to circuit breaking.
*/
@Spi(order = Constants.ORDER_DEGRADE_SLOT)
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
int count, boolean prioritized, Object... args) throws Throwable {
// 完成熔断降级检测
performChecking(context, resourceWrapper);
// 触发下一个节点
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 获取到当前资源的所有熔断器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
// 若熔断器为空,则直结束
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
return;
}
for (CircuitBreaker cb : circuitBreakers) {
// =====> 逐个尝试所有熔断器。若没有通过当前熔断器,则直接抛出异常
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
@Override
public void exit(Context context, ResourceWrapper r, int count, Object... args) {
Entry curEntry = context.getCurEntry();
if (curEntry.getBlockError() != null) {
fireExit(context, r, count, args);
return;
}
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
fireExit(context, r, count, args);
return;
}
if (curEntry.getBlockError() == null) {
// passed request
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
}
fireExit(context, r, count, args);
}
}
4.2 FlowSlot_Ext
a. Rule~Bean
AbstractRule
public abstract class AbstractRule implements Rule {
/**
* 资源名称
*
* Resource name.
*/
private String resource;
/**
* 请求来源
*
* Application name that will be limited by origin.
* The default limitApp is {@code default}, which means allowing all origin apps.
*
* For authority rules, multiple origin name can be separated with comma (',').
*/
private String limitApp;
// ...
}
FlowRule
public class FlowRule extends AbstractRule {
public FlowRule() {
super();
setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
}
public FlowRule(String resourceName) {
super();
setResource(resourceName);
setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
}
/**
* 阈值类型:0 表示线程数限流、1 表示 QPS 限流
* The threshold type of flow control (0: thread count, 1: QPS).
*/
private int grade = RuleConstant.FLOW_GRADE_QPS;
/**
* [阈值] Flow control threshold count.
*/
private double count;
/**
* [流控模式] Flow control strategy based on invocation chain.
*
* {RuleConstant#STRATEGY_DIRECT#直接流控} for direct flow control (by origin);
* {RuleConstant#STRATEGY_RELATE#关联流控} for relevant flow control (with relevant resource);
* {RuleConstant#STRATEGY_CHAIN #链路流控} for chain flow control (by entrance resource).
*/
private int strategy = RuleConstant.STRATEGY_DIRECT;
/**
* 若流控模式为关联流控时的关联资源
* Reference resource in flow control with relevant resource or context.
*/
private String refResource;
/**
* [流控效果] Rate limiter control behavior.
* 0. default(reject directly), 1. warm up, 2. rate limiter, 3. warm up + rate limiter
* 0-快速失败,1-预热令牌桶算法),2-排队等待(漏斗算法),3-预热+排队等待
*/
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
/**
* warm up 预热时长
*/
private int warmUpPeriodSec = 10;
/**
* 排队等待的超时时间 Max queueing time in rate limiter behavior.
*/
private int maxQueueingTimeMs = 500;
/**
* 是否集群模式
*/
private boolean clusterMode;
/**
* Flow rule config for cluster mode.
*/
private ClusterFlowConfig clusterConfig;
/**
* The traffic shaping (throttling) controller.
*/
private TrafficShapingController controller;
// ...
}
b. FlowRuleChecker
/**
* Rule checker for flow control rules.
*/
public class FlowRuleChecker {
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
// 获取到指定资源的所有流控规则
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
// =====> 逐个应用流控规则。若无法通过则抛出异常,后续规则不再应用
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
public boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount) {
return canPassCheck(rule, context, node, acquireCount, false);
}
public boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
// 从规则中获取要限定的来源
String limitApp = rule.getLimitApp();
// 若限流的来源为 null,则请求直接通过
if (limitApp == null) {
return true;
}
// 使用规则处理〈集群流控〉
if (rule.isClusterMode()) {
return passClusterCheck(rule, context, node, acquireCount, prioritized);
}
// =====> 使用规则处理〈单机流控〉
return passLocalCheck(rule, context, node, acquireCount, prioritized);
}
private static boolean passLocalCheck(FlowRule rule, Context context,
DefaultNode node, int acquireCount, boolean prioritized) {
// 通过 rule 形成 selectedRuleNode
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
// 若没有选择出 ruleNode,说明没有规则,则直接返回 true,表示通过检测
if (selectedNode == null) {
return true;
}
// =====> 使用规则进行逐项检测(见下图)
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
static Node selectReferenceNode(FlowRule rule, Context context, DefaultNode node) {
String refResource = rule.getRefResource();
int strategy = rule.getStrategy();
if (StringUtil.isEmpty(refResource)) {
return null;
}
if (strategy == RuleConstant.STRATEGY_RELATE) {
return ClusterBuilderSlot.getClusterNode(refResource);
}
if (strategy == RuleConstant.STRATEGY_CHAIN) {
if (!refResource.equals(context.getName())) {
return null;
}
return node;
}
// No node.
return null;
}
private static boolean filterOrigin(String origin) {
// Origin cannot be `default` or `other`.
return !RuleConstant.LIMIT_APP_DEFAULT.equals(origin) && !RuleConstant.LIMIT_APP_OTHER.equals(origin);
}
static Node selectNodeByRequesterAndStrategy(FlowRule rule, Context context, DefaultNode node) {
// The limit app should not be empty.
String limitApp = rule.getLimitApp();
int strategy = rule.getStrategy();
String origin = context.getOrigin();
if (limitApp.equals(origin) && filterOrigin(origin)) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// Matches limit origin, return origin statistic node.
return context.getOriginNode();
}
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// Return the cluster node.
return node.getClusterNode();
}
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
&& FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
return context.getOriginNode();
}
return selectReferenceNode(rule, context, node);
}
return null;
}
private static boolean passClusterCheck(FlowRule rule, Context context,
DefaultNode node, int acquireCount, boolean prioritized) {
try {
TokenService clusterService = pickClusterService();
if (clusterService == null) {
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
}
long flowId = rule.getClusterConfig().getFlowId();
TokenResult result = clusterService.requestToken(flowId, acquireCount, prioritized);
return applyTokenResult(result, rule, context, node, acquireCount, prioritized);
// If client is absent, then fallback to local mode.
} catch (Throwable ex) {
RecordLog.warn("[FlowRuleChecker] Request cluster token unexpected failed", ex);
}
// Fallback to local flow control when token client or server for this rule is not available.
// If fallback is not enabled, then directly pass.
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
}
private static boolean fallbackToLocalOrPass(FlowRule rule, Context context,
DefaultNode node, int acquireCount, boolean prioritized) {
if (rule.getClusterConfig().isFallbackToLocalWhenFail()) {
return passLocalCheck(rule, context, node, acquireCount, prioritized);
} else {
// The rule won't be activated, just pass.
return true;
}
}
private static TokenService pickClusterService() {
if (ClusterStateManager.isClient()) {
return TokenClientProvider.getClient();
}
if (ClusterStateManager.isServer()) {
return EmbeddedClusterTokenServerProvider.getServer();
}
return null;
}
private static boolean applyTokenResult(/*@NonNull*/ TokenResult result, FlowRule rule,
Context context, DefaultNode node, int acquireCount, boolean prioritized) {
switch (result.getStatus()) {
case TokenResultStatus.OK:
return true;
case TokenResultStatus.SHOULD_WAIT:
// Wait for next tick.
try {
Thread.sleep(result.getWaitInMs());
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
case TokenResultStatus.NO_RULE_EXISTS:
case TokenResultStatus.BAD_REQUEST:
case TokenResultStatus.FAIL:
case TokenResultStatus.TOO_MANY_REQUEST:
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
case TokenResultStatus.BLOCKED:
default:
return false;
}
}
}
使用规则进行逐项检测:
c. TrafficShapingController
/**
* A universal interface for traffic shaping controller.
*/
public interface TrafficShapingController {
/**
* Check whether given resource entry can pass with provided count.
*
* @param node resource node
* @param acquireCount count to acquire
* @param prioritized whether the request is prioritized
* @return true if the resource entry can pass; false if it should be blocked
*/
boolean canPass(Node node, int acquireCount, boolean prioritized);
/**
* Check whether given resource entry can pass with provided count.
*
* @param node resource node
* @param acquireCount count to acquire
* @return true if the resource entry can pass; false if it should be blocked
*/
boolean canPass(Node node, int acquireCount);
}
DefaultController
public class DefaultController implements TrafficShapingController {
// ...
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
/**
* 快速失败的流控效果中的通过性判断
* @param node resource node
* @param acquireCount count to acquire
* @param prioritized whether the request is prioritized
* @return
*/
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前时间窗中已经统计的数据
int curCount = avgUsedTokens(node);
// 已经统计的数据 + 本次请求的数量
// - 若 > 设置的阈值,则返回 false 表示没有通过检测
// - 若 <= 阈值,则返回true 表示通过检测
if (curCount + acquireCount > count) {
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
// PriorityWaitException indicates that the request will pass
// after waiting for {@link @waitInMs}.
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
private int avgUsedTokens(Node node) {
// 若没有选择出 node,则说明没有做统计工作,直接返回 0
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
// 若阈值类型为线程数,则直接返回当前的线程数量;
// 若阈值类型为 QPS,则返回统计的当前的 QPS
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
}
RateLimiterController
WarmUpController
WarmUpRateLimiterController
4.3 DegradeSlot_Ext
a. CircuitBreaker
/**
* Sentinel 1.8 将 3 种熔断策略(慢调用/异常比/异常数)封装为为 2 种熔断器:
* 1. 响应时间熔断器 ResponseTimeCircuitBreaker
* 2. 异常熔断器 ExceptionCircuitBreaker
*/
public interface CircuitBreaker {
/**
* 获取降级规则 Get the associated circuit breaking rule.
* @return associated circuit breaking rule
*/
DegradeRule getRule();
/**
* 判断请求是否可以通过,返回 true,表示通过,则不用降级;否则降级
*
* Acquires permission of an invocation only if it is available at the time of invoking.
*
* @param context context of current invocation
* @return {@code true} if permission was acquired and {@code false} otherwise
*/
boolean tryPass(Context context);
/**
* 获取当前熔断器状态
*
* Get current state of the circuit breaker.
* @return current state of the circuit breaker
*/
State currentState();
/**
* 回调方法:当请求通过并完成后会触发
*
* Record a completed request with the context and handle
* state transformation of the circuit breaker.
* Called when a passed invocation finished.
* @param context context of current invocation
*/
void onRequestComplete(Context context);
/**
* Circuit breaker state.
*/
enum State {
/**
* 打开状态,会拒绝所有请求
*
* In {@code OPEN} state, all requests will be rejected until the next recovery time point.
*/
OPEN,
/**
* 过滤状态
*
* In {@code HALF_OPEN} state, the circuit breaker will allow a "probe" invocation.
* If the invocation is abnormal according to the strategy (e.g. it's slow),
* the circuit breaker will re-transform to the {@code OPEN} state and
* wait for the next recovery time point;
*
* otherwise the resource will be regarded as "recovered" and the circuit breaker
* will cease cutting off requests and transform to {@code CLOSED} state.
*/
HALF_OPEN,
/**
* 关闭状态,所有请求可以通过
*
* In {@code CLOSED} state, all requests are permitted.
* When current metric value exceeds the threshold,
* the circuit breaker will transform to {@code OPEN} state.
*/
CLOSED
}
}
b. AbstractCircuitBreaker
public abstract class AbstractCircuitBreaker implements CircuitBreaker {
protected final DegradeRule rule;
protected final int recoveryTimeoutMs;
private final EventObserverRegistry observerRegistry;
protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED);
protected volatile long nextRetryTimestamp;
public AbstractCircuitBreaker(DegradeRule rule) {
this(rule, EventObserverRegistry.getInstance());
}
AbstractCircuitBreaker(DegradeRule rule, EventObserverRegistry observerRegistry) {
AssertUtil.notNull(observerRegistry, "observerRegistry cannot be null");
if (!DegradeRuleManager.isValidRule(rule)) {
throw new IllegalArgumentException("Invalid DegradeRule: " + rule);
}
this.observerRegistry = observerRegistry;
this.rule = rule;
this.recoveryTimeoutMs = rule.getTimeWindow() * 1000;
}
@Override
public DegradeRule getRule() {
return rule;
}
@Override
public State currentState() {
return currentState.get();
}
// =====================================================================
@Override
public boolean tryPass(Context context) {
// 熔断器状态为关闭状态,则请求可以通过
if (currentState.get() == State.CLOSED) {
return true;
}
// 熔断器状态为打开状态,此时再查看,
// 若下次时间窗时间点已经到达,且熔断器成功由 Open 变为了 Half-Open,则请求通过
if (currentState.get() == State.OPEN) {
// For half-open state we allow a request for probing.
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
return false;
}
// =====================================================================
/**
* Reset the statistic data.
*/
abstract void resetStat();
protected boolean retryTimeoutArrived() {
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
protected void updateNextRetryTimestamp() {
this.nextRetryTimestamp = TimeUtil.currentTimeMillis() + recoveryTimeoutMs;
}
protected boolean fromCloseToOpen(double snapshotValue) {
State prev = State.CLOSED;
if (currentState.compareAndSet(prev, State.OPEN)) {
updateNextRetryTimestamp();
notifyObservers(prev, State.OPEN, snapshotValue);
return true;
}
return false;
}
protected boolean fromOpenToHalfOpen(Context context) {
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
notifyObservers(State.OPEN, State.HALF_OPEN, null);
Entry entry = context.getCurEntry();
entry.whenTerminate(new BiConsumer<Context, Entry>() {
@Override
public void accept(Context context, Entry entry) {
// Note: This works as a temporary workaround for https://github.com/alibaba/Sentinel/issues/1638
// Without the hook, the circuit breaker won't recover from half-open state in some circumstances
// when the request is actually blocked by upcoming rules (not only degrade rules).
if (entry.getBlockError() != null) {
// Fallback to OPEN due to detecting request is blocked
currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
}
}
});
return true;
}
return false;
}
private void notifyObservers(CircuitBreaker.State prevState, CircuitBreaker.State newState, Double snapshotValue) {
for (CircuitBreakerStateChangeObserver observer : observerRegistry.getStateChangeObservers()) {
observer.onStateChange(prevState, newState, rule, snapshotValue);
}
}
protected boolean fromHalfOpenToOpen(double snapshotValue) {
if (currentState.compareAndSet(State.HALF_OPEN, State.OPEN)) {
updateNextRetryTimestamp();
notifyObservers(State.HALF_OPEN, State.OPEN, snapshotValue);
return true;
}
return false;
}
protected boolean fromHalfOpenToClose() {
if (currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) {
resetStat();
notifyObservers(State.HALF_OPEN, State.CLOSED, null);
return true;
}
return false;
}
protected void transformToOpen(double triggerValue) {
State cs = currentState.get();
switch (cs) {
case CLOSED:
fromCloseToOpen(triggerValue);
break;
case HALF_OPEN:
fromHalfOpenToOpen(triggerValue);
break;
default:
break;
}
}
}
c. ExceptionCircuitBreaker*
public class ExceptionCircuitBreaker extends AbstractCircuitBreaker {
private final int strategy;
private final int minRequestAmount;
private final double threshold;
private final LeapArray<SimpleErrorCounter> stat;
public ExceptionCircuitBreaker(DegradeRule rule) {
this(rule, new SimpleErrorCounterLeapArray(1, rule.getStatIntervalMs()));
}
ExceptionCircuitBreaker(DegradeRule rule, LeapArray<SimpleErrorCounter> stat) {
super(rule);
this.strategy = rule.getGrade();
boolean modeOk =
strategy == DEGRADE_GRADE_EXCEPTION_RATIO
|| strategy == DEGRADE_GRADE_EXCEPTION_COUNT;
AssertUtil.isTrue(modeOk, "rule strategy should be error-ratio or error-count");
AssertUtil.notNull(stat, "stat cannot be null");
this.minRequestAmount = rule.getMinRequestAmount();
this.threshold = rule.getCount();
this.stat = stat;
}
@Override
protected void resetStat() {
// Reset current bucket (bucket count = 1).
stat.currentWindow().value().reset();
}
@Override
public void onRequestComplete(Context context) {
Entry entry = context.getCurEntry();
if (entry == null) {
return;
}
Throwable error = entry.getError();
SimpleErrorCounter counter = stat.currentWindow().value();
if (error != null) {
counter.getErrorCount().add(1);
}
counter.getTotalCount().add(1);
handleStateChangeWhenThresholdExceeded(error);
}
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
if (currentState.get() == State.OPEN) {
return;
}
if (currentState.get() == State.HALF_OPEN) {
// In detecting request
if (error == null) {
fromHalfOpenToClose();
} else {
fromHalfOpenToOpen(1.0d);
}
return;
}
List<SimpleErrorCounter> counters = stat.values();
long errCount = 0;
long totalCount = 0;
for (SimpleErrorCounter counter : counters) {
errCount += counter.errorCount.sum();
totalCount += counter.totalCount.sum();
}
if (totalCount < minRequestAmount) {
return;
}
double curCount = errCount;
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
// Use errorRatio
curCount = errCount * 1.0d / totalCount;
}
if (curCount > threshold) {
transformToOpen(curCount);
}
}
static class SimpleErrorCounter {
private LongAdder errorCount;
private LongAdder totalCount;
public SimpleErrorCounter() {
this.errorCount = new LongAdder();
this.totalCount = new LongAdder();
}
public LongAdder getErrorCount() {
return errorCount;
}
public LongAdder getTotalCount() {
return totalCount;
}
public SimpleErrorCounter reset() {
errorCount.reset();
totalCount.reset();
return this;
}
@Override
public String toString() {
return "SimpleErrorCounter{" +
"errorCount=" + errorCount +
", totalCount=" + totalCount +
'}';
}
}
static class SimpleErrorCounterLeapArray extends LeapArray<SimpleErrorCounter> {
public SimpleErrorCounterLeapArray(int sampleCount, int intervalInMs) {
super(sampleCount, intervalInMs);
}
@Override
public SimpleErrorCounter newEmptyBucket(long timeMillis) {
return new SimpleErrorCounter();
}
@Override
protected WindowWrap<SimpleErrorCounter> resetWindowTo(WindowWrap<SimpleErrorCounter> w, long startTime) {
// Update the start time and reset value.
w.resetTo(startTime);
w.value().reset();
return w;
}
}
}
d. ResponseTimeCircuitBreaker*
public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker {
private static final double SLOW_REQUEST_RATIO_MAX_VALUE = 1.0d;
private final long maxAllowedRt;
private final double maxSlowRequestRatio;
private final int minRequestAmount;
private final LeapArray<SlowRequestCounter> slidingCounter;
public ResponseTimeCircuitBreaker(DegradeRule rule) {
this(rule, new SlowRequestLeapArray(1, rule.getStatIntervalMs()));
}
ResponseTimeCircuitBreaker(DegradeRule rule, LeapArray<SlowRequestCounter> stat) {
super(rule);
AssertUtil.isTrue(rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT, "rule metric type should be RT");
AssertUtil.notNull(stat, "stat cannot be null");
this.maxAllowedRt = Math.round(rule.getCount());
this.maxSlowRequestRatio = rule.getSlowRatioThreshold();
this.minRequestAmount = rule.getMinRequestAmount();
this.slidingCounter = stat;
}
@Override
public void resetStat() {
// Reset current bucket (bucket count = 1).
slidingCounter.currentWindow().value().reset();
}
@Override
public void onRequestComplete(Context context) {
SlowRequestCounter counter = slidingCounter.currentWindow().value();
Entry entry = context.getCurEntry();
if (entry == null) {
return;
}
long completeTime = entry.getCompleteTimestamp();
if (completeTime <= 0) {
completeTime = TimeUtil.currentTimeMillis();
}
long rt = completeTime - entry.getCreateTimestamp();
if (rt > maxAllowedRt) {
counter.slowCount.add(1);
}
counter.totalCount.add(1);
handleStateChangeWhenThresholdExceeded(rt);
}
private void handleStateChangeWhenThresholdExceeded(long rt) {
if (currentState.get() == State.OPEN) {
return;
}
if (currentState.get() == State.HALF_OPEN) {
// In detecting request
// TODO: improve logic for half-open recovery
if (rt > maxAllowedRt) {
fromHalfOpenToOpen(1.0d);
} else {
fromHalfOpenToClose();
}
return;
}
List<SlowRequestCounter> counters = slidingCounter.values();
long slowCount = 0;
long totalCount = 0;
for (SlowRequestCounter counter : counters) {
slowCount += counter.slowCount.sum();
totalCount += counter.totalCount.sum();
}
if (totalCount < minRequestAmount) {
return;
}
double currentRatio = slowCount * 1.0d / totalCount;
if (currentRatio > maxSlowRequestRatio) {
transformToOpen(currentRatio);
}
if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
transformToOpen(currentRatio);
}
}
static class SlowRequestCounter {
private LongAdder slowCount;
private LongAdder totalCount;
public SlowRequestCounter() {
this.slowCount = new LongAdder();
this.totalCount = new LongAdder();
}
public LongAdder getSlowCount() {
return slowCount;
}
public LongAdder getTotalCount() {
return totalCount;
}
public SlowRequestCounter reset() {
slowCount.reset();
totalCount.reset();
return this;
}
@Override
public String toString() {
return "SlowRequestCounter{" +
"slowCount=" + slowCount +
", totalCount=" + totalCount +
'}';
}
}
static class SlowRequestLeapArray extends LeapArray<SlowRequestCounter> {
public SlowRequestLeapArray(int sampleCount, int intervalInMs) {
super(sampleCount, intervalInMs);
}
@Override
public SlowRequestCounter newEmptyBucket(long timeMillis) {
return new SlowRequestCounter();
}
@Override
protected WindowWrap<SlowRequestCounter> resetWindowTo(WindowWrap<SlowRequestCounter> w, long startTime) {
w.resetTo(startTime);
w.value().reset();
return w;
}
}
}