前言
Sentinel1.8总共有8个重要的Slot,都是通过SPI机制加载的。
上两章学习了6个ProcessorSlot:
- NodeSelectorSlot:构建资源(Resource)的路径(DefaultNode),用树的结构存储
- ClusterBuilderSlot:构建ClusterNode,用于记录资源维度的统计信息
- StatisticSlot:使用Node记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑
- AuthoritySlot:授权规则校验
- SystemSlot:系统规则校验
- FlowSlot:流控规则校验
本章学习最后2个个ProcessorSlot,分别对应不同的规则校验:
- ParamFlowSlot:热点参数流控规则校验
- DegradeSlot:降级规则校验
1、ParamFlowSlot
ParamFlowSlot处理热点参数流控规则校验,在FlowSlot流控规则校验的基础上,增加了参数例外项。资源除了基础的阈值限制以外,可以控制不同入参的阈值限制。如下图所示,对于资源hot2,默认QPS阈值是3,但是对于方法第一个参数为B时,QPS阈值是30。
编码方式配置热点规则:
private static void initParamFlowRules() {
// QPS mode, threshold is 5 for every frequent "hot spot" parameter in index 0 (the first arg).
ParamFlowRule rule = new ParamFlowRule(RESOURCE_KEY)
.setParamIdx(0)
.setGrade(RuleConstant.FLOW_GRADE_QPS)
.setCount(5);
// We can set threshold count for specific parameter value individually.
// Here we add an exception item. That means: QPS threshold of entries with parameter `PARAM_B` (type: int)
// in index 0 will be 10, rather than the global threshold (5).
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
.setClassType(int.class.getName())
.setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}
ParamFlowRule热点规则的成员变量如下:
public class ParamFlowRule extends AbstractRule {
// 阈值类型
private int grade = RuleConstant.FLOW_GRADE_QPS;
// 参数下标
private Integer paramIdx;
// 阈值
private double count;
// 流控效果 0-快速失败 1-warm up(热点规则不支持,效果同0) 2-排队等待
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
// 排队等待超时时间
private int maxQueueingTimeMs = 0;
// 膨胀数量 阈值类型为QPS时,决定令牌桶的最大数量
private int burstCount = 0;
// 时间窗口
private long durationInSec = 1;
// 参数例外项
private List<ParamFlowItem> paramFlowItemList = new ArrayList<ParamFlowItem>();
// Sentinel内部使用,用于包装例外项
private Map<Object, Integer> hotItems = new HashMap<Object, Integer>();
}
// 例外项
public class ParamFlowItem {
// 例外项入参值,String类型
private String object;
// 阈值
private Integer count;
// Class
private String classType;
}
ParamFlowSlot的entry方法逻辑如下:
- 校验热点规则索引下标是否小于0,如果小于0做相应调整,忽略这段逻辑,因为正常配置,不会配置小于0的参数下标;
- 初始化资源对应ParameterMetric,用于统计热点参数频率;
- ParamFlowChecker.passCheck执行热点规则校验;
@Spi(order = -3000)
public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
fireEntry(context, resourceWrapper, node, count, prioritized, args);
return;
}
checkFlow(resourceWrapper, count, args);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
if (args == null) {
return;
}
if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
return;
}
List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
for (ParamFlowRule rule : rules) {
// 1 如果规则索引下标小于0,调整为正数 忽略
applyRealParamIdx(rule, args.length);
// 2 初始化ParameterMetric,用于统计热点参数频率
ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
// 3 校验热点规则
if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
String triggeredParam = "";
if (args.length > rule.getParamIdx()) {
Object value = args[rule.getParamIdx()];
triggeredParam = String.valueOf(value);
}
throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
}
}
}
}
初始化ParameterMetric
ParameterMetricStorage.initParamMetricsFor方法,初始化ParameterMetric,从全局metricsMap中获取资源对应ParameterMetric。
public final class ParameterMetricStorage {
// k=资源名称 v=ParameterMetric
private static final Map<String, ParameterMetric> metricsMap = new ConcurrentHashMap<>();
private static final Object LOCK = new Object();
public static void initParamMetricsFor(ResourceWrapper resourceWrapper, ParamFlowRule rule) {
if (resourceWrapper == null || resourceWrapper.getName() == null) {
return;
}
String resourceName = resourceWrapper.getName();
ParameterMetric metric;
if ((metric = metricsMap.get(resourceName)) == null) {
synchronized (LOCK) {
if ((metric = metricsMap.get(resourceName)) == null) {
metric = new ParameterMetric();
metricsMap.put(resourceWrapper.getName(), metric);
RecordLog.info("[ParameterMetricStorage] Creating parameter metric for: {}", resourceWrapper.getName());
}
}
}
// ParameterMetric的初始化方法
metric.initialize(rule);
}
}
ParameterMetric初始化三个维度的计数器。
特点是这三个ConcurrentLinkedHashMapWrapper底层都是用的google包下的基于LRU算法的LinkedHashMap。超过最大容量后,会淘汰最近最少使用的kv对,以防止内存泄露。
public class ParameterMetric {
private static final int THREAD_COUNT_MAX_CAPACITY = 4000;
private static final int BASE_PARAM_MAX_CAPACITY = 4000;
private static final int TOTAL_MAX_CAPACITY = 20_0000;
private final Object lock = new Object();
// 热点规则 - 入参 - 上次投递令牌时间 阈值类型为QPS时使用
private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTimeCounters = new HashMap<>();
// 热点规则 - 入参 - 令牌桶 阈值类型为QPS时使用
private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTokenCounter = new HashMap<>();
// 下标索引 - 索引位置对应入参 - 并发线程数 阈值类型为并发线程数时使用
private final Map<Integer, CacheMap<Object, AtomicInteger>> threadCountMap = new HashMap<>();
public void initialize(ParamFlowRule rule) {
if (!ruleTimeCounters.containsKey(rule)) {
synchronized (lock) {
if (ruleTimeCounters.get(rule) == null) {
long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
ruleTimeCounters.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
}
}
}
if (!ruleTokenCounter.containsKey(rule)) {
synchronized (lock) {
if (ruleTokenCounter.get(rule) == null) {
long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
ruleTokenCounter.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
}
}
}
if (!threadCountMap.containsKey(rule.getParamIdx())) {
synchronized (lock) {
if (threadCountMap.get(rule.getParamIdx()) == null) {
threadCountMap.put(rule.getParamIdx(),
new ConcurrentLinkedHashMapWrapper<Object, AtomicInteger>(THREAD_COUNT_MAX_CAPACITY));
}
}
}
}
}
选择入参
ParamFlowChecker的passCheck方法,首先根据规则配置下标,选择入参。
注意虽然Sentinel只支持基础数据类型和String等类型的热点规则配置,但是通过编码方式可以实现任意类型参数的热点规则。这里看到passCheck对于ParamFlowArgument类型的入参做了特殊处理,将参与规则校验的入参,替换为了ParamFlowArgument.paramFlowKey方法返回的参数。
public final class ParamFlowChecker {
public static boolean passCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object... args) {
if (args == null) {
return true;
}
// 1. 选择value入参
int paramIdx = rule.getParamIdx();
if (args.length <= paramIdx) {
return true;
}
Object value = args[paramIdx];
// 入参实现ParamFlowArgument,可以实现任意object类型的入参配置热点规则
if (value instanceof ParamFlowArgument) {
value = ((ParamFlowArgument) value).paramFlowKey();
}
if (value == null) {
return true;
}
// 2. 集群 or 单机规则校验
if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
return passClusterCheck(resourceWrapper, rule, count, value);
}
return passLocalCheck(resourceWrapper, rule, count, value);
}
}
这里暂时只看单机规则校验逻辑。
入参是否为集合
passLocalCheck区分了入参是否是集合或数组类型,如果是的话,循环集合或数组中每个元素,进行规则校验;如果是普通类型,直接进行规则校验。
// ParamFlowChecker.java
private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
Object value) {
try {
// 集合类型,循环校验集合中每个参数
if (Collection.class.isAssignableFrom(value.getClass())) {
for (Object param : ((Collection)value)) {
if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
return false;
}
}
}
// 数组类型,循环校验数组中每个参数
else if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
Object param = Array.get(value, i);
if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
return false;
}
}
}
// 其他直接校验入参
else {
return passSingleValueCheck(resourceWrapper, rule, count, value);
}
} catch (Throwable e) {
RecordLog.warn("[ParamFlowChecker] Unexpected error", e);
}
return true;
}
passSingleValueCheck单值规则校验。
当阈值类型为QPS时,区分流控效果,根据不同流控效果决定规则校验逻辑。注意在控制台上并不能设置流控效果,会采用默认流控效果;
当阈值类型为并发线程数时,通过ParameterMetric获取当前并发线程数,如果超过阈值,返回false。
// ParamFlowChecker.java
static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
Object value) {
// case1 : 阈值类型 QPS
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
// case1-1 : 流控效果 排队等待
if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
} else {
// case1-2 : 流控效果 默认
return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
}
}
// case2 : 阈值类型 并发线程数
else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
// 例外项入参集合
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
// 热点参数目前并发线程数
long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
// 如果热点参数在例外项中,取例外项中的阈值做校验,否则取热点规则默认阈值做校验
if (exclusionItems.contains(value)) {
int itemThreshold = rule.getParsedHotItems().get(value);
return ++threadCount <= itemThreshold;
}
long threshold = (long)rule.getCount();
return ++threadCount <= threshold;
}
return true;
}
阈值类型=QPS,流控效果=默认
当流控效果不为RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER排队等待时,即为默认流控效果逻辑。默认流控效果通过令牌桶算法实现,当请求进来后cas修改令牌桶,如果令牌数量小于0,则返回false。
- 确定入参是否匹配例外项,如果匹配例外项,阈值QPS取例外项的,否则取规则级别的阈值QPS;
- 假设配置时间窗口为3s,则每3秒对应一个令牌桶,令牌初始数量=配置阈值QPS + 配置burstCount,这个burstCount默认是0,比如配置阈值QPS是10,则令牌桶默认情况下初始容量为10;
- 如果距离上次访问超过3s时间窗口,按照时间比率 * 阈值QPS,填充令牌桶。比如当前距离上次访问为4s,阈值QPS为10,则新增token = (4 - 3) * 10 / 3 = 3,假设剩余token为2,本次申请token为1,令牌数量更新为Math.min(10, 3+ 2 - 1) = 10,不能超过令牌桶的最大容量;
- 如果距离上次访问不超过3s时间窗口,从原有令牌桶中获取令牌。如果令牌不足,返回false;
- 每次添加token后,都更新规则对应的时间计数器,用于后续时间窗口判断;
// ParamFlowChecker.java
// 阈值类型QPS 流控效果默认
static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
Object value) {
// 规则对应 ParameterMetric
ParameterMetric metric = getParameterMetric(resourceWrapper);
// 规则对应 令牌桶
CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
// 规则对应 上次添加令牌时间
CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);
if (tokenCounters == null || timeCounters == null) {
return true;
}
// 将QPS阈值转换为令牌概念 令牌数量 = QPS
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
long tokenCount = (long)rule.getCount();
if (exclusionItems.contains(value)) {
tokenCount = rule.getParsedHotItems().get(value);
}
if (tokenCount == 0) {
return false;
}
// 获取token的上限数量 = 阈值 + rule.burstCount(0)
long maxCount = tokenCount + rule.getBurstCount();
if (acquireCount > maxCount) {
return false;
}
while (true) {
long currentTime = TimeUtil.currentTimeMillis();
// case1 : 从来没有获取过token
// 尝试更新上次添加token的时间
AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
if (lastAddTokenTime == null) {
// 添加token = 上限 - 本次需要获取数量
tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
return true;
}
long passTime = currentTime - lastAddTokenTime.get();
if (passTime > rule.getDurationInSec() * 1000) {
// case2 : 当规则配置时间窗口过去后,计算令牌桶token数量
AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
if (oldQps == null) {
lastAddTokenTime.set(currentTime);
return true;
} else {
// 剩余token
long restQps = oldQps.get();
// 新增token = 阈值qps * 过去x秒没添加过token / 时间窗口大小
long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
// 确保新增token不超过令牌桶容量
long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
: (restQps + toAddCount - acquireCount);
// 如果增加之后 token数量仍然小于0,则不通过
if (newQps < 0) {
return false;
}
// cas修改令牌桶
if (oldQps.compareAndSet(restQps, newQps)) {
lastAddTokenTime.set(currentTime);
return true;
}
Thread.yield();
}
} else {
// case3 : 仍然处于当前时间窗口,令牌数量 -= 资源请求数量
AtomicLong oldQps = tokenCounters.get(value);
if (oldQps != null) {
long oldQpsValue = oldQps.get();
if (oldQpsValue - acquireCount >= 0) {
if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
return true;
}
} else {
return false;
}
}
Thread.yield();
}
}
}
阈值类型=QPS,流控效果=排队等待
Sentinel控制台上没有配置排队等待流控效果的热点规则入口,所以这类配置只能通过编码方式实现。
当流控效果为CONTROL_BEHAVIOR_RATE_LIMITER排队等待时,使用漏桶算法,控制请求速率。实现逻辑类似普通流控规则的RateLimiterController。不同点在于RateLimiterController没有时间窗口配置项,以QPS代表的秒作为窗口,而热点规则这里有时间窗口概念。此外,RateLimiterController没有使用cas,而是尝试addAndGet,如果add之后超出阈值,回滚,并返回失败;而热点规则这里是通过cas+循环的方式更新上次访问时间戳。
// ParamFlowChecker.java
// 阈值类型=QPS,流控效果=排队等待
static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
Object value) {
ParameterMetric metric = getParameterMetric(resourceWrapper);
CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
if (timeRecorderMap == null) {
return true;
}
// 1. 根据入参是否在例外项,选择配置阈值tokenCount
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
long tokenCount = (long)rule.getCount();
if (exclusionItems.contains(value)) {
tokenCount = rule.getParsedHotItems().get(value);
}
if (tokenCount == 0) {
return false;
}
// 2. 计算当前时间窗口预计耗时 = 1 / qps * 时间窗口大小 = 单次请求RT * 时间窗口大小
long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
while (true) {
long currentTime = TimeUtil.currentTimeMillis();
AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
if (timeRecorder == null) {
return true;
}
long lastPassTime = timeRecorder.get();
// 期望本次窗口结束时间 = 上次通过时间 + 当前时间窗口预计耗时
long expectedTime = lastPassTime + costTime;
// 如果期望时间小于当前时间 或 期望时间 - 当前时间 小于 排队超时时间,
if (expectedTime <= currentTime || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
// 如果期望时间需要排队,则睡眠
long waitTime = expectedTime - currentTime;
if (waitTime > 0) {
lastPastTimeRef.set(expectedTime);
try {
TimeUnit.MILLISECONDS.sleep(waitTime);
} catch (InterruptedException e) {
RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
}
}
return true;
} else {
Thread.yield();
}
} else {
return false;
}
}
}
从上述逻辑还可以得知,QPS阈值类型的热点规则中,它使用的流量指标都是在ParamFlowSlot中自己统计的,并非在统一的StatisticSlot中通过Node统计的。
阈值类型=线程数
ParamFlowChecker.passSingleValueCheck对于并发线程数的校验很简单:
- 获取ParamterMetric中记录的当前下标参数的并发线程数threadCount;
- 判断并发线程数是否超过阈值。如果下标参数在例外项中,阈值取例外项配置阈值,否则取热点规则级别阈值。
// ParamFlowChecker.java
static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
Object value) {
// case1 : 阈值类型 QPS
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
// ...
}
// case2 : 阈值类型 并发线程数
else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
// 例外项入参集合
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
// 热点参数目前并发线程数
long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
// 如果热点参数在例外项中,取例外项中的阈值做校验,否则取热点规则默认阈值做校验
if (exclusionItems.contains(value)) {
int itemThreshold = rule.getParsedHotItems().get(value);
return ++threadCount <= itemThreshold;
}
long threshold = (long)rule.getCount();
return ++threadCount <= threshold;
}
return true;
}
那么这个并发线程数是在哪里统计的呢?
在StatisticSlot的entry方法中,调用了ProcessorSlotEntryCallback的onPass方法;
在StatisticSlot的exit方法中,调用了ProcessorSlotExitCallback的onExit方法;
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 {
// 1. 先执行后续Slot的entry
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 2. 统计数据ThreadNum++ passRequest++
// ...
// 3. 给用户的扩展点,可以通过SPI加载
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException ex) {
// ...
} catch (BlockException e) {
// 1. 设置BlockError到上下文中
context.getCurEntry().setBlockError(e);
// 2. 统计数据 BlockQps++
// ...
// 3. 给用户的扩展点,可以通过SPI加载
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
} catch (Throwable e) {
context.getCurEntry().setError(e);
throw e;
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
Node node = context.getCurNode();
// 1. 如果没有发生BlockException,统计响应时间RT和错误率exceptionQps
if (context.getCurEntry().getBlockError() == null) {
// ...
}
// 2. 给用户的扩展点,可以通过SPI加载
Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
for (ProcessorSlotExitCallback handler : exitCallbacks) {
handler.onExit(context, resourceWrapper, count, args);
}
// 3. 执行后续Slot的exit方法
fireExit(context, resourceWrapper, count);
}
}
这两个回调函数,都是通过SPI加载的。
首先SPI会加载InitFunc=ParamFlowStatisticSlotCallbackInit,执行init方法时,注册了两个回调函数。
public class ParamFlowStatisticSlotCallbackInit implements InitFunc {
@Override
public void init() {
StatisticSlotCallbackRegistry.addEntryCallback(ParamFlowStatisticEntryCallback.class.getName(),
new ParamFlowStatisticEntryCallback());
StatisticSlotCallbackRegistry.addExitCallback(ParamFlowStatisticExitCallback.class.getName(),
new ParamFlowStatisticExitCallback());
}
}
public class ParamFlowStatisticEntryCallback implements ProcessorSlotEntryCallback<DefaultNode> {
@Override
public void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) {
ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetric(resourceWrapper);
if (parameterMetric != null) {
parameterMetric.addThreadCount(args);
}
}
}
public class ParamFlowStatisticExitCallback implements ProcessorSlotExitCallback {
@Override
public void onExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
if (context.getCurEntry().getBlockError() == null) {
ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetric(resourceWrapper);
if (parameterMetric != null) {
parameterMetric.decreaseThreadCount(args);
}
}
}
}
看一下ParameterMetric的addThreadCount方法实现。ParameterMetric的addThreadCount方法,遍历所有args参数列表,对于每个参数都进行threadCount++。
但是这里发现没有判断入参是否为ParamFlowArgument类型,也就是说如果入参为自定义Object类型,无法支持阈值类型为线程数的热点参数规则。这个特性我已经提交了PR。
// ParameterMetric.java
public void addThreadCount(Object... args) {
if (args == null) {
return;
}
try {
// 循环所有参数
for (int index = 0; index < args.length; index++) {
CacheMap<Object, AtomicInteger> threadCount = threadCountMap.get(index);
if (threadCount == null) {
continue;
}
Object arg = args[index];
if (arg == null) {
continue;
}
// 1-集合
if (Collection.class.isAssignableFrom(arg.getClass())) {
for (Object value : ((Collection)arg)) {
// ... 处理同普通类型
}
}
// 2-数组
else if (arg.getClass().isArray()) {
int length = Array.getLength(arg);
for (int i = 0; i < length; i++) {
// ... 处理同普通类型
}
} else {
AtomicInteger oldValue = threadCount.putIfAbsent(arg, new AtomicInteger());
if (oldValue != null) {
oldValue.incrementAndGet();
} else {
threadCount.put(arg, new AtomicInteger(1));
}
}
}
} catch (Throwable e) {
RecordLog.warn("[ParameterMetric] Param exception", e);
}
}
2、DegradeSlot
DegradeSlot负责处理降级规则校验。通过设置不同熔断策略,实现不同的熔断逻辑。Sentinel的降级和熔断是一个概念,而在Hystrix中熔断只是代表断路器打开,降级代表fallback方法。
编码方式配置降级规则案例:
// 慢调用比率
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule(KEY)
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
// Max allowed response time
.setCount(50)
// Retry timeout (in second)
.setTimeWindow(10)
// Circuit breaker opens when slow request ratio > 60%
.setSlowRatioThreshold(0.6)
.setMinRequestAmount(100)
.setStatIntervalMs(20000);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
// 异常率
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule(KEY)
.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
// Set ratio threshold to 50%.
.setCount(0.5d)
.setStatIntervalMs(30000)
.setMinRequestAmount(50)
// Retry timeout (in second)
.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
System.out.println("Degrade rule loaded: " + rules);
}
DegradeRule成员变量如下:
public class DegradeRule extends AbstractRule {
// 熔断策略 0-慢调用比率 1-异常率 2-异常数
private int grade = RuleConstant.DEGRADE_GRADE_RT;
// 阈值
// 当grade = 慢调用比率 时,阈值=最大rt
// 当grade = 异常率 时,阈值=异常率
// 当grade = 异常数 时,阈值=异常数
private double count;
// 熔断时长(秒):断路器 从 开放 变为 半开 的时长
private int timeWindow;
// 最少请求数 ---> 断路器起效 默认5
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
// 使用grade=慢调用比率时,代表慢调用比率的阈值
private double slowRatioThreshold = 1.0d;
// 统计时长(毫秒)--- 时间窗口大小
private int statIntervalMs = 1000;
}
CircuitBreaker
在DegradeRuleManager中,所有的降级规则,都会转换为CircuitBreaker断路器。
根据规则的grade不同,分为两种断路器:
- 如果grade=0,慢调用比率,则创建ResponseTimeCircuitBreaker;
- 如果grade=1或2,异常率或异常数,则创建ExceptionCircuitBreaker;
public final class DegradeRuleManager {
// 资源名称 - 断路器集合
private static volatile Map<String, List<CircuitBreaker>> circuitBreakers = new HashMap<>();
// 资源名称 - 降级规则集合
private static volatile Map<String, Set<DegradeRule>> ruleMap = new HashMap<>();
private static CircuitBreaker getExistingSameCbOrNew(DegradeRule rule) {
List<CircuitBreaker> cbs = getCircuitBreakers(rule.getResource());
if (cbs == null || cbs.isEmpty()) {
return newCircuitBreakerFrom(rule);
}
for (CircuitBreaker cb : cbs) {
if (rule.equals(cb.getRule())) {
return cb;
}
}
return newCircuitBreakerFrom(rule);
}
// 根据降级规则创建断路器
private static CircuitBreaker newCircuitBreakerFrom(DegradeRule rule) {
switch (rule.getGrade()) {
case RuleConstant.DEGRADE_GRADE_RT:
return new ResponseTimeCircuitBreaker(rule);
case RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO:
case RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT:
return new ExceptionCircuitBreaker(rule);
default:
return null;
}
}
}
CircuitBreaker接口,定义了Sentinel断路器需要具备的能力,定义了断路器三种状态:完全开放、半开、关闭。
public interface CircuitBreaker {
// 对应降级规则
DegradeRule getRule();
// 是否允许请求通过
boolean tryPass(Context context);
// 断路器状态
State currentState();
// 请求完成后的钩子方法
void onRequestComplete(Context context);
// 断路器状态
enum State {
OPEN,
HALF_OPEN,
CLOSED
}
}
在DegradeSlot中,entry方法,调用每个断路器的tryPass方法,判断是否可以通过;exit方法,当没有发生BlockException的情况下,会调用断路器的onRequestComplete方法,统计数据并变更断路器状态。
@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;
}
// 触发所有断路器的onRequestComplete方法
if (curEntry.getBlockError() == null) {
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
}
fireExit(context, r, count, args);
}
}
AbstractCircuitBreaker是断路器的抽象实现,实现了大部分断路器逻辑,包括CAS状态变更、tryPass等。
public abstract class AbstractCircuitBreaker implements CircuitBreaker {
// 降级规则
protected final DegradeRule rule;
// 恢复超时时间(ms)--- rule.timeWindow --- 熔断时间
protected final int recoveryTimeoutMs;
// 观察者注册中心,当断路器状态变更,会通知所有观察者 --- 目前只留给用户扩展
private final EventObserverRegistry observerRegistry;
// 断路器状态
protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED);
// 半开状态下,下次重试时间戳
protected volatile long nextRetryTimestamp;
AbstractCircuitBreaker(DegradeRule rule, EventObserverRegistry observerRegistry) {
this.observerRegistry = observerRegistry;
this.rule = rule;
this.recoveryTimeoutMs = rule.getTimeWindow() * 1000;
}
}
先来看一下tryPass方法的实现。
case1:如果断路器关闭,直接放行;
case2:如果断路器开放,需要判断当前时间是否已经超过熔断时间窗口nextRetryTimestamp,如果超过,尝试执行fromOpenToHalfOpen方法,将断路器变为半开状态;
case3:如果断路器半开,拒绝,半开状态,只允许一个请求通过(进入case2且fromOpenToHalfOpen执行成功的请求),其余请求通通拒绝;
// AbstractCircuitBreaker.java
// 半开状态下,下次重试时间戳
protected volatile long nextRetryTimestamp;
// 断路器状态
protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED);
@Override
public boolean tryPass(Context context) {
if (currentState.get() == State.CLOSED) {
return true;
}
if (currentState.get() == State.OPEN) {
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
return false;
}
protected boolean retryTimeoutArrived() {
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
重点关注fromOpenToHalfOpen方法,断路器从开放到半开的逻辑。
首先CAS将断路器状态从开,变为半开,如果CAS失败,返回false拒绝请求。这里保证半开状态断路器只允许放行一个探测请求。
然后给当前Entry注册一个回调钩子,当Entry退出时会被调用,如果后续规则校验抛出了BlockException,也将当前断路器重新打开。
这里注册回调方法是为了修复ISSUE#1638的BUG,由于当前降级规则之后的规则校验失败,抛出BlockException,导致当前降级规则对应断路器始终处于半开状态(DegradeSlot的exit方法,判断如果发生BlockException,不会调用CircuitBreaker的onRequestComplete钩子方法更新断路器状态)。
// AbstractCircuitBreaker.java
protected boolean fromOpenToHalfOpen(Context context) {
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
notifyObservers(State.OPEN, State.HALF_OPEN, null);
Entry entry = context.getCurEntry();
// 注册一个回调方法,修复#1638
entry.whenTerminate(new BiConsumer<Context, Entry>() {
@Override
public void accept(Context context, Entry entry) {
if (entry.getBlockError() != null) {
currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
}
}
});
return true;
}
return false;
}
此外AbstractCircuitBreaker为子类提供了状态变更的实现方法。
从半开到开,需要更新下次重试时间 = 当前时间 + 熔断时长:
// AbstractCircuitBreaker.java
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 void updateNextRetryTimestamp() {
this.nextRetryTimestamp = TimeUtil.currentTimeMillis() + recoveryTimeoutMs;
}
从关到开,和从半开到开逻辑一致:
// AbstractCircuitBreaker.java
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;
}
从半开到关,调用子类实现的resetStat方法,用于清除子类保存的统计数据:
// AbstractCircuitBreaker.java
protected boolean fromHalfOpenToClose() {
if (currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) {
// 子类实现 清除统计数据
resetStat();
notifyObservers(State.HALF_OPEN, State.CLOSED, null);
return true;
}
return false;
}
abstract void resetStat();
根据熔断策略不同,不同断路器实现类的onRequestComplete方法逻辑不同,分为异常断路器ExceptionCircuitBreaker和慢调用断路器ResponseTimeCircuitBreaker。
异常率和异常数
控制台-降级规则-异常率:
控制台-降级规则-异常数:
如果使用异常率和异常数的降级规则,在使用上与其他规则有些不同,需要手动记录发生的业务异常,否则统计不到错误请求。
Entry entry = null;
try {
entry = SphU.entry(KEY);
//...
} catch (BlockException e) {
// ...
} catch (Throwable t) {
// 手动记录业务异常到当前Entry
Tracer.traceEntry(t, entry);
} finally {
if (entry != null) {
// 这里才能统计到异常次数
entry.exit();
}
}
ExceptionCircuitBreaker实现了基于异常率和异常数的断路器。使用Sentinel自己的LeapArray来统计每个时间窗口的数据,每个窗口大小,取决于配置的统计时长statInterval。
public class ExceptionCircuitBreaker extends AbstractCircuitBreaker {
// 熔断策略 1-异常率 2-异常数
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;
}
}
SimpleErrorCounterLeapArray继承LeapArray只是负责时间窗口处理。
注意传入父类LeapArray构造方法的两个参数,第一个sampleCount决定了存储几个时间窗口,ExceptionCircuitBreaker传入了1,表示只存储一个窗口;第二个intervalInMs取自降级规则的统计时长决定每个窗口的大小。
// 窗口统计
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) {
w.resetTo(startTime);
w.value().reset();
return w;
}
}
实际统计数据靠LeapArray泛型中的对象,如这里错误率和错误数统计,使用SimpleErrorCounter。
static class SimpleErrorCounter {
// 错误数
private LongAdder errorCount;
// 总数
private LongAdder totalCount;
public SimpleErrorCounter() {
this.errorCount = new LongAdder();
this.totalCount = new LongAdder();
}
public SimpleErrorCounter reset() {
errorCount.reset();
totalCount.reset();
return this;
}
}
ExceptionCircuitBreaker一共实现了两个方法,第一个是resetStat方法。
当断路器变为关闭,会调用子类resetStat方法,清除当前窗口统计数据,错误数和总数清零。
// ExceptionCircuitBreaker.java
// 窗口数据统计
private final LeapArray<SimpleErrorCounter> stat;
protected void resetStat() {
stat.currentWindow().value().reset();
}
第二个方法是onRequestComplete方法,只有Entry正常通过或发生非BlockException时(见DegradeSlot.exit),才会进入这个方法。
// ExceptionCircuitBreaker.java
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);
}
根据当前断路器的状态和错误统计情况,ExceptionCircuitBreaker选择是否要改变断路器状态:
- 如果断路器开启,子类什么都不做,只能由父类tryPass方法负责从OPEN变为HALF_OPEN(根据熔断时间);
- 如果断路器半开,判断当前调用是否发生异常。如果发生异常,重新开启断路器,并根据熔断时间更新下次重试时间;如果没发生异常,关闭断路器,清空统计数据;
- 如果断路器关闭,首先判断1个时间窗口内的请求总数,是否超过降级规则的最小请求数,如果小于这个数,不做处理(即使到达错误阈值)。如果超过最小请求数量,校验错误数或错误率是否超过阈值,如果超过阈值,开启断路器,并根据熔断时间更新下次重试时间。
// ExceptionCircuitBreaker.java
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
// 1. 如果当前状态是OPEN,不变,由父类tryPass方法负责从OPEN变为HALF_OPEN
if (currentState.get() == State.OPEN) {
return;
}
// 2. 如果当前状态是HALF_OPEN,判断本次请求是否发生异常,如果发生异常,重新变为OPEN,否则变为CLOSE
if (currentState.get() == State.HALF_OPEN) {
if (error == null) {
fromHalfOpenToClose();
} else {
fromHalfOpenToOpen(1.0d);
}
return;
}
// 3. 当前是CLOSE状态,根据统计数据,判断是否需要OPEN
List<SimpleErrorCounter> counters = stat.values();
long errCount = 0;
long totalCount = 0;
// 其实这边只会有一个窗口,因为构造SimpleErrorCounterLeapArray时窗口数量是1
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) {
curCount = errCount * 1.0d / totalCount;
}
// 如果错误数或错误率超过阈值,调用父类方法OPEN断路器,并更新下次重试时间
if (curCount > threshold) {
transformToOpen(curCount);
}
}
其实Sentinel的DegradeSlot在根据异常率熔断时,和Hystrix的逻辑是差不多的。
类比Hystrix的几个配置:
| Sentinel配置项 | Hystrix配置项 | 释义 |
|---|---|---|
| DegradeRule.statIntervalMs | metrics.rollingStats.timeInMilliseconds | 数据统计时间窗口大小 |
| DegradeRule.minRequestAmount | circuitBreaker.requestVolumeThreshold | 满足一个时间窗口内,请求数到达多少,才判断熔断阈值 |
| DegradeRule.count | circuitBreaker.errorThresholdPercentage | 异常率阈值 |
| DegradeRule.timeWindow | circuitBreaker.sleepWindowInMilliseconds | 熔断时长,断路器从OPEN到HALF_OPEN的时长 |
慢调用比率
ResponseTimeCircuitBreaker实现了基于慢调用比率控制的断路器。
public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker {
// 最大允许响应时间 = DegradeRule.count
private final long maxAllowedRt;
// 最大慢请求比率 = DegradeRule.slowRatioThreshold
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;
}
}
与错误统计类似,这里用LeapArray+SlowRequestCounter记录慢调用数据。
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;
}
}
static class SlowRequestCounter {
// 慢调用数量
private LongAdder slowCount;
// 总数量
private LongAdder totalCount;
public SlowRequestCounter reset() {
slowCount.reset();
totalCount.reset();
return this;
}
}
当断路器关闭,重置当前窗口统计数据。
// ResponseTimeCircuitBreaker.java
// 窗口数据统计
private final LeapArray<SlowRequestCounter> slidingCounter;
public void resetStat() {
slidingCounter.currentWindow().value().reset();
}
重点关注onRequestComplete方法。
首先统计数据,如果本次请求响应时间超过最大允许响应时间,慢调用次数+1。
// ResponseTimeCircuitBreaker.java
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);
}
handleStateChangeWhenThresholdExceeded根据断路器状态和慢调用统计数据,决定断路器状态是否变更,逻辑与错误率类似:
- 如果断路器开启,子类什么都不做,只能由父类tryPass方法负责从OPEN变为HALF_OPEN(根据熔断时间);
- 如果断路器半开,判断当前调用是否超出RT阈值。如果超出,重新开启断路器,并根据熔断时间更新下次重试时间;如果没超出,关闭断路器,清空统计数据;
- 如果断路器关闭,首先判断1个时间窗口内的请求总数,是否超过降级规则的最小请求数,如果小于这个数,不做处理(即使到达慢调用比率阈值)。如果超过最小请求数量,校验慢调用比率是否超过阈值,如果超过阈值,开启断路器,并根据熔断时间更新下次重试时间。
// ResponseTimeCircuitBreaker.java
private void handleStateChangeWhenThresholdExceeded(long rt) {
// 1. 如果OPEN,不处理
if (currentState.get() == State.OPEN) {
return;
}
// 2. 如果HALF_OPEN,根据本次请求,调用速度有没有恢复到正常水平,决定 OPEN or CLOSE
if (currentState.get() == State.HALF_OPEN) {
if (rt > maxAllowedRt) {
fromHalfOpenToOpen(1.0d);
} else {
fromHalfOpenToClose();
}
return;
}
// 3. 如果CLOSE,判断慢调用比率是否超过阈值,如果超过阈值,OPEN
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);
}
// 特例,当前慢调用比率=配置慢调用比率=1
if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
transformToOpen(currentRatio);
}
}
总结
本章学习了两个规则校验:
-
ParamFlowSlot:热点参数规则校验。在FlowSlot流控规则校验的基础上,增加了参数例外项。资源除了基础的阈值限制以外,可以控制不同入参的阈值限制。
从编码角度,热点参数规则支持QPS和线程数两种阈值类型。
阈值类型=QPS:支持默认流控效果和排队等待流控效果。默认流控效果,使用令牌桶算法,将QPS转换为令牌;排队等待流控效果,使用漏桶算法,将QPS转换为RT;
阈值类型=并发线程数:在StatisticSlot中,通过热点规则注入的回调函数,统计每个参数的并发线程数。在ParamFlowSlot中利用这些数据做规则校验;
-
DegradeSlot:降级规则校验。
每个降级规则对应一个断路器CircuitBreaker。断路器有三种状态:
1)开:统计数据超过规则阈值,禁止请求通过;
2)半开:断路器保持开启状态超过熔断时间,仅允许一个请求通过。如果这个请求正常通过(取决于熔断策略,错误策略需要该请求无异常,慢调用比率策略需要该请求RT小于最大RT阈值),断路器关闭,否则恢复开放;
3)关闭:初始状态,允许请求通过。
熔断策略有三种:
1)异常率:与Hystrix完全类似,统计一个时间窗口内的错误率。在断路器关闭时,在满足最小请求数量的基础上,如果异常率超过阈值,开启断路器;在断路器半开时,如果本次请求没有发生异常,则关闭断路器,反之恢复开启;在断路器开启时,会判断当前时间是否已经超过了熔断时间窗口,如果超过了,变为半开;
2)异常数:与异常率类似,只不过阈值是异常数;
3)慢调用比率:慢调用比率=超出最大RT的请求次数 / 总请求次数。与异常率逻辑类似,只不过阈值是慢调用比率,从半开变为关闭的条件是请求RT小于最大RT阈值;