簡介
Sentinel 是什麼?
隨着微服務的流行,服務和服務之間的穩定性變得越來越重要。Sentinel 以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
官方地址:https://github.com/alibaba/Sentinel/
Sentinel 具有以下特徵:
- 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、消息削峯填谷、集羣流量控制、實時熔斷下游不可用應用等。
- 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級數據,甚至 500 臺以下規模的集羣的彙總運行情況。
- 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
- 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定製邏輯。例如定製規則管理、適配動態數據源等。
Sentinel 的主要特性:
Sentinel 的開源生態:
Sentinel 分爲兩個部分:
- 核心庫(Java 客戶端)不依賴任何框架/庫,能夠運行於所有 Java 運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
- 控制檯(Dashboard)基於 Spring Boot 開發,打包後可以直接運行,不需要額外的 Tomcat 等應用容器。
服務接入
服務端啓動:
將 Sentinel 源碼下載下來導入 IDEA 可以看到如下工程結構,啓動 DashboardApplication 就可以看到 Sentinel 管理頁面
登錄 Sentinel 並進入管理頁面默認用戶名密碼(sentinel/amin)
主頁面:
客戶端接入:
目前 sentinel 官方提供了一些常用框架接入的 demo :https://github.com/alibaba/Sentinel/tree/master/sentinel-demo
引入相關jar:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${版本號}</version>
</dependency>
Spring boot 接入爲例:
設置 sentinel 提供的 CommonFilter 來攔截所有的訪問
@Configuration
public class webConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
客戶端啓動參數:
-Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=test-server
啓動之後在 Sentinel 的 儀表板上看到客戶端上報的一些信息
工作原理
Slot 插槽
在 Sentinel 裏面,所有的資源都對應一個資源名稱(resourceName
),每次資源調用都會創建一個 Entry
對象。Entry 可以通過對主流框架的適配自動創建,也可以通過註解的方式或調用 SphU
API 顯式創建。Entry 創建的時候,同時也會創建一系列功能插槽(slot chain),這些插槽有不同的職責,例如:
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;ClusterBuilderSlot
則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作爲多維度限流,降級的依據;StatisticSlot
則用於記錄、統計不同緯度的 runtime 指標監控信息;FlowSlot
則用於根據預設的限流規則以及前面 slot 統計的狀態,來進行流量控制;AuthoritySlot
則根據配置的黑白名單和調用來源信息,來做黑白名單控制;DegradeSlot
則通過統計信息以及預設的規則,來做熔斷降級;SystemSlot
則通過系統的狀態,例如 load1 等,來控制總的入口流量;
Sentinel 提供了插槽接口 ProcessorSlot,其中提供了方法 enrty 處理進入請求 和 exit 處理請求結束操作
public interface ProcessorSlot<T> {
/**
* Entrance of this slot.
*
* @param context current {@link Context}
* @param resourceWrapper current resource
* @param param generics parameter, usually is a {@link com.alibaba.csp.sentinel.node.Node}
* @param count tokens needed
* @param prioritized whether the entry is prioritized
* @param args parameters of the original call
* @throws Throwable blocked exception or unexpected error
*/
void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized,
Object... args) throws Throwable;
/**
* Means finish of {@link #entry(Context, ResourceWrapper, Object, int, boolean, Object...)}.
*
* @param context current {@link Context}
* @param resourceWrapper current resource
* @param obj relevant object (e.g. Node)
* @param count tokens needed
* @param prioritized whether the entry is prioritized
* @param args parameters of the original call
* @throws Throwable blocked exception or unexpected error
*/
void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized,
Object... args) throws Throwable;
/**
* Exit of this slot.
*
* @param context current {@link Context}
* @param resourceWrapper current resource
* @param count tokens needed
* @param args parameters of the original call
*/
void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
/**
* Means finish of {@link #exit(Context, ResourceWrapper, int, Object...)}.
*
* @param context current {@link Context}
* @param resourceWrapper current resource
* @param count tokens needed
* @param args parameters of the original call
*/
void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
}
總體的框架如下:
Sentinel 將 SlotChainBuilder
作爲 SPI 接口進行擴展,使得 Slot Chain 具備了擴展的能力。您可以自行加入自定義的 slot 並編排 slot 間的順序,從而可以給 Sentinel 添加自定義的功能。
RuleManager 規則管理器
每個 Slot 插槽背後都對應着一個 RuleManager 的實現類,簡單理解就是每個 Slot 有一套規則,規則驗證處理由對應的 RuleManager 來進行處理。
流量控制:FlowSolt 對應 FlowRuleManager
降級控制:DegradeSlot 對應 DegradeRuleManager
權限控制:AuthoritySlot 對應 AuthorityRuleManager
系統規則控制: SystemSlot 對應 SystemRuleManager
降級控制實現原理
1、新增資源配置降級規則,目前對於降級策有如下三種:
- RT:平均響應時間 (DEGRADE_GRADE_RT):當 1s 內持續進入 5 個請求,對應時刻的平均響應時間(秒級)均超過閾值(count,以 ms 爲單位),那麼在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 爲單位)之內,對這個方法的調用都會自動地熔斷(拋出 DegradeException)。注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啓動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。
- 異常比例:當資源的每秒請求量 >= 5,並且每秒異常總數佔通過量的比值超過閾值(DegradeRule 中的 count)之後,資源進入降級狀態,即在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 爲單位)之內,對這個方法的調用都會自動地返回。異常比率的閾值範圍是 [0.0, 1.0],代表 0% - 100%。
- 異常數:當資源近 1 分鐘的異常數目超過閾值之後會進行熔斷。注意由於統計時間窗口是分鐘級別的,若 timeWindow 小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。
限流結果信息
Blocked by Sentinel (flow limiting)
2、實現邏輯
(1)在之前我們已經提及 Sentinel 是通過 slot 鏈來實現的,對於降級功能其提供了 DegradeSlot,實現源碼如下:
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
(2)通過上面代碼我們可以瞭解到,限流規則的實現是在 DegradeRuleManager 的checkDegrade中來處理的,限流可以-配置多個規則,依次按照規則來處理。
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
Set<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);
}
}
}
(3)在 DegradeRule 的 passCheck 方法中我們可以看到可以根據 RT、異常數和異常比例來進行熔斷降級處理。
@Override
public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
if (cut.get()) {
return false;
}
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
if (clusterNode == null) {
return true;
}
// 請求處理時間
if (grade == RuleConstant.DEGRADE_GRADE_RT) {
double rt = clusterNode.avgRt();
if (rt < this.count) {
passCount.set(0);
return true;
}
// Sentinel will degrade the service only if count exceeds.
if (passCount.incrementAndGet() < rtSlowRequestAmount) {
return true;
}
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
//異常比例
double exception = clusterNode.exceptionQps();
double success = clusterNode.successQps();
double total = clusterNode.totalQps();
// If total amount is less than minRequestAmount, the request will pass.
if (total < minRequestAmount) {
return true;
}
// In the same aligned statistic time window,
// "success" (aka. completed count) = exception count + non-exception count (realSuccess)
double realSuccess = success - exception;
if (realSuccess <= 0 && exception < minRequestAmount) {
return true;
}
if (exception / success < count) {
return true;
}
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {
//異常數
double exception = clusterNode.totalException();
if (exception < count) {
return true;
}
}
if (cut.compareAndSet(false, true)) {
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}
return false;
}
流量控制實現原理
接下來我們瞭解學習一下 Sentinel 是如何實現流量控制的
流量控制(flow control),其原理是監控應用流量的 QPS 或併發線程數等指標,當達到指定的閾值時對流量進行控制,以避免被瞬時的流量高峯沖垮,從而保障應用的高可用性。
FlowSlot 會根據預設的規則,結合前面 NodeSelectorSlot、ClusterNodeBuilderSlot、StatisticSlot 統計出來的實時信息進行流量控制。
限流的直接表現是在執行 Entry nodeA = SphU.entry(resourceName) 的時候拋出 FlowException 異常。FlowException 是 BlockException 的子類,您可以捕捉 BlockException 來自定義被限流之後的處理邏輯。
同一個資源可以創建多條限流規則。FlowSlot 會對該資源的所有限流規則依次遍歷,直到有規則觸發限流或者所有規則遍歷完畢。
一條限流規則主要由下面幾個因素組成,我們可以組合這些元素來實現不同的限流效果:
- resource:資源名,即限流規則的作用對象
- count: 限流閾值
- grade: 限流閾值類型(QPS 或併發線程數)
- limitApp: 流控針對的調用來源,若爲 default 則不區分調用來源
- strategy: 調用關係限流策略
- controlBehavior: 流量控制效果(直接拒絕、Warm Up、勻速排隊)
流控-QPS配置
流控-線程數配置
限流結果信息
Blocked by Sentinel (flow limiting)
實現流程
(1)Sentinel 提供了 FlowSlot 用來進行流量控制,流量規則的最終實現在 FlowRuleChecker 的 checkFlow 中實現的。
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);
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<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
return flowRules.get(resource);
}
};
}
(2)在 checkFlow 中會依次獲取我們配置的流控規則,然後依次進行流控判斷處理,如果被流控則拋出異常 FlowException
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);
}
}
}
}
(3)在 canPassCheck 中會判斷是集羣限流還是本地限流
public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
String limitApp = rule.getLimitApp();
if (limitApp == null) {
return true;
}
if (rule.isClusterMode()) {
return passClusterCheck(rule, context, node, acquireCount, prioritized);
}
return passLocalCheck(rule, context, node, acquireCount, prioritized);
}
(4)如果是本地限流則獲取節點信息,然後根據流控規則進行流控判斷
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
(5)當 QPS 超過某個閾值的時候,則採取措施進行流量控制。流量控制的手段包括以下幾種:直接拒絕、Warm Up、勻速排隊。對應 FlowRule
中的 controlBehavior
字段。
直接拒絕(RuleConstant.CONTROL_BEHAVIOR_DEFAULT
)方式是默認的流量控制方式,當QPS超過任意規則的閾值後,新的請求就會被立即拒絕,拒絕方式爲拋出FlowException
。這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的準確水位時。具體的例子參見 FlowQpsDemo。
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即預熱/冷啓動方式。當系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。通過"冷啓動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。詳細文檔可以參考 流量控制 - Warm Up 文檔
目前 Sentinel 對於流量控制提供瞭如下幾種方式:
- 直接拒絕(DefaultController):支持拋出異常
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = avgUsedTokens(node);
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;
}
- 勻速排隊(RateLimiterController):判斷等待時間,如果等待時間過長也是會限流,並且使用 Thread.sleep 如果配置不正確可能會導致線程過多。
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// Pass when acquire count is less or equal than 0.
if (acquireCount <= 0) {
return true;
}
// Reject when count is less or equal than 0.
// Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// Calculate the interval between every two requests.
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// Expected pass time of this request.
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
// Contention may exist here, but it's okay.
latestPassedTime.set(currentTime);
return true;
} else {
// Calculate the time to wait.
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
- Warm Up(WarmUpController 和 WarmUpRateLimiterController):預熱啓動
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);
// 開始計算它的斜率
// 如果進入了警戒線,開始調整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 消耗的速度要比warning快,但是要比慢
// current interval = restToken*slope+1/count
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
總結:其他的限流規則我們就不一一去查看源碼學習了,通過了解降級和流控這兩個規則的實現原理,我們可以瞭解其他的實現原理都是類似的。當然這些目前是 Sentinel 提供的一些 限流等功能,這對於我們業務中使用響應的限流實現方案有一些借鑑意義,當然限流也可以通過其他方案來實現,可以讀一下博主之前整理的一篇博客《業務學習 -- 高併發系統保護之限流和降級熔斷》