瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備 【博客園總入口 】javascript
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高併發核心編程》 大廠必備 + 大廠必備 + 大廠必備 【博客園總入口 】html
入大廠+漲工資必備: 高併發【 億級流量IM實戰】 實戰系列 【 SpringCloud Nginx秒殺】 實戰系列 【博客園總入口 】java
推薦閱讀 |
---|
nacos 實戰(史上最全) |
sentinel (史上最全+入門教程) |
springcloud + webflux 高併發實戰 |
Webflux(史上最全) |
SpringCloud gateway (史上最全) |
和 1000+ Java 高併發 發燒友、 一塊兒 交流 、學習、入大廠、作架構,GO |
開發的緣由,須要對吞吐量(TPS)、QPS、併發數、響應時間(RT)幾個概念作下了解,查自百度百科,記錄以下:node
- 響應時間(RT)
響應時間是指系統對請求做出響應的時間。直觀上看,這個指標與人對軟件性能的主觀感覺是很是一致的,由於它完整地記錄了整個計算機系統處理請求的時間。因爲一個系統一般會提供許多功能,而不一樣功能的處理邏輯也千差萬別,於是不一樣功能的響應時間也不盡相同,甚至同一功能在不一樣輸入數據的狀況下響應時間也不相同。因此,在討論一個系統的響應時間時,人們一般是指該系統全部功能的平均時間或者全部功能的最大響應時間。固然,每每也須要對每一個或每組功能討論其平均響應時間和最大響應時間。
對於單機的沒有併發操做的應用系統而言,人們廣泛認爲響應時間是一個合理且準確的性能指標。須要指出的是,響應時間的絕對值並不能直接反映軟件的性能的高低,軟件性能的高低實際上取決於用戶對該響應時間的接受程度。對於一個遊戲軟件來講,響應時間小於100毫秒應該是不錯的,響應時間在1秒左右可能屬於勉強能夠接受,若是響應時間達到3秒就徹底難以接受了。而對於編譯系統來講,完整編譯一個較大規模軟件的源代碼可能須要幾十分鐘甚至更長時間,但這些響應時間對於用戶來講都是能夠接受的。- 吞吐量(Throughput)
吞吐量是指系統在單位時間內處理請求的數量。對於無併發的應用系統而言,吞吐量與響應時間成嚴格的反比關係,實際上此時吞吐量就是響應時間的倒數。前面已經說過,對於單用戶的系統,響應時間(或者系統響應時間和應用延遲時間)能夠很好地度量系統的性能,但對於併發系統,一般須要用吞吐量做爲性能指標。
對於一個多用戶的系統,若是隻有一個用戶使用時系統的平均響應時間是t,當有你n個用戶使用時,每一個用戶看到的響應時間一般並非n×t,而每每比n×t小不少(固然,在某些特殊狀況下也可能比n×t大,甚至大不少)。這是由於處理每一個請求須要用到不少資源,因爲每一個請求的處理過程當中有許多不走難以併發執行,這致使在具體的一個時間點,所佔資源每每並很少。也就是說在處理單個請求時,在每一個時間點均可能有許多資源被閒置,當處理多個請求時,若是資源配置合理,每一個用戶看到的平均響應時間並不隨用戶數的增長而線性增長。實際上,不一樣系統的平均響應時間隨用戶數增長而增加的速度也不大相同,這也是採用吞吐量來度量併發系統的性能的主要緣由。通常而言,吞吐量是一個比較通用的指標,兩個具備不一樣用戶數和用戶使用模式的系統,若是其最大吞吐量基本一致,則能夠判斷兩個系統的處理能力基本一致。- 併發用戶數
併發用戶數是指系統能夠同時承載的正常使用系統功能的用戶的數量。與吞吐量相比,併發用戶數是一個更直觀但也更籠統的性能指標。實際上,併發用戶數是一個很是不許確的指標,由於用戶不一樣的使用模式會致使不一樣用戶在單位時間發出不一樣數量的請求。一網站系統爲例,假設用戶只有註冊後才能使用,但註冊用戶並非每時每刻都在使用該網站,所以具體一個時刻只有部分註冊用戶同時在線,在線用戶就在瀏覽網站時會花不少時間閱讀網站上的信息,於是具體一個時刻只有部分在線用戶同時向系統發出請求。這樣,對於網站系統咱們會有三個關於用戶數的統計數字:註冊用戶數、在線用戶數和同時發請求用戶數。因爲註冊用戶可能長時間不登錄網站,使用註冊用戶數做爲性能指標會形成很大的偏差。而在線用戶數和同事發請求用戶數均可以做爲性能指標。相比而言,以在線用戶做爲性能指標更直觀些,而以同時發請求用戶數做爲性能指標更準確些。- QPS每秒查詢率(Query Per Second)
每秒查詢率QPS是對一個特定的查詢服務器在規定時間內所處理流量多少的衡量標準,在因特網上,做爲域名系統服務器的機器的性能常常用每秒查詢率來衡量。對應fetches/sec,即每秒的響應請求數,也便是最大吞吐能力。 (看來是相似於TPS,只是應用於特定場景的吞吐量)
Sentinel是阿里開源的項目,提供了流量控制、熔斷降級、系統負載保護等多個維度來保障服務之間的穩定性。
官網:https://github.com/alibaba/Sentinel/wikigit
2012年,Sentinel誕生於阿里巴巴,其主要目標是流量控制。2013-2017年,Sentinel迅速發展,併成爲阿里巴巴全部微服務的基本組成部分。 它已在6000多個應用程序中使用,涵蓋了幾乎全部核心電子商務場景。2018年,Sentinel演變爲一個開源項目。2020年,Sentinel Golang發佈。github
豐富的應用場景 :Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即
突發流量控制在系統容量能夠承受的範圍)、消息削峯填谷、集羣流量控制、實時熔斷下游不可用應用等。
完備的實時監控 :Sentinel 同時提供實時的監控功能。您能夠在控制檯中看到接入應用的單臺機
器秒級數據,甚至 500 臺如下規模的集羣的彙總運行狀況。
普遍的開源生態 :Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring
Cloud、Dubbo、gRPC 的整合。您只須要引入相應的依賴並進行簡單的配置便可快速地接入Sentinel。web
完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您能夠經過實現擴展接口來快
速地定製邏輯。例如定製規則管理、適配動態數據源等。面試
Sentinel的生態圈算法
關於Sentinel與Hystrix的區別見:https://yq.aliyun.com/articles/633786/spring
到這已經學習Sentinel的基本的使用,在不少的特性和Hystrix有不少相似的功能。如下是Sentinel和Hystrix的對比。
Sentinel 的使用能夠分爲兩個部分:
控制檯(Dashboard):控制檯主要負責管理推送規則、監控、集羣限流分配管理、機器發現等。
核心庫(Java 客戶端):不依賴任何框架/庫,可以運行於 Java 7 及以上的版本的運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
在這裏咱們看下控制檯的使用
您能夠從 release 頁面 下載最新版本的控制檯 jar 包。
您也能夠從最新版本的源碼自行構建 Sentinel 控制檯:
mvn clean package
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 配置文件, 以後作到開機啓動
/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 的方法
Sentinel 能夠簡單的分爲 Sentinel 核心庫和 Dashboard。核心庫不依賴 Dashboard,可是結合
Dashboard 能夠取得最好的效果。
使用 Sentinel 來進行熔斷保護,主要分爲幾個步驟:
定義資源
資源:能夠是任何東西,一個服務,服務裏的方法,甚至是一段代碼。
定義規則
規則:Sentinel 支持如下幾種規則:流量控制規則、熔斷降級規則、系統保護規則、來源訪問控制規則
和 熱點參數規則。
檢驗規則是否生效
Sentinel 的全部規則均可以在內存態中動態地查詢及修改,修改以後當即生效. 先把可能須要保護的資源定義好,以後再配置規則。
也能夠理解爲,只要有了資源,咱們就能夠在任什麼時候候靈活地定義各類流量控制規則。在編碼的時候,只須要考慮這個代碼是否須要保護,若是須要保
護,就將之定義爲一個資源。
資源是 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!"); } } }
也可使用Sentinel提供的註解@SentinelResource來定義資源,實例以下:
@SentinelResource("HelloWorld") public void helloWorld() { // 資源中的邏輯 System.out.println("hello world"); }
注意:註解方式埋點不支持 private 方法。
@SentinelResource
用於定義資源,並提供可選的異常處理和 fallback 配置項。 @SentinelResource
註解包含如下屬性:
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 會生效。
返回值類型必須與原函數返回值類型一致;
方法參數列表須要和原函數一致,或者能夠額外多一個 Throwable
類型的參數用於接收對應的異常。
fallback 函數默認須要和原方法在同一個類中。若但願使用其餘類的函數,則能夠指定 fallbackClass爲對應的類的 Class 對象,注意對應的函數必需爲 static 函數,不然沒法解析。
Throwable
類型的參數用於接收對應的異常。規則主要有流控規則、 熔斷降級規則、系統規則、權限規則、熱點參數規則等:
一段硬編碼的方式定義流量控制規則以下:
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); // 修改受權規則
熔斷降級對調用鏈路中不穩定的資源進行熔斷降級是保障高可用的重要措施之一。
因爲調用關係的複雜性,若是調用鏈路中的某個資源不穩定,最終會致使請求發生堆積。Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而致使級聯錯誤。當資源被降級後,在接下來的降級時間窗口以內,對該資源的調用都自動熔斷(默認行爲是拋出 DegradeException)
熔斷降級規則包含下面幾個重要的屬性:
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 引入) |
咱們一般用如下幾種降級策略:
平均響應時間 (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,則結束熔斷狀態後仍可能再進入熔斷狀態。
能夠經過調用 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 源碼工程
配置
參數
Field | 說明 | 默認值 |
---|---|---|
resource | 資源名,即限流規則的做用對象 | |
count | 閾值 | |
grade | 降級模式,根據 RT 降級仍是根據異常比例降級 | RT |
timeWindow | 降級的時間,單位爲 s |
Hystrix經常使用的線程池隔離會形成線程上下切換的overhead比較大;Hystrix使用的信號量隔離對某個資源調用的併發數進行控制,效果不錯,可是沒法對慢調用進行自動降級;
Sentinel經過併發線程數的流量控制提供信號量隔離的功能;此外,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級別的限流
/** * 限流實現方式一: 拋出異常的方式定義資源 * * @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。
配置
![流控規則](
參數
Field | 說明 | 默認值 |
---|---|---|
resource | 資源名,資源名是限流規則的做用對象 | |
count | 限流閾值 | |
grade | 限流閾值類型,QPS 或線程數模式 | QPS 模式 |
limitApp | 流控針對的調用來源 | default ,表明不區分調用來源 |
strategy | 判斷的根據是資源自身,仍是根據其它關聯資源 (refResource ),仍是根據鏈路入口 |
根據資源自己 |
controlBehavior | 流控效果(直接拒絕 / 排隊等待 / 慢啓動模式) | 直接拒絕 |
頻繁刷新請求,1秒訪問2次請求,正常,超過設置的閾值,將報默認的錯誤。
再次的1秒訪問2次請求,訪問正常。超過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失效了
鏈路類型的關聯也相似,就再也不演示了。多個請求調用同一微服務。
當流量忽然增大的時候,咱們經常會但願系統從空閒狀態到繁忙狀態的切換的時間長一些。即若是系統在此以前長期處於空閒的狀態,咱們但願處理請求的數量是緩步的增多,通過預期的時間之後,到達系統處理請求個數的最大值。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 曲線以下圖所示:
如秒殺系統在開啓瞬間,會有不少流量上來,極可能把系統打死,預熱方式就是爲了保護系統,可慢慢的把流量放進來,慢慢的把閾值增加到設置的閾值。
勻速排隊(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);
何爲熱點?熱點即常常訪問的數據。不少時候咱們但願統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。好比:
熱點參數規則(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));
請參見視頻
在開始以前,咱們先了解一下系統保護的目的:
長期以來,系統保護的思路是根據硬指標,即系統的負載 (load1) 來作系統過載保護。當系統負載高於某個閾值,就禁止或者減小流量的進入;當 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 服務端接收的請求,都屬於入口流量。
系統規則的參數說明:
硬編碼的方式定義流量控制規則以下:
List<SystemRule> srules = new ArrayList<>(); SystemRule srule = new SystemRule(); srule.setAvgRt(3000); srules.add(srule); SystemRuleManager.loadRules(srules);
不少時候,咱們須要根據調用方來限制資源是否經過,這時候可使用 Sentinel 的訪問控制(黑白名單)的功能。黑白名單根據資源的請求來源(origin)限制資源是否經過,若配置白名單則只有請求來源位於白名單內時纔可經過;若配置黑名單則請求來源位於黑名單時不經過,其他的請求經過。
調用方信息經過 ContextUtil.enter(resourceName, origin) 方法中的 origin 參數傳入。
受權規則,即黑白名單規則(AuthorityRule)很是簡單,主要有如下配置項:
AuthorityRule rule = new AuthorityRule(); rule.setResource("test"); rule.setStrategy(RuleConstant.AUTHORITY_WHITE); rule.setLimitApp("appA,appB"); AuthorityRuleManager.loadRules(Collections.singletonList(rule));
resource是sentinel中最重要的一個概念,sentinel經過資源來保護具體的業務代碼或其餘後方服務。sentinel把複雜的邏輯給屏蔽掉了,用戶只須要爲受保護的代碼或服務定義一個資源,而後定義規則就能夠了,剩下的統統交給sentinel來處理了。而且資源和規則是解耦的,規則甚至能夠在運行時動態修改。定義完資源後,就能夠經過在程序中埋點來保護你本身的服務了,埋點的方式有兩種:
經過 SphU.entry(...)
),當 catch 到BlockException時執行異常處理(或fallback)經過 SphO.entry(...)
),當返回 false 時執行異常處理(或fallback)以上這兩種方式都是經過硬編碼的形式定義資源而後進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,sentinel加入了註解的支持,能夠經過註解來定義資源,具體的註解爲:SentinelResource 。經過註解除了能夠定義資源外,還能夠指定 blockHandler 和 fallback 方法。
在sentinel中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper。顧名思義,StringResourceWrapper 是經過對一串字符串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。
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清空。
首先咱們要清楚的一點就是,每次執行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的過程:
那保存在ThreadLocal中的上下文何時會清除呢?從代碼中能夠看到具體的清除工做在ContextUtil的exit方法中,當執行該方法時,會將保存在ThreadLocal中的context對象清除,具體的代碼很是簡單,這裏就不貼代碼了。
那ContextUtil.exit方法何時會被調用呢?有兩種狀況:一是主動調用ContextUtil.exit的時候,二是當一個入口Entry要退出,執行該Entry的trueExit方法的時候,此時會觸發ContextUtil.exit的方法。可是有一個前提,就是當前Entry的父Entry爲null時,此時說明該Entry已是最頂層的根節點了,能夠清除context。
剛纔在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,這個又是什麼東東呢 😦,我們接着往下看:
Node(關於StatisticNode的討論放到下一小節)默認實現類DefaultNode,該類還有一個子類EntranceNode;context有一個entranceNode屬性,Entry中有一個curNode屬性。
看到這裏,你是否是有疑問?爲何一個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