文章很長,建議收藏起來慢慢讀!瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版 為您奉上珍貴的學習資源 :
-
免費贈送 經典圖書:《Java高並發核心編程(卷1)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《Java高並發核心編程(卷2)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《SpringCloud Nginx高並發核心編程》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文
入大廠 、做架構、大力提升Java 內功 必備的精彩博文 | 2021 秋招漲薪1W + 必備的精彩博文 |
---|---|
1:Redis 分布式鎖 (圖解-秒懂-史上最全) | 2:Zookeeper 分布式鎖 (圖解-秒懂-史上最全) |
3: Redis與MySQL雙寫一致性如何保證? (面試必備) | 4: 面試必備:秒殺超賣 解決方案 (史上最全) |
5:面試必備之:Reactor模式 | 6: 10分鍾看懂, Java NIO 底層原理 |
7:TCP/IP(圖解+秒懂+史上最全) | 8:Feign原理 (圖解) |
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) | 10:CDN圖解(秒懂 + 史上最全 + 高薪必備) |
11: 分布式事務( 圖解 + 史上最全 + 吐血推薦 ) | 12:限流:計數器、漏桶、令牌桶 三大算法的原理與實戰(圖解+史上最全) |
13:架構必看:12306搶票系統億級流量架構 (圖解+秒懂+史上最全) |
14:seata AT模式實戰(圖解+秒懂+史上最全) |
15:seata 源碼解讀(圖解+秒懂+史上最全) | 16:seata TCC模式實戰(圖解+秒懂+史上最全) |
Java 面試題 30個專題 , 史上最全 , 面試必刷 | 阿里、京東、美團... 隨意挑、橫着走!!! |
---|---|
1: JVM面試題(史上最強、持續更新、吐血推薦) | 2:Java基礎面試題(史上最全、持續更新、吐血推薦 |
3:架構設計面試題 (史上最全、持續更新、吐血推薦) | 4:設計模式面試題 (史上最全、持續更新、吐血推薦) |
17、分布式事務面試題 (史上最全、持續更新、吐血推薦) | 一致性協議 (史上最全) |
29、多線程面試題(史上最全) | 30、HR面經,過五關斬六將后,小心陰溝翻船! |
9.網絡協議面試題(史上最全、持續更新、吐血推薦) | 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄 】 |
SpringCloud 微服務 精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
SpringCloud gateway (史上最全) | 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄 】 |
sentinel 基本概念
開發的原因,需要對吞吐量(TPS)、QPS、並發數、響應時間(RT)幾個概念做下了解,查自百度百科,記錄如下:
- 響應時間(RT)
響應時間是指系統對請求作出響應的時間。直觀上看,這個指標與人對軟件性能的主觀感受是非常一致的,因為它完整地記錄了整個計算機系統處理請求的時間。由於一個系統通常會提供許多功能,而不同功能的處理邏輯也千差萬別,因而不同功能的響應時間也不盡相同,甚至同一功能在不同輸入數據的情況下響應時間也不相同。所以,在討論一個系統的響應時間時,人們通常是指該系統所有功能的平均時間或者所有功能的最大響應時間。當然,往往也需要對每個或每組功能討論其平均響應時間和最大響應時間。
對於單機的沒有並發操作的應用系統而言,人們普遍認為響應時間是一個合理且准確的性能指標。需要指出的是,響應時間的絕對值並不能直接反映軟件的性能的高低,軟件性能的高低實際上取決於用戶對該響應時間的接受程度。對於一個游戲軟件來說,響應時間小於100毫秒應該是不錯的,響應時間在1秒左右可能屬於勉強可以接受,如果響應時間達到3秒就完全難以接受了。而對於編譯系統來說,完整編譯一個較大規模軟件的源代碼可能需要幾十分鍾甚至更長時間,但這些響應時間對於用戶來說都是可以接受的。- 吞吐量(Throughput)
吞吐量是指系統在單位時間內處理請求的數量。對於無並發的應用系統而言,吞吐量與響應時間成嚴格的反比關系,實際上此時吞吐量就是響應時間的倒數。前面已經說過,對於單用戶的系統,響應時間(或者系統響應時間和應用延遲時間)可以很好地度量系統的性能,但對於並發系統,通常需要用吞吐量作為性能指標。
對於一個多用戶的系統,如果只有一個用戶使用時系統的平均響應時間是t,當有你n個用戶使用時,每個用戶看到的響應時間通常並不是n×t,而往往比n×t小很多(當然,在某些特殊情況下也可能比n×t大,甚至大很多)。這是因為處理每個請求需要用到很多資源,由於每個請求的處理過程中有許多不走難以並發執行,這導致在具體的一個時間點,所占資源往往並不多。也就是說在處理單個請求時,在每個時間點都可能有許多資源被閑置,當處理多個請求時,如果資源配置合理,每個用戶看到的平均響應時間並不隨用戶數的增加而線性增加。實際上,不同系統的平均響應時間隨用戶數增加而增長的速度也不大相同,這也是采用吞吐量來度量並發系統的性能的主要原因。一般而言,吞吐量是一個比較通用的指標,兩個具有不同用戶數和用戶使用模式的系統,如果其最大吞吐量基本一致,則可以判斷兩個系統的處理能力基本一致。- 並發用戶數
並發用戶數是指系統可以同時承載的正常使用系統功能的用戶的數量。與吞吐量相比,並發用戶數是一個更直觀但也更籠統的性能指標。實際上,並發用戶數是一個非常不准確的指標,因為用戶不同的使用模式會導致不同用戶在單位時間發出不同數量的請求。一網站系統為例,假設用戶只有注冊后才能使用,但注冊用戶並不是每時每刻都在使用該網站,因此具體一個時刻只有部分注冊用戶同時在線,在線用戶就在瀏覽網站時會花很多時間閱讀網站上的信息,因而具體一個時刻只有部分在線用戶同時向系統發出請求。這樣,對於網站系統我們會有三個關於用戶數的統計數字:注冊用戶數、在線用戶數和同時發請求用戶數。由於注冊用戶可能長時間不登陸網站,使用注冊用戶數作為性能指標會造成很大的誤差。而在線用戶數和同事發請求用戶數都可以作為性能指標。相比而言,以在線用戶作為性能指標更直觀些,而以同時發請求用戶數作為性能指標更准確些。- QPS每秒查詢率(Query Per Second)
每秒查詢率QPS是對一個特定的查詢服務器在規定時間內所處理流量多少的衡量標准,在因特網上,作為域名系統服務器的機器的性能經常用每秒查詢率來衡量。對應fetches/sec,即每秒的響應請求數,也即是最大吞吐能力。 (看來是類似於TPS,只是應用於特定場景的吞吐量)
1、什么是Sentinel:
Sentinel是阿里開源的項目,提供了流量控制、熔斷降級、系統負載保護等多個維度來保障服務之間的穩定性。
官網:https://github.com/alibaba/Sentinel/wiki
2012年,Sentinel誕生於阿里巴巴,其主要目標是流量控制。2013-2017年,Sentinel迅速發展,並成為阿里巴巴所有微服務的基本組成部分。 它已在6000多個應用程序中使用,涵蓋了幾乎所有核心電子商務場景。2018年,Sentinel演變為一個開源項目。2020年,Sentinel Golang發布。
Sentinel 具有以下特征:
豐富的應用場景 :Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即
突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
完備的實時監控 :Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機
器秒級數據,甚至 500 台以下規模的集群的匯總運行情況。
廣泛的開源生態 :Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring
Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入Sentinel。
完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快
速地定制邏輯。例如定制規則管理、適配動態數據源等。
Sentinel的生態圈
Sentinel主要特性:
關於Sentinel與Hystrix的區別見:https://yq.aliyun.com/articles/633786/
到這已經學習Sentinel的基本的使用,在很多的特性和Hystrix有很多類似的功能。以下是Sentinel和Hystrix的對比。
Sentinel 的使用
Sentinel 的使用可以分為兩個部分:
-
控制台(Dashboard):控制台主要負責管理推送規則、監控、集群限流分配管理、機器發現等。
-
核心庫(Java 客戶端):不依賴任何框架/庫,能夠運行於 Java 7 及以上的版本的運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
在這里我們看下控制台的使用
Sentinel中的管理控制台
2.1 獲取 Sentinel 控制台
您可以從 release 頁面 下載最新版本的控制台 jar 包。
您也可以從最新版本的源碼自行構建 Sentinel 控制台:
- 下載 控制台 工程
- 使用以下命令將代碼打包成一個 fat jar:
mvn clean package
2.2 sentinel服務啟動
java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar
開機啟動:啟動命令可以加入到啟動的 rc.local 配置文件, 之后做到開機啟動
啟動 sentinel
/usr/bin/su - root -c "nohup java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar 2>&1 &"
除了流量控制以外,對調用鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。
由於調用關系的復雜性,如果調用鏈路中的某個資源不穩定,最終會導致請求發生堆積。Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)。
關於熔斷降級的介紹見:Sentinel熔斷降級。
下面就使用基於注解的方式實現Sentinel的熔斷降級的demo。
注意:啟動 Sentinel 控制台需要 JDK 版本為 1.8 及以上版本。
使用如下命令啟動控制台:
nohup java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar &
其中 -Dserver.port=8849用於指定 Sentinel 控制台端口為 8849 , 這個端口可以按需指定。
從 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登錄功能,默認用戶名和密碼都是 sentinel
。可以參考 鑒權模塊文檔 配置用戶名和密碼。
注:若您的應用為 Spring Boot 或 Spring Cloud 應用,您可以通過 Spring 配置文件來指定配置,詳情請參考 Spring Cloud Alibaba Sentinel 文檔。(1)獲取 Sentinel 控制台
您可以從官方 網站中 下載最新版本的控制台 jar 包,下載地址如下:
https://github.com/alibaba/Sentinel/releases/download/1.6.3/sentinel-dashboard-1.7.1.jar
(2)啟動
使用如下命令啟動控制台:
其中 - Dserver.port=8888 用於指定 Sentinel 控制台端口為 8888 。
從 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登錄功能,默認用戶名和密碼都是 sentinel 。可以參考 鑒權模塊文檔 配置用戶名和密碼。
[root@192 ~]# java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.6.3.jar
INFO: log base dir is: /root/logs/csp/
INFO: log name use pid is: false
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.5.RELEASE)
2020-02-08 13:07:29.316 INFO 114031 --- [ main] c.a.c.s.dashboard.DashboardApplication : Starting DashboardApplication on 192.168.180.137 with PID 114031 (/root/sentinel-dashboard-1.6.3.jar started by root in /root)
2020-02-08 13:07:29.319 INFO 114031 --- [ main] c.a.c.s.dashboard.DashboardApplication : No active profile set, falling back to default profiles: default
2020-02-08 13:07:29.456 INFO 114031 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@59690aa4: startup date [Sat Feb 08 13:07:29 CST 2020]; root of context hierarchy
2020-02-08 13:07:33.783 INFO 114031 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8888 (http)
啟動 Sentinel 控制台需要 JDK 版本為 1.8 及以上版本。
查看機器列表以及健康情況
默認情況下Sentinel 會在客戶端首次調用的時候進行初始化,開始向控制台發送心跳包。也可以配置
sentinel.eager=true ,取消Sentinel控制台懶加載。
打開瀏覽器即可展示Sentinel的管理控制台
客戶端能接入控制台
控制台啟動后,客戶端需要按照以下步驟接入到控制台。
父工程引入 alibaba實現的SpringCloud
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子工程中引入 sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
( 2)配置啟動參數
在工程的application.yml中添加Sentinel 控制台配置信息
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.180.137:8888 #sentinel控制台的請求地址
這里的 spring.cloud.sentinel.transport.dashboard 配置控制台的請求路徑。
Sentinel與Hystrix的區別
遷移方案
Sentinel 官方提供了詳細的由Hystrix 遷移到Sentinel 的方法
2 使用 Sentinel 來進行熔斷與限流
Sentinel 可以簡單的分為 Sentinel 核心庫和 Dashboard。核心庫不依賴 Dashboard,但是結合
Dashboard 可以取得最好的效果。
使用 Sentinel 來進行熔斷保護,主要分為幾個步驟:
-
定義資源
資源:可以是任何東西,一個服務,服務里的方法,甚至是一段代碼。
-
定義規則
規則:Sentinel 支持以下幾種規則:流量控制規則、熔斷降級規則、系統保護規則、來源訪問控制規則
和 熱點參數規則。 -
檢驗規則是否生效
Sentinel 的所有規則都可以在內存態中動態地查詢及修改,修改之后立即生效. 先把可能需要保護的資源定義好,之后再配置規則。
也可以理解為,只要有了資源,我們就可以在任何時候靈活地定義各種流量控制規則。在編碼的時候,只需要考慮這個代碼是否需要保護,如果需要保
護,就將之定義為一個資源。
1. 定義資源
資源是 Sentinel 的關鍵概念。它可以是 Java 應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它應用提供的服務,RPC接口方法,甚至可以是一段代碼。
只要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,甚至服務名稱作為資源名來標示資源。
把需要控制流量的代碼用 Sentinel的關鍵代碼 SphU.entry("資源名") 和 entry.exit() 包圍起來即可。
實例代碼:
Entry entry = null;
try {
// 定義一個sentinel保護的資源,名稱為test-sentinel-api
entry = SphU.entry(resourceName);
// 模擬執行被保護的業務邏輯耗時
Thread.sleep(100);
return a;
} catch (BlockException e) {
// 如果被保護的資源被限流或者降級了,就會拋出BlockException
log.warn("資源被限流或降級了", e);
return "資源被限流或降級了";
} catch (InterruptedException e) {
return "發生InterruptedException";
} finally {
if (entry != null) {
entry.exit();
}
ContextUtil.exit();
}
}
在下面的例子中, 用 try-with-resources 來定義資源。參考代碼如下:
public static void main(String[] args) {
// 配置規則.
initFlowRules();
while (true) {
// 1.5.0 版本開始可以直接利用 try-with-resources 特性
try (Entry entry = SphU.entry("HelloWorld")) {
// 被保護的邏輯
System.out.println("hello world");
} catch (BlockException ex) {
// 處理被流控的邏輯
System.out.println("blocked!");
}
}
}
資源注解@SentinelResource
也可以使用Sentinel提供的注解@SentinelResource來定義資源,實例如下:
@SentinelResource("HelloWorld")
public void helloWorld() {
// 資源中的邏輯
System.out.println("hello world");
}
@SentinelResource 注解
注意:注解方式埋點不支持 private 方法。
@SentinelResource
用於定義資源,並提供可選的異常處理和 fallback 配置項。 @SentinelResource
注解包含以下屬性:
- value:資源名稱,必需項(不能為空)
- entryType:entry 類型,可選項(默認為 EntryType.OUT)
- blockHandler / blockHandlerClass:
blockHandler 對應處理 BlockException的函數名稱,可選項。blockHandler 函數訪問范圍需要是 public,返回類型需要與原方法相匹配,參數類型需要和原方法相匹配並且最后加一個額外的參數,類型為 BlockException。blockHandler 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 blockHandlerClass 為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。
-
fallback /fallbackClass
:fallback 函數名稱,可選項,用於在拋出異常的時候提供 fallback 處理邏輯。fallback 函數可以針對所有類型的異常(除了exceptionsToIgnore里面排除掉的異常類型)進行處理。
-
defaultFallback
(since 1.6.0):默認的 fallback 函數名稱,可選項,通常用於通用的 fallback 邏輯(即可以用於很多服務或方法)。默認 fallback 函數可以針對所有類型的異常(除了exceptionsToIgnore里面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,則只有 fallback 會生效。
fallback 函數簽名和位置要求:
-
返回值類型必須與原函數返回值類型一致;
-
方法參數列表需要和原函數一致,或者可以額外多一個
Throwable
類型的參數用於接收對應的異常。 -
fallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。
defaultFallback 函數簽名要求:
- 返回值類型必須與原函數返回值類型一致;
- 方法參數列表需要為空,或者可以額外多一個
Throwable
類型的參數用於接收對應的異常。 - defaultFallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。
- exceptionsToIgnore(since 1.6.0):用於指定哪些異常被排除掉,不會計入異常統計中,也不會進入 fallback 邏輯中,而是會原樣拋出。
2. 定義規則
規則主要有流控規則、 熔斷降級規則、系統規則、權限規則、熱點參數規則等:
一段硬編碼的方式定義流量控制規則如下:
private void initSystemRule() {
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(10);
rules.add(rule);
SystemRuleManager.loadRules(rules);
}
加載規則:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系統規則
AuthorityRuleManager.loadRules(List<AuthorityRule> rules); // 修改授權規則
3 sentinel 熔斷降級
1 什么是熔斷降級
熔斷降級對調用鏈路中不穩定的資源進行熔斷降級是保障高可用的重要措施之一。
由於調用關系的復雜性,如果調用鏈路中的某個資源不穩定,最終會導致請求發生堆積。Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)
2. 熔斷降級規則
熔斷降級規則包含下面幾個重要的屬性:
Field | 說明 | 默認值 |
resource | 資源名,即規則的作用對象 | |
grade | 熔斷策略,支持慢調用比例/異常比例/異常數策略 | 慢調用比例 |
count | 慢調用比例模式下為慢調用臨界 RT(超出該值計為慢調用);異常比例/異常數模式下為對應的閾值 | |
timeWindow | 熔斷時長,單位為 s | |
minRequestAmount | 熔斷觸發的最小請求數,請求數小於該值時即使異常比率超出閾值也不會熔斷(1.7.0 引入) | 5 |
statIntervalMs | 統計時長(單位為 ms),如 60*1000 代表分鍾級(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢調用比例閾值,僅慢調用比例模式有效(1.8.0 引入) |
3 幾種降級策略
我們通常用以下幾種降級策略:
-
平均響應時間 (DEGRADE_GRADE_RT):
當資源的平均響應時間超過閾值(DegradeRule 中的 count,以 ms 為單位)之后,資源進入准降級狀態。如果接下來 1s 內持續進入 5 個請求(即 QPS >= 5),它們的 RT 都持續超過這個閾值,那么在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地熔斷(拋出 DegradeException)。
注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。
-
異常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):
當資源的每秒異常總數占通過量的比值超過閾值(DegradeRule 中的 count)之后,資源進入降級狀態,即在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地返回。
異常比率的閾值范圍是 [0.0, 1.0],代表 0% - 100%。
-
異常數 (DEGRADE_GRADE_EXCEPTION_COUNT):
當資源近 1 分鍾的異常數目超過閾值之后會進行熔斷。
注意由於統計時間窗口是分鍾級別的,若 timeWindow 小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。
4 熔斷降級代碼實現
可以通過調用 DegradeRuleManager.loadRules() 方法來用硬編碼的方式定義流量控制規則。
@PostConstruct
public void initSentinelRule()
{
//熔斷規則: 5s內調用接口出現異常次數超過5的時候, 進行熔斷
List<DegradeRule> degradeRules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("queryGoodsInfo");
rule.setCount(5);
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);//熔斷規則
rule.setTimeWindow(5);
degradeRules.add(rule);
DegradeRuleManager.loadRules(degradeRules);
}
具體源碼,請參見瘋狂創客圈crazy-springcloud 源碼工程
5 控制台降級規則
配置
參數
Field | 說明 | 默認值 |
---|---|---|
resource | 資源名,即限流規則的作用對象 | |
count | 閾值 | |
grade | 降級模式,根據 RT 降級還是根據異常比例降級 | RT |
timeWindow | 降級的時間,單位為 s |
6 與Hystrix的熔斷對比:
Hystrix常用的線程池隔離會造成線程上下切換的overhead比較大;Hystrix使用的信號量隔離對某個資源調用的並發數進行控制,效果不錯,但是無法對慢調用進行自動降級;
Sentinel通過並發線程數的流量控制提供信號量隔離的功能;此外,Sentinel支持的熔斷降級維度更多,可對多種指標進行流控、熔斷,且提供了實時監控和控制面板,功能更為強大。
4 Sentinel 流控(限流)
流量控制(Flow Control),原理是監控應用流量的QPS或並發線程數等指標,當達到指定閾值時對流量進行控制,避免系統被瞬時的流量高峰沖垮,保障應用高可用性。
通過流控規則來指定允許該資源通過的請求次數,例如下面的代碼定義了資源 HelloWorld 每秒最多只能通過 20 個請求。 參考的規則定義如下:
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
一條限流規則主要由下面幾個因素組成,我們可以組合這些元素來實現不同的限流效果:
-
resource
:資源名,即限流規則的作用對象 -
count
: 限流閾值 -
grade
: 限流閾值類型(QPS 或並發線程數) -
limitApp
: 流控針對的調用來源,若為default
則不區分調用來源 -
strategy
: 調用關系限流策略 -
controlBehavior
: 流量控制效果(直接拒絕、Warm Up、勻速排隊)
基本的參數
資源名:唯一名稱,默認請求路徑
針對來源:Sentinel可以針對調用者進行限流,填寫微服務名,默認為default(不區分來源)
閾值類型/單機閾值:
1.QPS:每秒請求數,當前調用該api的QPS到達閾值的時候進行限流
2.線程數:當調用該api的線程數到達閾值的時候,進行限流
是否集群:是否為集群
流控的幾種strategy
:
1.直接:當api大達到限流條件時,直接限流
2.關聯:當關聯的資源到達閾值,就限流自己
3.鏈路:只記錄指定路上的流量,指定資源從入口資源進來的流量,如果達到閾值,就進行限流,api級別的限流
4.1 直接失敗模式
使用API進行資源定義
/**
* 限流實現方式一: 拋出異常的方式定義資源
*
* @param orderId
* @return
*/
@ApiOperation(value = "純代碼限流")
@GetMapping("/getOrder")
@ResponseBody
public String getOrder(@RequestParam(value = "orderId", required = false)String orderId)
{
Entry entry = null;
// 資源名
String resourceName = "getOrder";
try
{
// entry可以理解成入口登記
entry = SphU.entry(resourceName);
// 被保護的邏輯, 這里為訂單查詢接口
return "正常的業務邏輯 OrderInfo :" + orderId;
} catch (BlockException blockException)
{
// 接口被限流的時候, 會進入到這里
log.warn("---getOrder1接口被限流了---, exception: ", blockException);
return "接口限流, 返回空";
} finally
{
// SphU.entry(xxx) 需要與 entry.exit() 成對出現,否則會導致調用鏈記錄異常
if (entry != null)
{
entry.exit();
}
}
}
代碼限流規則
//限流規則 QPS mode,
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource("getOrder");
// QPS控制在2以內
rule1.setCount(2);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
網頁限流規則配置
選擇QPS,直接,快速失敗,單機閾值為2。
配置
,還是根據鏈路入口 |
根據資源本身 |
controlBehavior | 流控效果(直接拒絕 / 排隊等待 / 慢啟動模式) | 直接拒絕 |
測試
頻繁刷新請求,1秒訪問2次請求,正常,超過設置的閾值,將報默認的錯誤。
再次的1秒訪問2次請求,訪問正常。超過2次,訪問異常
4.2 關聯模式
調用關系包括調用方、被調用方;一個方法又可能會調用其它方法,形成一個調用鏈路的層次關系。Sentinel 通過 NodeSelectorSlot
建立不同資源間的調用的關系,並且通過 ClusterBuilderSlot
記錄每個資源的實時統計信息。
當兩個資源之間具有資源爭搶或者依賴關系的時候,這兩個資源便具有了關聯。
比如對數據庫同一個字段的讀操作和寫操作存在爭搶,讀的速度過高會影響寫得速度,寫的速度過高會影響讀的速度。如果放任讀寫操作爭搶資源,則爭搶本身帶來的開銷會降低整體的吞吐量。可使用關聯限流來避免具有關聯關系的資源之間過度的爭搶.
舉例來說,read_db
和 write_db
這兩個資源分別代表數據庫讀寫,我們可以給 read_db
設置限流規則來達到寫優先的目的。具體的方法:
設置 `strategy` 為 `RuleConstant.STRATEGY_RELATE`
設置 `refResource` 為 `write_db`。
這樣當寫庫操作過於頻繁時,讀數據的請求會被限流。
還有一個例子,電商的 下訂單 和 支付兩個操作,需要優先保障 支付, 可以根據 支付接口的 流量閾值,來對訂單接口進行限制,從而保護支付的目的。
使用注解進行資源定義
添加2個請求
@SentinelResource(value = "test1", blockHandler = "exceptionHandler")
@GetMapping("/test1")
public String test1()
{
log.info(Thread.currentThread().getName() + "\t" + "...test1");
return "-------hello baby,i am test1";
}
// Block 異常處理函數,參數最后多一個 BlockException,其余與原函數一致.
public String exceptionHandler(BlockException ex)
{
// Do some log here.
ex.printStackTrace();
log.info(Thread.currentThread().getName() + "\t" + "...exceptionHandler");
return String.format("error: test1 is not OK");
}
@SentinelResource(value = "test1_ref")
@GetMapping("/test1_ref")
public String test1_ref()
{
log.info(Thread.currentThread().getName() + "\t" + "...test1_related");
return "-------hello baby,i am test1_ref";
}
代碼配置關聯限流規則
// 關聯模式流控 QPS控制在1以內
String refResource = "test1_ref";
FlowRule rRule = new FlowRule("test1")
.setCount(1) // QPS控制在1以內
.setStrategy(RuleConstant.STRATEGY_RELATE)
.setRefResource(refResource);
rules.add(rRule);
FlowRuleManager.loadRules(rules);
網頁限流規則配置
測試
選擇QPS,單機閾值為1,選擇關聯,關聯資源為/test_ref,這里用Jmeter模擬高並發,請求/test_ref。
在大批量線程高並發訪問/test_ref,導致/test失效了
鏈路類型的關聯也類似,就不再演示了。多個請求調用同一微服務。
4.3 Warm up(預熱)模式
當流量突然增大的時候,我們常常會希望系統從空閑狀態到繁忙狀態的切換的時間長一些。即如果系統在此之前長期處於空閑的狀態,我們希望處理請求的數量是緩步的增多,經過預期的時間以后,到達系統處理請求個數的最大值。Warm Up(冷啟動,預熱)模式就是為了實現這個目的的。
默認 coldFactor 為 3,即請求 QPS 從 threshold / 3 開始,經預熱時長逐漸升至設定的 QPS 閾值。
使用注解定義資源
@SentinelResource(value = "testWarmUP", blockHandler = "exceptionHandlerOfWarmUp")
@GetMapping("/testWarmUP")
public String testWarmUP()
{
log.info(Thread.currentThread().getName() + "\t" + "...test1");
return "-------hello baby,i am testWarmUP";
}
代碼限流規則
FlowRule warmUPRule = new FlowRule();
warmUPRule.setResource("testWarmUP");
warmUPRule.setCount(20);
warmUPRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
warmUPRule.setLimitApp("default");
warmUPRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
warmUPRule.setWarmUpPeriodSec(10);
網頁限流規則配置
先在單機閾值10/3,3的時候,預熱10秒后,慢慢將閾值升至20。剛開始刷/testWarmUP,會出現默認錯誤,預熱時間到了后,閾值增加,沒超過閾值刷新,請求正常。
通常冷啟動的過程系統允許通過的 QPS 曲線如下圖所示:
如秒殺系統在開啟瞬間,會有很多流量上來,很可能把系統打死,預熱方式就是為了保護系統,可慢慢的把流量放進來,慢慢的把閾值增長到設置的閾值。
通過jmeter進行測試
4.4 排隊等待模式
勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶算法。閾值必須設置為QPS。
這種方式主要用於處理間隔性突發的流量,例如消息隊列。想象一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閑狀態,我們希望系統能夠在接下來的空閑期間逐漸處理這些請求,而不是在第一秒直接拒絕多余的請求。
某瞬時來了大流量的請求, 而如果此時要處理所有請求,很可能會導致系統負載過高,影響穩定性。但其實可能后面幾秒之內都沒有消息投遞,若直接把多余的消息丟掉則沒有充分利用系統處理消息的能力。Sentinel的Rate Limiter模式能在某一段時間間隔內以勻速方式處理這樣的請求, 充分利用系統的處理能力, 也就是削峰填谷, 保證資源的穩定性.
Sentinel會以固定的間隔時間讓請求通過, 訪問資源。當請求到來的時候,如果當前請求距離上個通過的請求通過的時間間隔不小於預設值,則讓當前請求通過;否則,計算當前請求的預期通過時間,如果該請求的預期通過時間小於規則預設的 timeout 時間,則該請求會等待直到預設時間到來通過;反之,則馬上拋出阻塞異常。
使用Sentinel的這種策略, 簡單點說, 就是使用一個時間段(比如20s的時間)處理某一瞬時產生的大量請求, 起到一個削峰填谷的作用, 從而充分利用系統的處理能力, 下圖能很形象的展示這種場景: X軸代表時間, Y軸代表系統處理的請求.
示例
模擬2個用戶同時並發的訪問資源,發出100個請求,
如果設置QPS閾值為1, 拒絕策略修改為Rate Limiter勻速RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER方式, 還需要設置setMaxQueueingTimeMs(20 * 1000)表示每一請求最長等待時間, 這里等待時間大一點, 以保證讓所有請求都能正常通過;
假設這里設置的排隊等待時間過小的話, 導致排隊等待的請求超時而拋出異常BlockException, 最終結果可能是這100個並發請求中只有一個請求或幾個才能正常通過, 所以使用這種模式得根據訪問資源的耗時時間決定排隊等待時間. 按照目前這種設置, QPS閾值為10的話, 每一個請求相當於是以勻速100ms左右通過.
使用注解定義資源
@SentinelResource(value = "testLineUp",
blockHandler = "exceptionHandlerOftestLineUp")
@GetMapping("/testLineUp")
public String testLineUp()
{
log.info(Thread.currentThread().getName() + "\t" + "...test1");
return "-------hello baby,i am testLineUp";
}
代碼限流規則
FlowRule lineUpRule = new FlowRule();
lineUpRule.setResource("testLineUp");
lineUpRule.setCount(10);
lineUpRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
lineUpRule.setLimitApp("default");
lineUpRule.setMaxQueueingTimeMs(20 * 1000);
// CONTROL_BEHAVIOR_DEFAULT means requests more than threshold will be rejected immediately.
// CONTROL_BEHAVIOR_DEFAULT將超過閾值的流量立即拒絕掉.
lineUpRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
rules.add(lineUpRule);
網頁限流規則配置
通過jmeter進行測試
4.5 熱點規則 (ParamFlowRule)
何為熱點?熱點即經常訪問的數據。很多時候我們希望統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。比如:
- 商品 ID 為參數,統計一段時間內最常購買的商品 ID 並進行限制
- 用戶 ID 為參數,針對一段時間內頻繁訪問的用戶 ID 進行限制 熱點參數限流會統計傳入參數中的熱點參數,並根據配置的限流閾值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流可以看做是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。 使用該規則需要引入依賴:
熱點參數規則(ParamFlowRule
)類似於流量控制規則(FlowRule
):
屬性 | 說明 | 默認值 |
---|---|---|
resource | 資源名,必填 | |
count | 限流閾值,必填 | |
grade | 限流模式 | QPS 模式 |
durationInSec | 統計窗口時間長度(單位為秒),1.6.0 版本開始支持 | 1s |
controlBehavior | 流控效果(支持快速失敗和勻速排隊模式),1.6.0 版本開始支持 | 快速失敗 |
maxQueueingTimeMs | 最大排隊等待時長(僅在勻速排隊模式生效),1.6.0 版本開始支持 | 0ms |
paramIdx | 熱點參數的索引,必填,對應 SphU.entry(xxx, args) 中的參數索引位置 |
|
paramFlowItemList | 參數例外項,可以針對指定的參數值單獨設置限流閾值,不受前面 count 閾值的限制。僅支持基本類型和字符串類型 |
|
clusterMode | 是否是集群參數流控規則 | false |
clusterConfig | 集群流控相關配置 |
自定義資源
@GetMapping("/byHotKey")
@SentinelResource(value = "byHotKey",
blockHandler = "userAccessError")
public String test4(@RequestParam(value = "userId", required = false) String userId,
@RequestParam(value = "goodId", required = false) int goodId)
{
log.info(Thread.currentThread().getName() + "\t" + "...byHotKey");
return "-----------by HotKey: UserId";
}
限流規則代碼:
可以通過 ParamFlowRuleManager 的 loadRules 方法更新熱點參數規則,下面是官方實例:
ParamFlowRule rule = new ParamFlowRule(resourceName)
.setParamIdx(0)
.setCount(5);
// 針對 int 類型的參數 PARAM_B,單獨設置限流 QPS 閾值為 10,而不是全局的閾值 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 pRule = new ParamFlowRule("byHotKey")
.setParamIdx(1)
.setCount(1);
// 針對 參數值1000,單獨設置限流 QPS 閾值為 5,而不是全局的閾值 1.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(1000))
.setClassType(int.class.getName())
.setCount(5);
pRule.setParamFlowItemList(Collections.singletonList(item));
ParamFlowRuleManager.loadRules(Collections.singletonList(pRule));
網頁限流規則配置
測試:
請參見視頻
5. Sentinel 系統保護
系統保護的目的
在開始之前,我們先了解一下系統保護的目的:
- 保證系統不被拖垮
- 在系統穩定的前提下,保持系統的吞吐量
長期以來,系統保護的思路是根據硬指標,即系統的負載 (load1) 來做系統過載保護。當系統負載高於某個閾值,就禁止或者減少流量的進入;當 load 開始好轉,則恢復流量的進入。這個思路給我們帶來了不可避免的兩個問題:
- load 是一個“結果”,如果根據 load 的情況來調節流量的通過率,那么就始終有延遲性。也就意味着通過率的任何調整,都會過一段時間才能看到效果。當前通過率是使 load 惡化的一個動作,那么也至少要過 1 秒之后才能觀測到;同理,如果當前通過率調整是讓 load 好轉的一個動作,也需要 1 秒之后才能繼續調整,這樣就浪費了系統的處理能力。所以我們看到的曲線,總是會有抖動。
- 恢復慢。想象一下這樣的一個場景(真實),出現了這樣一個問題,下游應用不可靠,導致應用 RT 很高,從而 load 到了一個很高的點。過了一段時間之后下游應用恢復了,應用 RT 也相應減少。這個時候,其實應該大幅度增大流量的通過率;但是由於這個時候 load 仍然很高,通過率的恢復仍然不高。
系統保護的目標是 在系統不被拖垮的情況下,提高系統的吞吐率,而不是 load 一定要到低於某個閾值。如果我們還是按照固有的思維,超過特定的 load 就禁止流量進入,系統 load 恢復就放開流量,這樣做的結果是無論我們怎么調參數,調比例,都是按照果來調節因,都無法取得良好的效果。
Sentinel 在系統自適應保護的做法是,用 load1 作為啟動自適應保護的因子,而允許通過的流量由處理請求的能力,即請求的響應時間以及當前系統正在處理的請求速率來決定。
系統保護規則的應用
系統規則支持以下的模式:
-
Load 自適應(僅對 Linux/Unix-like 機器生效):系統的 load1 作為啟發指標,進行自適應系統保護。當系統 load1 超過設定的啟發值,且系統當前的並發線程數超過估算的系統容量時才會觸發系統保護(BBR 階段)。系統容量由系統的
maxQps * minRt
估算得出。設定參考值一般是CPU cores * 2.5
。 -
CPU usage(1.5.0+ 版本):當系統 CPU 使用率超過閾值即觸發系統保護(取值范圍 0.0-1.0),比較靈敏。
-
平均 RT:當單台機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。
-
並發線程數:當單台機器上所有入口流量的並發線程數達到閾值即觸發系統保護。
-
入口 QPS:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。
系統保護規則是從應用級別的入口流量進行控制,從單台機器的 load、CPU 使用率、平均 RT、入口 QPS 和並發線程數等幾個維度監控應用指標,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。
系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量生效。入口流量指的是進入應用的流量(EntryType.IN
),比如 Web 服務或 Dubbo 服務端接收的請求,都屬於入口流量。
系統規則的參數說明:
- highestSystemLoad 最大的 load1,參考值 -1 (不生效)
- avgRt 所有入口流量的平均響應時間 -1 (不生效)
- maxThread 入口流量的最大並發數 -1 (不生效)
- qps 所有入口資源的 QPS -1 (不生效)
硬編碼的方式定義流量控制規則如下:
List<SystemRule> srules = new ArrayList<>();
SystemRule srule = new SystemRule();
srule.setAvgRt(3000);
srules.add(srule);
SystemRuleManager.loadRules(srules);
網頁限流規則配置
6 黑白名單規則
很多時候,我們需要根據調用方來限制資源是否通過,這時候可以使用 Sentinel 的訪問控制(黑白名單)的功能。黑白名單根據資源的請求來源(origin)限制資源是否通過,若配置白名單則只有請求來源位於白名單內時才可通過;若配置黑名單則請求來源位於黑名單時不通過,其余的請求通過。
調用方信息通過 ContextUtil.enter(resourceName, origin) 方法中的 origin 參數傳入。
訪問控制規則 (AuthorityRule)
授權規則,即黑白名單規則(AuthorityRule)非常簡單,主要有以下配置項:
- resource:資源名,即限流規則的作用對象
- limitApp:對應的黑名單/白名單,不同 origin 用 , 分隔,如 appA,appB
- strategy:限制模式,AUTHORITY_WHITE 為白名單模式,AUTHORITY_BLACK 為黑名單模式,默認為白名單模式 比如我們希望控制對資源 test 的訪問設置白名單,只有來源為 appA 和 appB 的請求才可通過,則可以配置如下白名單規則:
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
7 核心組件
Resource
resource是sentinel中最重要的一個概念,sentinel通過資源來保護具體的業務代碼或其他后方服務。sentinel把復雜的邏輯給屏蔽掉了,用戶只需要為受保護的代碼或服務定義一個資源,然后定義規則就可以了,剩下的通通交給sentinel來處理了。並且資源和規則是解耦的,規則甚至可以在運行時動態修改。定義完資源后,就可以通過在程序中埋點來保護你自己的服務了,埋點的方式有兩種:
- try-catch 方式(
通過 SphU.entry(...)
),當 catch 到BlockException時執行異常處理(或fallback) - if-else 方式(
通過 SphO.entry(...)
),當返回 false 時執行異常處理(或fallback)
以上這兩種方式都是通過硬編碼的形式定義資源然后進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,sentinel加入了注解的支持,可以通過注解來定義資源,具體的注解為:SentinelResource 。通過注解除了可以定義資源外,還可以指定 blockHandler 和 fallback 方法。
在sentinel中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper。顧名思義,StringResourceWrapper 是通過對一串字符串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。
Context
Context是對資源操作時的上下文環境,每個資源操作(針對Resource進行的entry/exit
)必須屬於一個Context,如果程序中未指定Context,會創建name為"sentinel_default_context"的默認Context。一個Context生命周期內可能有多個資源操作,Context生命周期內的最后一個資源exit時會清理該Context,這也預示這整個Context生命周期的結束。Context主要屬性如下:
public class Context {
// context名字,默認名字 "sentinel_default_context"
private final String name;
// context入口節點,每個context必須有一個entranceNode
private DefaultNode entranceNode;
// context當前entry,Context生命周期中可能有多個Entry,所有curEntry會有變化
private Entry curEntry;
// The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
private String origin = "";
private final boolean async;
}
注意:一個Context生命期內Context只能初始化一次,因為是存到ThreadLocal中,並且只有在非null時才會進行初始化。
如果想在調用 SphU.entry() 或 SphO.entry() 前,自定義一個context,則通過ContextUtil.enter()方法來創建。context是保存在ThreadLocal中的,每次執行的時候會優先到ThreadLocal中獲取,為null時會調用 MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType())
創建一個context。當Entry執行exit方法時,如果entry的parent節點為null,表示是當前Context中最外層的Entry了,此時將ThreadLocal中的context清空。
Context的創建與銷毀
首先我們要清楚的一點就是,每次執行entry()方法,試圖沖破一個資源時,都會生成一個上下文。這個上下文中會保存着調用鏈的根節點和當前的入口。
Context是通過ContextUtil創建的,具體的方法是trueEntry,代碼如下:
protected static Context trueEnter(String name, String origin) {
// 先從ThreadLocal中獲取
Context context = contextHolder.get();
if (context == null) {
// 如果ThreadLocal中獲取不到Context
// 則根據name從map中獲取根節點,只要是相同的資源名,就能直接從map中獲取到node
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 省略部分代碼
try {
LOCK.lock();
node = contextNameNodeMap.get(name);
if (node == null) {
// 省略部分代碼
// 創建一個新的入口節點
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
Constants.ROOT.addChild(node);
// 省略部分代碼
}
} finally {
LOCK.unlock();
}
}
// 創建一個新的Context,並設置Context的根節點,即設置EntranceNode
context = new Context(node, name);
context.setOrigin(origin);
// 將該Context保存到ThreadLocal中去
contextHolder.set(context);
}
return context;
}
上面的代碼中我省略了部分代碼,只保留了核心的部分。從源碼中還是可以比較清晰的看出生成Context的過程:
- 1.先從ThreadLocal中獲取,如果能獲取到直接返回,如果獲取不到則繼續第2步
- 2.從一個static的map中根據上下文的名稱獲取,如果能獲取到則直接返回,否則繼續第3步
- 3.加鎖后進行一次double check,如果還是沒能從map中獲取到,則創建一個EntranceNode,並把該EntranceNode添加到一個全局的ROOT節點中去,然后將該節點添加到map中去(這部分代碼在上述代碼中省略了)
- 4.根據EntranceNode創建一個上下文,並將該上下文保存到ThreadLocal中去,下一個請求可以直接獲取
那保存在ThreadLocal中的上下文什么時候會清除呢?從代碼中可以看到具體的清除工作在ContextUtil的exit方法中,當執行該方法時,會將保存在ThreadLocal中的context對象清除,具體的代碼非常簡單,這里就不貼代碼了。
那ContextUtil.exit方法什么時候會被調用呢?有兩種情況:一是主動調用ContextUtil.exit的時候,二是當一個入口Entry要退出,執行該Entry的trueExit方法的時候,此時會觸發ContextUtil.exit的方法。但是有一個前提,就是當前Entry的父Entry為null時,此時說明該Entry已經是最頂層的根節點了,可以清除context。
Entry
剛才在Context身影中也看到了Entry的出現,現在就談談Entry。每次執行 SphU.entry() 或 SphO.entry() 都會返回一個Entry,Entry表示一次資源操作,內部會保存當前invocation信息。在一個Context生命周期中多次資源操作,也就是對應多個Entry,這些Entry形成parent/child結構保存在Entry實例中,entry類CtEntry結構如下:
class CtEntry extends Entry {
protected Entry parent = null;
protected Entry child = null;
protected ProcessorSlot<Object> chain;
protected Context context;
}
public abstract class Entry implements AutoCloseable {
private long createTime;
private Node curNode;
/**
* {@link Node} of the specific origin, Usually the origin is the Service Consumer.
*/
private Node originNode;
private Throwable error; // 是否出現異常
protected ResourceWrapper resourceWrapper; // 資源信息
}
Entry實例代碼中出現了Node,這個又是什么東東呢 😦,咱們接着往下看:
DefaultNode
Node(關於StatisticNode的討論放到下一小節)默認實現類DefaultNode,該類還有一個子類EntranceNode;context有一個entranceNode屬性,Entry中有一個curNode屬性。
- EntranceNode:該類的創建是在初始化Context時完成的(ContextUtil.trueEnter方法),注意該類是針對Context維度的,也就是一個context有且僅有一個EntranceNode。
- DefaultNode:該類的創建是在NodeSelectorSlot.entry完成的,當不存在context.name對應的DefaultNode時會新建(new DefaultNode(resourceWrapper, null),對應resouce)並保存到本地緩存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);獲取到context.name對應的DefaultNode后會將該DefaultNode設置到當前context的curEntry.curNode屬性,也就是說,在NodeSelectorSlot中是一個context有且僅有一個DefaultNode。
看到這里,你是不是有疑問?為什么一個context有且僅有一個DefaultNode,我們的resouece跑哪去了呢,其實,這里的一個context有且僅有一個DefaultNode是在NodeSelectorSlot范圍內,NodeSelectorSlot是ProcessorSlotChain中的一環,獲取ProcessorSlotChain是根據Resource維度來的。總結為一句話就是:針對同一個Resource,多個context對應多個DefaultNode;針對不同Resource,(不管是否是同一個context)對應多個不同DefaultNode。這還沒看明白 : (,好吧,我不bb了,上圖吧:
public class DefaultNode extends StatisticNode {
private ResourceWrapper id;
/**
* The list of all child nodes.
* 子節點集合
/
private volatile Set
/
* Associated cluster node.
*/
private ClusterNode clusterNode;
}
一個Resouce只有一個clusterNode,多個defaultNode對應一個clusterNode,如果defaultNode.clusterNode為null,則在ClusterBuilderSlot.entry中會進行初始化。
同一個Resource,對應同一個ProcessorSlotChain,這塊處理邏輯在lookProcessChain方法中,如下:
ProcessorSlot