上篇文章講了流控規(guī)則,而除了流控規(guī)則之后還有降級、熱點、系統(tǒng)、授權(quán)等規(guī)則,這篇文件主要講降級規(guī)則。
降級規(guī)則主要處理節(jié)點是DegradeSlot,其中具體邏輯由DegradeRuleManager.checkDegrade實現(xiàn)
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
if (degradeRules == null) {
return;
}
List<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules == null) {
return;
}
for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}
獲取所有的降級規(guī)則,進行一個個的校驗,校驗邏輯是由DegradeRule實現(xiàn),這里和流控規(guī)則FlowRule類似,先看下內(nèi)部屬性
public class DegradeRule extends AbstractRule {
//
private static final int RT_MAX_EXCEED_N = 5;
private double count;
private int timeWindow;
private int grade = RuleConstant.DEGRADE_GRADE_RT;
private volatile boolean cut = false;
private AtomicLong passCount = new AtomicLong(0);
- RT_MAX_EXCEED_N:在降級策略為RT的情況下,如果連續(xù)RT_MAX_EXCEED_N個請求都大于配置的值,那么會在窗口時間內(nèi)會進行降級狀態(tài),所有流量都會返回false(拋出 DegradeException);在降級策略為異常比例的情況下,總qps且異常數(shù)大于該值才會進行異常比例的判斷
- count:降級策略為RT則表示響應時間;降級策略為異常比例則表示異常比例;降級策略為異常數(shù)則表示異常數(shù)量
- timeWindow:降級的時間窗口,在該窗口時間內(nèi)請求都不能通過
- grade:降級熔斷策略
- cut:是否被降級熔斷,如果true,則請求過來直接拒絕
- passCount:降級策略為RT的時候用來統(tǒng)計超過配置值的數(shù)量
接下來看下DegradeRule的處理
@Override
public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
//是否降級
if (cut) {
return false;
}
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
if (clusterNode == null) {
return true;
}
//省略降級策略的處理....
// 到達這里表示觸發(fā)了降級規(guī)則,需要降級熔斷
// 這里用鎖是防止多線程更新cut,導致重復創(chuàng)建了ResetTask
synchronized (lock) {
if (!cut) {// 如果沒有降級熔斷,則需要設置為true
// Automatically degrade.
cut = true;
// 創(chuàng)建一個延時任務,在時間窗口過后將cut設為false,將passCount設為0
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}
// 返回false表示當前操作失敗
return false;
}
}
接下來看下具體策略的處理
降級策略:RT
double rt = clusterNode.avgRt();
//從node中獲取平均rt
if (rt < this.count) {// 如果小于配置的值,則可以直接返回成功
// 將passCount重置
passCount.set(0);
return true;
}
// 到達這里表示當前請求rt已經(jīng)超過閾值,是否返回失敗需要判斷passCount是否大于等于RT_MAX_EXCEED_N
// 遞增passCount的值,然后判斷是否大于RT_MAX_EXCEED_N
// 如果小于RT_MAX_EXCEED_N那么還是返回成功
// 直到連續(xù)超過閾值RT_MAX_EXCEED_N次才返回失敗
if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) {
return true;
}
這種情況下需要注意一種情況:
假設接口平均rt很小,但是某一次請求時間大幅度的上升,這樣會導致整個接口的rt大幅度上升,這樣會導致異常降級,例如某個接口平均rt為1ms,配置的閾值為10ms,例如某一次請求rt達到了1s,導致整個接口的平均rt到了100ms,那么就會導致錯誤降級熔斷
請求量小的接口可能會出現(xiàn)上述情況,如qps只有10,某一次接口達到了1s會導致整個接口平均rt上升到100ms左右
降級策略:失敗比例
// 異常qps
double exception = clusterNode.exceptionQps();
// 成功qps
double success = clusterNode.successQps();
// 總qps=passQps+blockQps
long total = clusterNode.totalQps();
// 總qps小于RT_MAX_EXCEED_N則無視
if (total < RT_MAX_EXCEED_N) {
return true;
}
double realSuccess = success - exception;
// 失敗數(shù)小于RT_MAX_EXCEED_N且成功數(shù)小于0的情況則無視
if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) {
return true;
}
// 異常比例判斷
if (exception / success < count) {
return true;
}
注意:
-
clusterNode.successQps()返回的是成功執(zhí)行完了Slot鏈且沒有被規(guī)則攔截的數(shù)量 -
clusterNode.exceptionQps()返回的是基于1的基礎且業(yè)務處理中出現(xiàn)異常的數(shù)量,該需要需要用Tracer.trace(t)捕獲,才會計入統(tǒng)計 - 由12可知,
clusterNode.successQps()包含了clusterNode.exceptionQps(),所以realSuccess需要減去重合的部分才是真正成功的數(shù)量
降級策略:異常數(shù)
double exception = clusterNode.totalException();
if (exception < count) {
return true;
}
異常數(shù)這個規(guī)則比較簡單,就是判斷一分鐘內(nèi)的異常數(shù)是否大于閾值。這里還有個注意點:在時間窗口小于60s的時候,會導致降級熔斷時間窗口過后,還是會被降級熔斷,是因為這里是判斷的一分鐘的異常數(shù),時間窗口太小會導致恢復熔斷后,異常數(shù)還是大于等于閾值。
測試代碼如下(在官方提供的ExceptionCountDegradeDemo基礎上修改),先配置一個規(guī)則
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<DegradeRule>();
DegradeRule rule = new DegradeRule();
rule.setResource(KEY);
// set limit exception count to 4
rule.setCount(4);
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
/**
* When degrading by {@link RuleConstant#DEGRADE_GRADE_EXCEPTION_COUNT}, time window
* less than 60 seconds will not work as expected. Because the exception count is
* summed by minute, when a short time window elapsed, the degradation condition
* may still be satisfied.
*/
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
配置一個降級規(guī)則,策略是異常數(shù),數(shù)量為4,熔斷時間窗口是10s(代碼中的注釋是官方提交的,從這里也看出降級熔斷窗口太小是會有問題的)
運行代碼如下:
private static final String KEY = "abc";
private static AtomicInteger total = new AtomicInteger();
private static AtomicInteger pass = new AtomicInteger();
private static AtomicInteger block = new AtomicInteger();
private static AtomicInteger bizException = new AtomicInteger();
private static volatile boolean stop = false;
private static final int threadCount = 1;
private static int seconds = 60 + 40;
public static void main(String[] args) throws Exception {
initDegradeRule();
// 運行10次,每次都拋出異常
for (int i = 0; i < 10; i++) {
Entry entry = null;
try {
entry = SphU.entry(KEY);
pass.addAndGet(1);
throw new RuntimeException("throw runtime ");
} catch (BlockException e) {
block.addAndGet(1);
} catch (Throwable t) {
bizException.incrementAndGet();
Tracer.trace(t);
} finally {
total.addAndGet(1);
if (entry != null) {
entry.exit();
}
}
}
System.out.println("total:" + total.get() + ", pass:" + pass.get()
+ ", block:" + block.get() + ", bizException:" + bizException.get());
// 上面運行后,會被降級熔斷,窗口時間為10s,這里睡眠11s,等待窗口時間過去
Thread.sleep(11000);
// 繼續(xù)執(zhí)行
Entry entry = null;
try {
entry = SphU.entry(KEY);
pass.addAndGet(1);
} catch (BlockException e) {
block.addAndGet(1);
} catch (Throwable t) {
bizException.incrementAndGet();
Tracer.trace(t);
} finally {
total.addAndGet(1);
if (entry != null) {
entry.exit();
}
}
System.out.println("total:" + total.get() + ", pass:" + pass.get()
+ ", block:" + block.get() + ", bizException:" + bizException.get());
}
上面代碼中,第一次for循環(huán)執(zhí)行10次邏輯,每次都拋出異常,并且用Tracer.trace記錄我們的業(yè)務異常,由于配置的異常數(shù)為4,所以執(zhí)行第四次結(jié)果過后,就已經(jīng)被降級熔斷了,打印的結(jié)果如下:
total:10, pass:4, block:6, bizException:4
可以看到后面6次被block了,即被降級規(guī)則降級熔斷了,此時sleep11s,這個時候窗口時間已經(jīng)過了,但是執(zhí)行后續(xù)代碼發(fā)現(xiàn)輸出如下:
total:11, pass:4, block:7, bizException:4
即這次請求也被block了,因為恢復之后異常數(shù)還是4,仍然不符合exception < count的判斷,這時如果將sleep的時間設置成60s,輸出如下
total:11, pass:5, block:6, bizException:4
這時候,就正常了,因為統(tǒng)計的時間窗口已經(jīng)往后移動了,統(tǒng)計的原理需要了解一下sentinel的滑動時間窗口的原理